dsh-remote-plugin 0.5.2 → 0.5.4

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/public/index.html CHANGED
@@ -3,10 +3,11 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no">
6
- <meta name="theme-color" content="#0b0e1a">
6
+ <meta name="theme-color" content="#05348B">
7
7
  <meta name="mobile-web-app-capable" content="yes">
8
8
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
9
9
  <title>DSH Remote</title>
10
+ <script>/* 首帧前应用皮肤, 避免闪白/闪黑 */ (function(){try{var t=localStorage.getItem('dshTheme');if(t!=='default'&&t!=='dark'&&t!=='light'&&t!=='neutral'){t=window.matchMedia('(prefers-color-scheme: light)').matches?'light':'default'}document.documentElement.setAttribute('data-theme',t)}catch(e){}})()</script>
10
11
  <link rel="manifest" href="manifest.webmanifest">
11
12
  <link rel="icon" href="icon.svg" type="image/svg+xml">
12
13
  <link rel="stylesheet" href="styles.css">
@@ -18,9 +19,9 @@
18
19
  <span class="brand-name">DSH Remote</span>
19
20
  </div>
20
21
  <div class="topbar-right">
21
- <button id="btn-admin" class="icon-btn" title="主机管理" aria-label="主机管理">🖥</button>
22
- <button id="btn-refresh" class="icon-btn" title="刷新" aria-label="刷新">⟳</button>
23
- <span id="conn-badge" class="conn-badge off">未连接</span>
22
+ <button id="btn-admin" class="icon-btn" data-i18n-title="a11y.hostAdmin" data-i18n-aria="a11y.hostAdmin">🖥</button>
23
+ <button id="btn-refresh" class="icon-btn" data-i18n-title="a11y.refresh" data-i18n-aria="a11y.refresh">⟳</button>
24
+ <span id="conn-badge" class="conn-badge off" data-i18n="conn.off">未连接</span>
24
25
  </div>
25
26
  </header>
26
27
 
@@ -29,30 +30,30 @@
29
30
  <section id="view-home" class="view">
30
31
  <div class="stat-strip" id="stat-strip"></div>
31
32
  <div class="section-head">
32
- <span>会话</span>
33
- <button id="btn-new-session" class="mini-btn">+ 新会话</button>
33
+ <span data-i18n="nav.sessions">会话</span>
34
+ <button id="btn-new-session" class="mini-btn" data-i18n="home.newSession">+ 新会话</button>
34
35
  </div>
35
36
  <div id="session-list" class="session-list"></div>
36
- <div id="home-empty" class="empty hidden">暂无会话</div>
37
+ <div id="home-empty" class="empty hidden" data-i18n="home.empty">暂无会话</div>
37
38
  </section>
38
39
 
39
40
  <!-- 文件传输 -->
40
41
  <section id="view-files" class="view hidden">
41
42
  <div class="fs-head">
42
- <button id="fs-up" class="icon-btn" title="返回上级" aria-label="返回上级">‹</button>
43
- <div id="fs-path" class="fs-path">加载中…</div>
44
- <button id="fs-upload-btn" class="mini-btn">上传</button>
45
- <button id="fs-refresh" class="icon-btn" title="刷新" aria-label="刷新">⟳</button>
43
+ <button id="fs-up" class="icon-btn" data-i18n-title="fs.up" data-i18n-aria="fs.up">‹</button>
44
+ <div id="fs-path" class="fs-path" data-i18n="fs.loading">加载中…</div>
45
+ <button id="fs-upload-btn" class="mini-btn" data-i18n="fs.upload">上传</button>
46
+ <button id="fs-refresh" class="icon-btn" data-i18n-title="a11y.refresh" data-i18n-aria="a11y.refresh">⟳</button>
46
47
  </div>
47
48
  <div id="fs-progress" class="fs-progress hidden">
48
49
  <div class="fs-progress-track"><div id="fs-progress-bar" class="fs-progress-bar"></div></div>
49
50
  <div id="fs-progress-text" class="fs-progress-text muted">0%</div>
50
51
  <div class="fs-progress-actions">
51
- <button id="fs-pause-btn" class="mini-btn hidden">暂停</button>
52
- <button id="fs-cancel-btn" class="mini-btn hidden">取消</button>
52
+ <button id="fs-pause-btn" class="mini-btn hidden" data-i18n="fs.pause">暂停</button>
53
+ <button id="fs-cancel-btn" class="mini-btn hidden" data-i18n="fs.cancel">取消</button>
53
54
  </div>
54
55
  </div>
55
- <div id="fs-pull" class="fs-pull">松开刷新</div>
56
+ <div id="fs-pull" class="fs-pull" data-i18n="fs.pullRelease">松开刷新</div>
56
57
  <div id="fs-list" class="fs-list"></div>
57
58
  <input type="file" id="fs-file-input" class="hidden">
58
59
  </section>
@@ -62,22 +63,22 @@
62
63
  <div id="session-head" class="session-head">
63
64
  <button id="btn-back" class="icon-btn">‹</button>
64
65
  <div class="session-head-meta">
65
- <div id="session-title" class="session-title">加载中…</div>
66
+ <div id="session-title" class="session-title" data-i18n="session.loading">加载中…</div>
66
67
  <div id="session-sub" class="session-sub"></div>
67
68
  </div>
68
- <button id="btn-stats" class="icon-btn" title="本轮统计">📊</button>
69
- <button id="btn-cancel" class="danger-btn hidden">停止</button>
69
+ <button id="btn-stats" class="icon-btn" data-i18n-title="stats.title">📊</button>
70
+ <button id="btn-cancel" class="danger-btn hidden" data-i18n="session.stop">停止</button>
70
71
  </div>
71
72
 
72
73
  <div id="session-cards" class="cards"></div>
73
74
 
74
75
  <div class="section-head small">
75
- <span>对话</span>
76
+ <span data-i18n="session.history">对话</span>
76
77
  <span id="history-hint" class="muted"></span>
77
78
  </div>
78
79
  <div id="history-wrap" class="history-wrap">
79
80
  <div id="history" class="history"></div>
80
- <div id="history-rail" class="history-rail">
81
+ <div class="history-rail">
81
82
  <div class="rail-track"></div>
82
83
  <div id="rail-thumb" class="rail-thumb"></div>
83
84
  <div id="rail-nodes" class="rail-nodes"></div>
@@ -87,43 +88,43 @@
87
88
  <div class="composer-wrap">
88
89
  <div id="composer-menu" class="composer-menu hidden">
89
90
  <div class="menu-group">
90
- <div class="menu-title">输入指令</div>
91
+ <div class="menu-title" data-i18n="menu.commandsTitle">输入指令</div>
91
92
  <div class="menu-chips">
