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