92
- <button class="menu-chip" data-cmd="/compact">/compact 压缩上下文</button>
93
- <button class="menu-chip" data-cmd="/goal">/goal 查看目标</button>
94
- <button class="menu-chip" data-cmd="/goal pause">/goal 暂停</button>
95
- <button class="menu-chip" data-cmd="/goal resume">/goal 继续</button>
93
+ <button class="menu-chip" data-cmd="/compact" data-i18n="menu.compact">/compact 压缩上下文</button>
94
+ <button class="menu-chip" data-cmd="/goal" data-i18n="menu.goalView">/goal 查看目标</button>
95
+ <button class="menu-chip" data-cmd="/goal pause" data-i18n="menu.goalPause">/goal 暂停</button>
96
+ <button class="menu-chip" data-cmd="/goal resume" data-i18n="menu.goalResume">/goal 继续</button>
96
97
  </div>
97
98
  </div>
98
99
  <div class="menu-group">
99
- <div class="menu-title">权限控制</div>
100
+ <div class="menu-title" data-i18n="menu.permissionTitle">权限控制</div>
100
101
  <div class="menu-chips">
101
- <button class="menu-chip" data-cmd="/permission workspace-write">工作区写(免审批)</button>
102
- <button class="menu-chip danger" data-cmd="/permission danger-full-access">完全访问(全部免审批)</button>
102
+ <button class="menu-chip" data-cmd="/permission workspace-write" data-i18n="menu.permWorkspace">工作区写(免审批)</button>
103
+ <button class="menu-chip danger" data-cmd="/permission danger-full-access" data-i18n="menu.permFull">完全访问(全部免审批)</button>
103
104
  </div>
104
105
  </div>
105
106
  <div class="menu-group">
106
- <div class="menu-title">模型切换 <button id="btn-model-refresh" class="mini-btn" style="padding:1px 8px">刷新</button></div>
107
- <div id="menu-models" class="menu-models muted">点开时自动加载</div>
107
+ <div class="menu-title"><span data-i18n="menu.modelTitle">模型切换</span> <button id="btn-model-refresh" class="mini-btn" style="padding:1px 8px" data-i18n="menu.refresh">刷新</button></div>
108
+ <div id="menu-models" class="menu-models muted" data-i18n="menu.modelAuto">点开时自动加载</div>
108
109
  </div>
109
110
  <div class="menu-group hidden" id="menu-effort-group">
110
- <div class="menu-title">思考深度</div>
111
+ <div class="menu-title" data-i18n="menu.effortTitle">思考深度</div>
111
112
  <div id="menu-efforts" class="menu-chips"></div>
112
113
  </div>
113
114
  </div>
114
115
  <div class="composer">
115
- <button id="btn-plus" class="plus-btn" aria-label="更多操作" title="指令/权限/模型">+</button>
116
- <textarea id="composer-input" rows="1" placeholder="给 DSH 发消息…"></textarea>
117
- <button id="btn-send" class="send-btn">发送</button>
116
+ <button id="btn-plus" class="plus-btn" data-i18n-aria="a11y.more" data-i18n-title="a11y.moreTitle">+</button>
117
+ <textarea id="composer-input" rows="1" data-i18n-placeholder="composer.placeholder" placeholder="给 DSH 发消息…"></textarea>
118
+ <button id="btn-send" class="send-btn" data-i18n="composer.send">发送</button>
118
119
  </div>
119
120
  </div>
120
121
  </section>
121
122
 
122
123
  <!-- 待办 -->
123
124
  <section id="view-activity" class="view hidden">
124
- <div class="section-head"><span>待处理</span><span id="pending-count" class="muted"></span></div>
125
+ <div class="section-head"><span data-i18n="pending.title">待处理</span><span id="pending-count" class="muted"></span></div>
125
126
  <div id="pending-list" class="pending-list"></div>
126
- <div class="section-head"><span>后台任务</span></div>
127
+ <div class="section-head"><span data-i18n="jobs.title">后台任务</span></div>
127
128
  <div id="jobs-list" class="jobs-list"></div>
128
129
  </section>
129
130
 
@@ -131,65 +132,74 @@
131
132
  <section id="view-settings" class="view hidden">
132
133
  <div class="settings-group">
133
134
  <div class="setting-row">
134
- <div><div class="setting-name">服务器地址(可多个)</div><div class="setting-desc" id="server-desc">默认 = 当前页面地址</div></div>
135
- <button id="btn-server-speed" class="mini-btn">测速</button>
135
+ <div><div class="setting-name" data-i18n="settings.servers">服务器地址(可多个)</div><div class="setting-desc" id="server-desc" data-i18n="servers.defaultDesc">默认 = 当前页面地址</div></div>
136
+ <button id="btn-server-speed" class="mini-btn" data-i18n="settings.speedTest">测速</button>
136
137
  </div>
137
138
  <div id="server-list" class="server-list"></div>
138
139
  <div class="server-add">
139
140
  <input id="server-input" type="url" placeholder="http://IP:8787">
140
- <button id="btn-server-add" class="mini-btn">添加</button>
141
+ <button id="btn-server-add" class="mini-btn" data-i18n="settings.add">添加</button>
141
142
  </div>
142
143
  <div class="setting-row">
143
- <div><div class="setting-name">扫码连接</div><div class="setting-desc">拍照/相册本地解码(无谷歌服务依赖);也可系统相机扫码或手动粘贴</div></div>
144
- <button id="btn-scan-pair" class="mini-btn">扫码</button>
144
+ <div><div class="setting-name" data-i18n="settings.scanTitle">扫码连接</div><div class="setting-desc" data-i18n="settings.scanDesc">拍照/相册本地解码(无谷歌服务依赖);也可系统相机扫码或手动粘贴</div></div>
145
+ <button id="btn-scan-pair" class="mini-btn" data-i18n="settings.scan">扫码</button>
145
146
  </div>
146
147
  <div class="setting-row">
147
- <div><div class="setting-name">通知</div><div class="setting-desc">收到审批/提问时推送</div></div>
148
+ <div><div class="setting-name" data-i18n="settings.notifyTitle">通知</div><div class="setting-desc" data-i18n="settings.notifyDesc">收到审批/提问时推送</div></div>
148
149
  <label class="switch"><input type="checkbox" id="opt-notify"><span class="slider"></span></label>
149
150
  </div>
150
151
  <div class="setting-row">
151
- <div><div class="setting-name">显示工具调用</div><div class="setting-desc">关闭后隐藏 🔧/📦 工具调用与结果</div></div>
152
+ <div><div class="setting-name" data-i18n="settings.toolsTitle">显示工具调用</div><div class="setting-desc" data-i18n="settings.toolsDesc">关闭后隐藏 🔧/📦 工具调用与结果</div></div>
152
153
  <label class="switch"><input type="checkbox" id="opt-tools"><span class="slider"></span></label>
153
154
  </div>
154
155
  <div class="setting-row">
155
- <div><div class="setting-name">访问令牌</div><div class="setting-desc" id="token-desc">已保存</div></div>
156
- <button id="btn-change-token" class="mini-btn">更换</button>
156
+ <div><div class="setting-name" data-i18n="settings.langTitle">语言 / Language</div><div class="setting-desc" data-i18n="settings.langDesc">界面语言,跟随系统或手动切换</div></div>
157
+ <button id="btn-lang" class="mini-btn">EN</button>
157
158
  </div>
158
159
  <div class="setting-row">
159
- <div><div class="setting-name">DSH 状态</div><div class="setting-desc" id="host-desc">探测中…</div></div>
160
- <button id="btn-host-describe" class="mini-btn">探测</button>
160
+ <div><div class="setting-name" data-i18n="settings.themeTitle">皮肤</div><div class="setting-desc" data-i18n="settings.themeDesc">默认深空 / 落日 / 易北爱乐厅 / 草原孤塔</div></div>
161
+ <button id="btn-theme" class="mini-btn" style="min-width:104px">默认深空</button>
161
162
  </div>
162
163
  <div class="setting-row">
163
- <div><div class="setting-name">检查更新</div><div class="setting-desc" id="update-desc">加载中…</div>
164
- <button id="btn-download-update" class="mini-btn hidden" style="margin-top:6px">下载并安装更新</button></div>
165
- <button id="btn-check-update" class="mini-btn">检查</button>
164
+ <div><div class="setting-name" data-i18n="settings.tokenTitle">访问令牌</div><div class="setting-desc" id="token-desc" data-i18n="token.notSet">未设置</div></div>
165
+ <button id="btn-change-token" class="mini-btn" data-i18n="settings.change">更换</button>
166
+ </div>
167
+ <div class="setting-row">
168
+ <div><div class="setting-name" data-i18n="settings.hostTitle">DSH 状态</div><div class="setting-desc" id="host-desc" data-i18n="settings.hostProbing">探测中…</div></div>
169
+ <button id="btn-host-describe" class="mini-btn" data-i18n="settings.probe">探测</button>
170
+ </div>
171
+ <div class="setting-row">
172
+ <div><div class="setting-name" data-i18n="settings.updateTitle">检查更新</div><div class="setting-desc" id="update-desc" data-i18n="settings.updateLoading">加载中…</div>
173
+ <button id="btn-download-update" class="mini-btn hidden" style="margin-top:6px" data-i18n="settings.downloadUpdate">下载并安装更新</button>
174
+ <button id="btn-update-expand" class="mini-btn hidden" style="margin-top:6px" data-i18n="update.expand">展开</button></div>
175
+ <button id="btn-check-update" class="mini-btn" data-i18n="settings.check">检查</button>
166
176
  </div>
167
177
  </div>
168
178
  <div class="settings-group">
169
179
  <div class="setting-row">
170
- <div><div class="setting-name">清空本地数据</div><div class="setting-desc">令牌与缓存</div></div>
171
- <button id="btn-reset" class="mini-btn">重置</button>
180
+ <div><div class="setting-name" data-i18n="settings.resetTitle">清空本地数据</div><div class="setting-desc" data-i18n="settings.resetDesc">令牌与缓存</div></div>
181
+ <button id="btn-reset" class="mini-btn" data-i18n="settings.reset">重置</button>
172
182
  </div>
173
183
  </div>
174
- <p class="about">DSH Remote · 局域网 / Tailscale 均可访问<br>仅作为 DSH 本机 3080 端口的带认证代理</p>
184
+ <p class="about" data-i18n="settings.about">DSH Remote · 局域网 / Tailscale 均可访问<br>仅作为 DSH 本机 3080 端口的带认证代理</p>
175
185
  </section>
176
186
  </main>
177
187
 
178
188
  <nav class="bottom-nav">
179
- <button data-view="view-home" class="nav-btn active"><span class="nav-ico">▤</span><span>会话</span></button>
180
- <button data-view="view-files" class="nav-btn"><span class="nav-ico">⇅</span><span>文件</span></button>
181
- <button data-view="view-activity" class="nav-btn"><span class="nav-ico">◷</span><span>待办<b id="nav-pending" class="nav-badge hidden"></b></span></button>
182
- <button data-view="view-settings" class="nav-btn"><span class="nav-ico">⚙</span><span>设置</span></button>
189
+ <button data-view="view-home" class="nav-btn active"><span class="nav-ico">▤</span><span data-i18n="nav.sessions">会话</span></button>
190
+ <button data-view="view-files" class="nav-btn"><span class="nav-ico">⇅</span><span data-i18n="nav.files">文件</span></button>
191
+ <button data-view="view-activity" class="nav-btn"><span class="nav-ico">◷</span><span data-i18n="nav.pending">待办</span><b id="nav-pending" class="nav-badge hidden"></b></button>
192
+ <button data-view="view-settings" class="nav-btn"><span class="nav-ico">⚙</span><span data-i18n="nav.settings">设置</span></button>
183
193
  </nav>
184
194
 
185
195
  <!-- 审批模态 -->
186
196
  <div id="modal-approval" class="modal hidden">
187
197
  <div class="modal-card">
188
- <div class="modal-title">工具审批</div>
198
+ <div class="modal-title" data-i18n="modal.approvalTitle">工具审批</div>
189
199
  <div id="approval-body" class="modal-body"></div>
190
200
  <div class="modal-actions">
191
- <button id="approval-reject" class="btn subtle">拒绝</button>
192
- <button id="approval-allow" class="btn primary">允许一次</button>
201
+ <button id="approval-reject" class="btn subtle" data-i18n="modal.reject">拒绝</button>
202
+ <button id="approval-allow" class="btn primary" data-i18n="modal.allowOnce">允许一次</button>
193
203
  </div>
194
204
  </div>
195
205
  </div>
@@ -197,11 +207,11 @@
197
207
  <!-- 提问模态 -->
198
208
  <div id="modal-question" class="modal hidden">
199
209
  <div class="modal-card">
200
- <div class="modal-title">DSH 需要你回答</div>
210
+ <div class="modal-title" data-i18n="modal.questionTitle">DSH 需要你回答</div>
201
211
  <div id="question-body" class="modal-body"></div>
202
212
  <div class="modal-actions">
203
- <button id="question-later" class="btn subtle">稍后</button>
204
- <button id="question-submit" class="btn primary">提交</button>
213
+ <button id="question-later" class="btn subtle" data-i18n="modal.later">稍后</button>
214
+ <button id="question-submit" class="btn primary" data-i18n="modal.submit">提交</button>
205
215
  </div>
206
216
  </div>
207
217
  </div>
@@ -209,11 +219,11 @@
209
219
  <!-- goal 模态 -->
210
220
  <div id="modal-goal" class="modal hidden">
211
221
  <div class="modal-card">
212
- <div class="modal-title">目标控制</div>
222
+ <div class="modal-title" data-i18n="modal.goalTitle">目标控制</div>
213
223
  <div id="goal-body" class="modal-body"></div>
214
224
  <div class="modal-actions">
215
- <button id="goal-close" class="btn subtle">关闭</button>
216
- <button id="goal-edit" class="btn primary hidden">更新目标</button>
225
+ <button id="goal-close" class="btn subtle" data-i18n="modal.close">关闭</button>
226
+ <button id="goal-edit" class="btn primary hidden" data-i18n="modal.updateGoal">更新目标</button>
217
227
  </div>
218
228
  </div>
219
229
  </div>
@@ -221,18 +231,269 @@
221
231
  <!-- 统计模态 -->
222
232
  <div id="modal-stats" class="modal hidden">
223
233
  <div class="modal-card">
224
- <div class="modal-title">本轮统计</div>
234
+ <div class="modal-title" data-i18n="modal.statsTitle">本轮统计</div>
225
235
  <div id="stats-body" class="modal-body"></div>
226
236
  <div class="modal-actions">
227
- <button id="stats-close" class="btn subtle">关闭</button>
237
+ <button id="stats-close" class="btn subtle" data-i18n="modal.close">关闭</button>
238
+ </div>
239
+ </div>
240
+ </div>
241
+
242
+ <!-- 皮肤选择模态 -->
243
+ <div id="modal-theme" class="modal hidden">
244
+ <div class="modal-card">
245
+ <div class="modal-title" data-i18n="theme.panelTitle">选择配色</div>
246
+ <div id="theme-options" class="theme-panel-list"></div>
247
+ <div class="modal-actions">
248
+ <button id="theme-close" class="btn subtle" data-i18n="theme.close">关闭</button>
228
249
  </div>
229
250
  </div>
230
251
  </div>
231
252
 
232
253
  <div id="toast" class="toast hidden"></div>
233
254
 
255
+ <script>
256
+ window.APP_STR = {
257
+ zh: {
258
+ 'a11y.hostAdmin': '主机管理', 'a11y.refresh': '刷新', 'a11y.more': '更多操作', 'a11y.moreTitle': '指令/权限/模型',
259
+ 'conn.on': '已连接', 'conn.off': '未连接', 'conn.reconnecting': '连接中断,正在重连…',
260
+ 'common.refreshing': '刷新中…',
261
+ 'nav.sessions': '会话', 'nav.files': '文件', 'nav.pending': '待办', 'nav.settings': '设置',
262
+ 'home.newSession': '+ 新会话', 'home.empty': '暂无会话', 'home.createFailed': '新建会话失败', 'home.created': '会话已创建',
263
+ 'sessions.running': '运行中', 'sessions.statRunning': '运行中', 'sessions.statPending': '待处理', 'sessions.statTotal': '会话总数',
264
+ 'sessions.queueBadge': '队列 {n}', 'sessions.goalBadge': '目标·{phase}',
265
+ 'sessions.cacheFallback': '网络不可用:显示本地缓存的会话列表',
266
+ 'err.badResponse': '坏响应', 'err.dshError': 'DSH 返回错误', 'err.accessDenied': '访问被拒绝:请检查令牌',
267
+ 'err.sessionList': '拉取会话列表失败',
268
+ 'token.notSet': '未设置', 'token.notSetHint': '未设置——点「更换」粘贴网关启动时打印的 token',
269
+ 'token.saved': '已保存', 'token.savedLocal': '已保存(本机)', 'token.savedScan': '已保存(扫码)',
270
+ 'token.savedReconnect': '已保存,正在重连', 'token.invalid': '令牌无效,点「更换」重新设置',
271
+ 'token.prompt': '输入访问令牌(网关启动时打印的 token):',
272
+ 'speed.testing': '正在测速各服务器…', 'speed.origin': '当前页面',
273
+ 'speed.switched': '已切换到最快服务器:{url}({ms}ms)', 'speed.alreadyBest': '当前已是最快:{url}({ms}ms)',
274
+ 'speed.allDown': '全部服务器不可达',
275
+ 'servers.empty': '未添加备用服务器 · 默认使用当前页面地址', 'servers.untested': '未测速',
276
+ 'servers.current': ' · 当前', 'servers.unreachable': '不可达', 'servers.delete': '删除服务器',
277
+ 'servers.currentDesc': '当前: {url}', 'servers.notSet': '未设置服务器', 'servers.defaultDesc': '默认 = 当前页面地址',
278
+ 'servers.needAddress': '请输入服务器地址', 'servers.badProtocol': '地址需以 http:// 或 https:// 开头',
279
+ 'servers.duplicate': '该地址已在列表中', 'servers.added': '已添加服务器', 'servers.removedActive': '已删除当前服务器,重新测速…',
280
+ 'stream.error': '事件流错误:{msg}',
281
+ 'session.loading': '加载中…', 'session.unknown': '未知会话', 'session.running': '运行中', 'session.interrupted': '已中断',
282
+ 'session.stop': '停止', 'session.history': '对话', 'session.errorMsg': '会话出错:{msg}',
283
+ 'session.confirmStop': '停止当前会话正在运行的任务?', 'session.stopFailed': '停止失败', 'session.stopRequested': '已请求停止',
284
+ 'notify.approvalTitle': '工具审批', 'notify.approvalBody': '{tool} 需要批准', 'notify.questionTitle': 'DSH 提问', 'notify.questionBody': '需要你回答',
285
+ 'notify.permissionFailed': '通知权限申请失败:{msg}',
286
+ 'history.loading': '加载历史…', 'history.cacheFallback': '网络不可用:显示本地缓存的历史', 'history.loadFailed': '加载历史失败:{msg}',
287
+ 'history.offlineCache': '离线缓存 {n} 条', 'history.count': '{n} 条', 'history.empty': '还没有消息',
288
+ 'history.queueAndCount': '队列 {q} · 历史 {n}', 'history.countOnly': '历史 {n}',
289
+ 'role.me': '我', 'role.dsh': 'DSH',
290
+ 'tool.default': '工具', 'tool.unknown': '未知工具', 'tool.result': '结果',
291
+ 'event.taskStart': '▶ 任务开始', 'event.taskEnd': '■ 任务结束',
292
+ 'block.image': '图片', 'block.thinking': '🧠 思考过程', 'block.toolCall': '工具调用', 'block.toolResult': '工具结果', 'block.unknown': '块 · {type}',
293
+ 'truncated': '…(截断)',
294
+ 'stats.title': '本轮统计', 'stats.roundTitle': '本轮统计', 'stats.turnsSteps': '轮次 / 步骤',
295
+ 'stats.llmTime': '模型耗时', 'stats.minutes': ' 分钟', 'stats.outputCache': '输出 / 缓存读',
296
+ 'stats.ctxPressure': '上下文压力', 'stats.permission': '权限', 'stats.empty': '暂无统计数据',
297
+ 'goal.title': '目标', 'goal.pause': '暂停', 'goal.resume': '继续', 'goal.complete': '完成', 'goal.edit': '改目标', 'goal.clear': '清除',
298
+ 'goal.none': '当前会话没有目标', 'goal.confirmClear': '清除当前目标?(不会删除会话)', 'goal.confirmComplete': '将目标标记为完成?',
299
+ 'goal.actionFailed': '目标操作失败', 'goal.actionSubmitted': '目标操作已提交',
300
+ 'goal.cannotEmpty': '目标不能为空', 'goal.updateFailed': '更新失败', 'goal.updated': '目标已更新',
301
+ 'todos.title': '任务清单',
302
+ 'subagent.title': '子代理', 'subagent.diagnostic': '诊断项', 'subagent.running': '· 运行中', 'subagent.interrupt': '中断',
303
+ 'subagent.confirmInterrupt': '中断这个子代理当前回合?', 'subagent.interruptFailed': '中断失败', 'subagent.interruptSubmitted': '中断请求已提交',
304
+ 'send.failed': '发送失败', 'send.commandSent': '指令已发送', 'send.sent': '已发送', 'send.commandExecuted': '命令已执行',
305
+ 'models.loading': '模型加载中…', 'models.loadFailed': '模型列表加载失败:{msg}', 'models.unavailable': '不可用', 'models.none': '没有可用模型',
306
+ 'models.switchFailed': '切换模型失败', 'models.switched': '已切换模型:{model}',
307
+ 'models.effortFailed': '切换思考深度失败', 'models.effortSwitched': '思考深度:{effort}',
308
+ 'menu.commandsTitle': '输入指令', 'menu.compact': '/compact 压缩上下文', 'menu.goalView': '/goal 查看目标',
309
+ 'menu.goalPause': '/goal 暂停', 'menu.goalResume': '/goal 继续',
310
+ 'menu.permissionTitle': '权限控制', 'menu.permWorkspace': '工作区写(免审批)', 'menu.permFull': '完全访问(全部免审批)',
311
+ 'menu.modelTitle': '模型切换', 'menu.refresh': '刷新', 'menu.modelAuto': '点开时自动加载', 'menu.effortTitle': '思考深度',
312
+ 'composer.placeholder': '给 DSH 发消息…', 'composer.send': '发送',
313
+ 'pending.title': '待处理', 'pending.count': '{n} 项', 'pending.approvalTitle': '🔧 {tool} 请求批准', 'pending.noReason': '无说明',
314
+ 'pending.allow': '允许', 'pending.reject': '拒绝', 'pending.question': '❓ {question}',
315
+ 'pending.questionCount': '共 {n} 个问题', 'pending.answer': '去回答', 'pending.empty': '暂无待处理事项',
316
+ 'pending.allowed': '已允许', 'pending.rejected': '已拒绝', 'pending.stale': '审批已不在待处理状态',
317
+ 'question.customPlaceholder': '其他 / 自定义回答(可选)', 'question.needAnswer': '请先选择或填写回答',
318
+ 'question.submitted': '已提交回答', 'question.stale': '提问已不在待处理状态',
319
+ 'jobs.title': '后台任务', 'jobs.empty': '暂无后台任务',
320
+ 'fs.loading': '加载中…', 'fs.up': '返回上级', 'fs.upload': '上传', 'fs.pause': '暂停', 'fs.resume': '继续', 'fs.cancel': '取消',
321
+ 'fs.pullDown': '下拉刷新', 'fs.pullRelease': '松开刷新', 'fs.refreshing': '刷新中…',
322
+ 'fs.noToken': '未设置令牌', 'fs.goSettings': '请先到「设置」页粘贴网关令牌',
323
+ 'fs.notFound': '目录不存在', 'fs.forbidden': '路径不在允许范围内',
324
+ 'fs.loadFailed': '加载失败:{msg}', 'fs.loadFailedToast': '文件列表加载失败:{msg}',
325
+ 'fs.emptyDir': '空目录', 'fs.dir': '目录',
326
+ 'fs.downloadStarted': '开始下载到「下载/dsh-remote」目录', 'fs.downloadFailed': '无法启动下载:{msg}',
327
+ 'fs.downloadUnsupported': '当前 App 版本不支持系统下载,请先更新 App',
328
+ 'fs.uploadCancelled': '已取消上传', 'fs.pausedErr': '已暂停', 'fs.cancelledErr': '已取消',
329
+ 'fs.noTokenToast': '请先到「设置」页设置令牌', 'fs.tooLarge': '单文件超过 2GB 上限', 'fs.uploadBusy': '已有上传任务,先暂停或取消',
330
+ 'fs.networkError': '网络错误', 'fs.networkInterrupt': '网络中断',
331
+ 'fs.confirmOverwrite': '文件已存在(大小相同),覆盖它?', 'fs.confirmOverwrite2': '文件已存在,覆盖它?',
332
+ 'fs.uploadDone': '已上传 {name}(SHA-256 已校验{resumed})', 'fs.resumedSuffix': ' · 断点续传',
333
+ 'fs.checksumMismatch': '文件校验不一致(SHA-256)', 'fs.checksumCleared': '文件校验不一致(SHA-256),已清除坏分片',
334
+ 'fs.pausedPct': '已暂停 · {pct}%', 'fs.pausedToast': '已暂停,点「继续」接着传',
335
+ 'fs.checksumFailed': '校验失败 · 点「继续」重新上传', 'fs.uploadInterrupted': '上传中断:{msg}(重选同一文件可断点续传)',
336
+ 'fs.alreadyRoot': '已在允许的根目录',
337
+ 'update.currentV': '当前版本 v{version}', 'update.noVersion': '未获取到版本', 'update.needServer': '请先设置服务器地址',
338
+ 'update.noServerDesc': '请先设置服务器地址', 'update.checking': '正在检查更新…',
339
+ 'update.found': '发现新版本 v{version}{notes}', 'update.foundTitle': '发现新版本', 'update.foundBody': 'v{version} 可更新',
340
+ 'update.latestV': '已是最新 v{version}', 'update.latestRemote': '最新版本 v{version}', 'update.latestToast': '已是最新版本',
341
+ 'update.checkFailedDesc': '检查失败:{msg}', 'update.checkFailed': '检查更新失败:{msg}',
342
+ 'update.downloadStarted': '开始下载,完成后会弹出安装页', 'update.downloadFailed': '无法启动下载:{msg}',
343
+ 'update.installUnsupported': '当前版本不支持 App 内安装,已转浏览器下载',
344
+ 'update.expand': '展开', 'update.collapse': '收起',
345
+ 'scan.imageLoadFailed': '图片加载失败', 'scan.decodeUnsupported': '当前设备不支持图片解码',
346
+ 'scan.browserHint': '浏览器请打开主机管理页,用手机相机扫码', 'scan.unsupported': '当前 App 版本不支持拍照扫码,请先更新 App',
347
+ 'scan.permissionDenied': '未授予相机权限',
348
+ 'scan.promptHeader': '扫描 DSH Remote 配对二维码', 'scan.promptPhoto': '拍照扫描', 'scan.promptGallery': '从相册选择',
349
+ 'scan.noPhoto': '没有拿到图片,请重试', 'scan.noQr': '未识别到二维码,请对准后重试',
350
+ 'scan.paired': '配对成功,正在连接', 'scan.pairedLink': '已通过二维码配对', 'scan.notPair': '这不是 DSH Remote 的配对二维码',
351
+ 'scan.cancelled': '已取消扫码', 'scan.failed': '扫码失败:{msg}',
352
+ 'settings.servers': '服务器地址(可多个)', 'settings.speedTest': '测速', 'settings.add': '添加',
353
+ 'settings.scanTitle': '扫码连接', 'settings.scanDesc': '拍照/相册本地解码(无谷歌服务依赖);也可系统相机扫码或手动粘贴', 'settings.scan': '扫码',
354
+ 'settings.notifyTitle': '通知', 'settings.notifyDesc': '收到审批/提问时推送',
355
+ 'settings.toolsTitle': '显示工具调用', 'settings.toolsDesc': '关闭后隐藏 🔧/📦 工具调用与结果',
356
+ 'settings.langTitle': '语言 / Language', 'settings.langDesc': '界面语言,跟随系统或手动切换',
357
+ 'settings.themeTitle': '皮肤', 'settings.themeDesc': '默认深空 / 落日 / 易北爱乐厅 / 草原孤塔',
358
+ 'theme.default': '默认深空', 'theme.dark': '落日', 'theme.light': '易北爱乐厅', 'theme.neutral': '草原孤塔',
359
+ 'theme.panelTitle': '选择配色', 'theme.close': '关闭',
360
+ 'settings.tokenTitle': '访问令牌', 'settings.change': '更换',
361
+ 'settings.hostTitle': 'DSH 状态', 'settings.hostProbing': '探测中…', 'settings.probe': '探测', 'settings.probeFailed': '探测失败',
362
+ 'settings.hostDesc': 'DSH {version} · {cwd} · 附加会话 {n}',
363
+ 'settings.updateTitle': '检查更新', 'settings.updateLoading': '加载中…', 'settings.downloadUpdate': '下载并安装更新', 'settings.check': '检查',
364
+ 'settings.resetTitle': '清空本地数据', 'settings.resetDesc': '令牌与缓存', 'settings.reset': '重置',
365
+ 'settings.confirmReset': '清除本地令牌、服务器与缓存?', 'settings.notifyDenied': '通知权限未开启',
366
+ 'settings.toolsShown': '已显示工具调用', 'settings.toolsHidden': '已隐藏工具调用',
367
+ 'settings.about': 'DSH Remote · 局域网 / Tailscale 均可访问<br>仅作为 DSH 本机 3080 端口的带认证代理',
368
+ 'modal.approvalTitle': '工具审批', 'modal.reject': '拒绝', 'modal.allowOnce': '允许一次',
369
+ 'modal.questionTitle': 'DSH 需要你回答', 'modal.later': '稍后', 'modal.submit': '提交',
370
+ 'modal.goalTitle': '目标控制', 'modal.close': '关闭', 'modal.updateGoal': '更新目标',
371
+ 'modal.statsTitle': '本轮统计',
372
+ 'time.justNow': '刚刚', 'time.minAgo': ' 分钟前', 'time.hourAgo': ' 小时前'
373
+ },
374
+ en: {
375
+ 'a11y.hostAdmin': 'Host admin', 'a11y.refresh': 'Refresh', 'a11y.more': 'More actions', 'a11y.moreTitle': 'Commands / Permissions / Models',
376
+ 'conn.on': 'Connected', 'conn.off': 'Offline', 'conn.reconnecting': 'Connection lost, reconnecting…',
377
+ 'common.refreshing': 'Refreshing…',
378
+ 'nav.sessions': 'Sessions', 'nav.files': 'Files', 'nav.pending': 'Inbox', 'nav.settings': 'Settings',
379
+ 'home.newSession': '+ New session', 'home.empty': 'No sessions yet', 'home.createFailed': 'Failed to create session', 'home.created': 'Session created',
380
+ 'sessions.running': 'Running', 'sessions.statRunning': 'Running', 'sessions.statPending': 'Pending', 'sessions.statTotal': 'Sessions',
381
+ 'sessions.queueBadge': 'Queue {n}', 'sessions.goalBadge': 'Goal·{phase}',
382
+ 'sessions.cacheFallback': 'Network unavailable: showing cached session list',
383
+ 'err.badResponse': 'Bad response', 'err.dshError': 'DSH returned an error', 'err.accessDenied': 'Access denied: check your token',
384
+ 'err.sessionList': 'Failed to load session list',
385
+ 'token.notSet': 'Not set', 'token.notSetHint': 'Not set — tap "Change" and paste the token printed when the gateway starts',
386
+ 'token.saved': 'Saved', 'token.savedLocal': 'Saved (this device)', 'token.savedScan': 'Saved (QR scan)',
387
+ 'token.savedReconnect': 'Saved, reconnecting', 'token.invalid': 'Token invalid, tap "Change" to set it again',
388
+ 'token.prompt': 'Enter the access token (printed when the gateway starts):',
389
+ 'speed.testing': 'Testing servers…', 'speed.origin': 'this page',
390
+ 'speed.switched': 'Switched to fastest server: {url} ({ms}ms)', 'speed.alreadyBest': 'Already on fastest: {url} ({ms}ms)',
391
+ 'speed.allDown': 'All servers unreachable',
392
+ 'servers.empty': 'No backup servers · using the current page URL', 'servers.untested': 'Untested',
393
+ 'servers.current': ' · current', 'servers.unreachable': 'Unreachable', 'servers.delete': 'Remove server',
394
+ 'servers.currentDesc': 'Current: {url}', 'servers.notSet': 'No server set', 'servers.defaultDesc': 'Default = current page URL',
395
+ 'servers.needAddress': 'Enter a server address', 'servers.badProtocol': 'Address must start with http:// or https://',
396
+ 'servers.duplicate': 'This address is already in the list', 'servers.added': 'Server added', 'servers.removedActive': 'Removed active server, re-testing…',
397
+ 'stream.error': 'Event stream error: {msg}',
398
+ 'session.loading': 'Loading…', 'session.unknown': 'Unknown session', 'session.running': 'Running', 'session.interrupted': 'Interrupted',
399
+ 'session.stop': 'Stop', 'session.history': 'Conversation', 'session.errorMsg': 'Session error: {msg}',
400
+ 'session.confirmStop': 'Stop the currently running task in this session?', 'session.stopFailed': 'Stop failed', 'session.stopRequested': 'Stop requested',
401
+ 'notify.approvalTitle': 'Tool approval', 'notify.approvalBody': '{tool} needs approval', 'notify.questionTitle': 'DSH question', 'notify.questionBody': 'Needs your answer',
402
+ 'notify.permissionFailed': 'Notification permission failed: {msg}',
403
+ 'history.loading': 'Loading history…', 'history.cacheFallback': 'Network unavailable: showing cached history', 'history.loadFailed': 'Failed to load history: {msg}',
404
+ 'history.offlineCache': 'Offline cache {n} items', 'history.count': '{n} items', 'history.empty': 'No messages yet',
405
+ 'history.queueAndCount': 'Queue {q} · History {n}', 'history.countOnly': 'History {n}',
406
+ 'role.me': 'Me', 'role.dsh': 'DSH',
407
+ 'tool.default': 'tool', 'tool.unknown': 'Unknown tool', 'tool.result': 'Result',
408
+ 'event.taskStart': '▶ Task started', 'event.taskEnd': '■ Task finished',
409
+ 'block.image': 'Image', 'block.thinking': '🧠 Thinking', 'block.toolCall': 'Tool call', 'block.toolResult': 'Tool result', 'block.unknown': 'Block · {type}',
410
+ 'truncated': '…(truncated)',
411
+ 'stats.title': 'This round', 'stats.roundTitle': 'This round', 'stats.turnsSteps': 'Turns / steps',
412
+ 'stats.llmTime': 'LLM time', 'stats.minutes': ' min', 'stats.outputCache': 'Output / cache read',
413
+ 'stats.ctxPressure': 'Context pressure', 'stats.permission': 'Permission', 'stats.empty': 'No stats yet',
414
+ 'goal.title': 'Goal', 'goal.pause': 'Pause', 'goal.resume': 'Resume', 'goal.complete': 'Complete', 'goal.edit': 'Edit goal', 'goal.clear': 'Clear',
415
+ 'goal.none': 'This session has no goal', 'goal.confirmClear': 'Clear the current goal? (the session stays)', 'goal.confirmComplete': 'Mark the goal as completed?',
416
+ 'goal.actionFailed': 'Goal action failed', 'goal.actionSubmitted': 'Goal action submitted',
417
+ 'goal.cannotEmpty': 'Goal cannot be empty', 'goal.updateFailed': 'Update failed', 'goal.updated': 'Goal updated',
418
+ 'todos.title': 'Todos',
419
+ 'subagent.title': 'Subagents', 'subagent.diagnostic': 'Diagnostic', 'subagent.running': '· running', 'subagent.interrupt': 'Interrupt',
420
+ 'subagent.confirmInterrupt': 'Interrupt this subagent\'s current turn?', 'subagent.interruptFailed': 'Interrupt failed', 'subagent.interruptSubmitted': 'Interrupt requested',
421
+ 'send.failed': 'Send failed', 'send.commandSent': 'Command sent', 'send.sent': 'Sent', 'send.commandExecuted': 'Command executed',
422
+ 'models.loading': 'Loading models…', 'models.loadFailed': 'Failed to load models: {msg}', 'models.unavailable': 'unavailable', 'models.none': 'No models available',
423
+ 'models.switchFailed': 'Model switch failed', 'models.switched': 'Switched model: {model}',
424
+ 'models.effortFailed': 'Failed to switch reasoning effort', 'models.effortSwitched': 'Reasoning effort: {effort}',
425
+ 'menu.commandsTitle': 'Commands', 'menu.compact': '/compact compact context', 'menu.goalView': '/goal view goal',
426
+ 'menu.goalPause': '/goal pause', 'menu.goalResume': '/goal resume',
427
+ 'menu.permissionTitle': 'Permissions', 'menu.permWorkspace': 'Workspace write (auto-approve)', 'menu.permFull': 'Full access (auto-approve all)',
428
+ 'menu.modelTitle': 'Model', 'menu.refresh': 'Refresh', 'menu.modelAuto': 'Auto-loads when opened', 'menu.effortTitle': 'Reasoning effort',
429
+ 'composer.placeholder': 'Message DSH…', 'composer.send': 'Send',
430
+ 'pending.title': 'Inbox', 'pending.count': '{n} items', 'pending.approvalTitle': '🔧 {tool} requests approval', 'pending.noReason': 'No reason given',
431
+ 'pending.allow': 'Allow', 'pending.reject': 'Reject', 'pending.question': '❓ {question}',
432
+ 'pending.questionCount': '{n} questions', 'pending.answer': 'Answer', 'pending.empty': 'Nothing pending',
433
+ 'pending.allowed': 'Allowed', 'pending.rejected': 'Rejected', 'pending.stale': 'Approval is no longer pending',
434
+ 'question.customPlaceholder': 'Other / custom answer (optional)', 'question.needAnswer': 'Select or type an answer first',
435
+ 'question.submitted': 'Answer submitted', 'question.stale': 'Question is no longer pending',
436
+ 'jobs.title': 'Background jobs', 'jobs.empty': 'No background jobs',
437
+ 'fs.loading': 'Loading…', 'fs.up': 'Up one level', 'fs.upload': 'Upload', 'fs.pause': 'Pause', 'fs.resume': 'Resume', 'fs.cancel': 'Cancel',
438
+ 'fs.pullDown': 'Pull to refresh', 'fs.pullRelease': 'Release to refresh', 'fs.refreshing': 'Refreshing…',
439
+ 'fs.noToken': 'No token set', 'fs.goSettings': 'Go to Settings and paste the gateway token first',
440
+ 'fs.notFound': 'Directory not found', 'fs.forbidden': 'Path is outside the allowed roots',
441
+ 'fs.loadFailed': 'Load failed: {msg}', 'fs.loadFailedToast': 'Failed to load file list: {msg}',
442
+ 'fs.emptyDir': 'Empty directory', 'fs.dir': 'Directory',
443
+ 'fs.downloadStarted': 'Downloading to "Download/dsh-remote"', 'fs.downloadFailed': 'Could not start download: {msg}',
444
+ 'fs.downloadUnsupported': 'This app version cannot download to the system folder, please update',
445
+ 'fs.uploadCancelled': 'Upload cancelled', 'fs.pausedErr': 'Paused', 'fs.cancelledErr': 'Cancelled',
446
+ 'fs.noTokenToast': 'Set a token in Settings first', 'fs.tooLarge': 'File exceeds the 2GB limit', 'fs.uploadBusy': 'Another upload is active, pause or cancel it first',
447
+ 'fs.networkError': 'Network error', 'fs.networkInterrupt': 'Network interrupted',
448
+ 'fs.confirmOverwrite': 'File exists (same size). Overwrite?', 'fs.confirmOverwrite2': 'File already exists. Overwrite?',
449
+ 'fs.uploadDone': 'Uploaded {name} (SHA-256 verified{resumed})', 'fs.resumedSuffix': ' · resumed',
450
+ 'fs.checksumMismatch': 'Checksum mismatch (SHA-256)', 'fs.checksumCleared': 'Checksum mismatch (SHA-256), bad chunks cleared',
451
+ 'fs.pausedPct': 'Paused · {pct}%', 'fs.pausedToast': 'Paused, tap "Resume" to continue',
452
+ 'fs.checksumFailed': 'Checksum failed · tap "Resume" to re-upload', 'fs.uploadInterrupted': 'Upload interrupted: {msg} (re-select the same file to resume)',
453
+ 'fs.alreadyRoot': 'Already at the allowed root',
454
+ 'update.currentV': 'Current version v{version}', 'update.noVersion': 'Version unavailable', 'update.needServer': 'Set a server address first',
455
+ 'update.noServerDesc': 'Set a server address first', 'update.checking': 'Checking for updates…',
456
+ 'update.found': 'New version v{version}{notes}', 'update.foundTitle': 'Update available', 'update.foundBody': 'v{version} is available',
457
+ 'update.latestV': 'Up to date v{version}', 'update.latestRemote': 'Latest version v{version}', 'update.latestToast': 'Already up to date',
458
+ 'update.checkFailedDesc': 'Check failed: {msg}', 'update.checkFailed': 'Update check failed: {msg}',
459
+ 'update.downloadStarted': 'Downloading, the install page will open when ready', 'update.downloadFailed': 'Could not start download: {msg}',
460
+ 'update.installUnsupported': 'This version cannot install in-app, opening browser download',
461
+ 'update.expand': 'Expand', 'update.collapse': 'Collapse',
462
+ 'scan.imageLoadFailed': 'Image failed to load', 'scan.decodeUnsupported': 'This device cannot decode images',
463
+ 'scan.browserHint': 'In a browser, open the host admin page and scan with your phone camera', 'scan.unsupported': 'This app version cannot scan, please update first',
464
+ 'scan.permissionDenied': 'Camera permission not granted',
465
+ 'scan.promptHeader': 'Scan DSH Remote pairing QR', 'scan.promptPhoto': 'Take photo', 'scan.promptGallery': 'Choose from gallery',
466
+ 'scan.noPhoto': 'No image received, try again', 'scan.noQr': 'No QR code detected, aim and try again',
467
+ 'scan.paired': 'Paired, connecting', 'scan.pairedLink': 'Paired via QR code', 'scan.notPair': 'Not a DSH Remote pairing QR code',
468
+ 'scan.cancelled': 'Scan cancelled', 'scan.failed': 'Scan failed: {msg}',
469
+ 'settings.servers': 'Servers (one or more)', 'settings.speedTest': 'Test', 'settings.add': 'Add',
470
+ 'settings.scanTitle': 'Scan to pair', 'settings.scanDesc': 'Photo/gallery QR decoding on device (no Google services); system camera or manual paste also work', 'settings.scan': 'Scan',
471
+ 'settings.notifyTitle': 'Notifications', 'settings.notifyDesc': 'Push on approvals / questions',
472
+ 'settings.toolsTitle': 'Show tool calls', 'settings.toolsDesc': 'Hide 🔧/📦 tool calls and results when off',
473
+ 'settings.langTitle': 'Language / 语言', 'settings.langDesc': 'UI language, follows system or switch manually',
474
+ 'settings.themeTitle': 'Theme', 'settings.themeDesc': 'Default deep space / Sunset / Elbphilharmonie / Prairie Tower',
475
+ 'theme.default': 'Default', 'theme.dark': 'Sunset', 'theme.light': 'Elbphilharmonie', 'theme.neutral': 'Prairie Tower',
476
+ 'theme.panelTitle': 'Choose theme', 'theme.close': 'Close',
477
+ 'settings.tokenTitle': 'Access token', 'settings.change': 'Change',
478
+ 'settings.hostTitle': 'DSH status', 'settings.hostProbing': 'Probing…', 'settings.probe': 'Probe', 'settings.probeFailed': 'Probe failed',
479
+ 'settings.hostDesc': 'DSH {version} · {cwd} · {n} attached sessions',
480
+ 'settings.updateTitle': 'Check for updates', 'settings.updateLoading': 'Loading…', 'settings.downloadUpdate': 'Download & install update', 'settings.check': 'Check',
481
+ 'settings.resetTitle': 'Clear local data', 'settings.resetDesc': 'Token and caches', 'settings.reset': 'Reset',
482
+ 'settings.confirmReset': 'Clear local token, servers and caches?', 'settings.notifyDenied': 'Notification permission not granted',
483
+ 'settings.toolsShown': 'Tool calls shown', 'settings.toolsHidden': 'Tool calls hidden',
484
+ 'settings.about': 'DSH Remote · accessible over LAN / Tailscale<br>Authenticated proxy in front of the local DSH 3080 endpoint',
485
+ 'modal.approvalTitle': 'Tool approval', 'modal.reject': 'Reject', 'modal.allowOnce': 'Allow once',
486
+ 'modal.questionTitle': 'DSH needs your answer', 'modal.later': 'Later', 'modal.submit': 'Submit',
487
+ 'modal.goalTitle': 'Goal control', 'modal.close': 'Close', 'modal.updateGoal': 'Update goal',
488
+ 'modal.statsTitle': 'This round',
489
+ 'time.justNow': 'just now', 'time.minAgo': ' min ago', 'time.hourAgo': ' hr ago'
490
+ }
491
+ }
492
+ </script>
234
493
  <script src="sha256.js"></script>
235
494
  <script src="jsqr.min.js"></script>
495
+ <script src="i18n.js"></script>
496
+ <script src="theme.js"></script>
236
497
  <script src="app.js"></script>
237
498
  </body>
238
499
  </html>