claude-remote 0.5.2 → 0.6.0

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/lib/cli.js CHANGED
@@ -137,6 +137,7 @@ function initConfig() {
137
137
  CWD: parsed.cwd,
138
138
  AUTH_TOKEN: authToken,
139
139
  AUTH_DISABLED: authDisabled,
140
+ ENABLE_WEB: process.env.ENABLE_WEB === '1',
140
141
  CLAUDE_EXTRA_ARGS: parsed.claudeArgs,
141
142
  DEBUG_TTY_INPUT: process.env.CLAUDE_REMOTE_DEBUG_TTY_INPUT === '1',
142
143
  blockedArgs: parsed.blocked,
@@ -144,6 +144,11 @@ function createHttpServer() {
144
144
  }
145
145
 
146
146
  // --- Static files ---
147
+ if (!state.ENABLE_WEB) {
148
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
149
+ res.end('Web UI disabled. Start with ENABLE_WEB=1 to enable.');
150
+ return;
151
+ }
147
152
  const filePath = path.join(__dirname, '..', 'web', url === '/' ? 'index.html' : url);
148
153
  const ext = path.extname(filePath);
149
154
  fs.readFile(filePath, (err, data) => {
package/lib/state.js CHANGED
@@ -38,6 +38,7 @@ const state = {
38
38
  CWD: process.cwd(),
39
39
  AUTH_TOKEN: null,
40
40
  AUTH_DISABLED: false,
41
+ ENABLE_WEB: false,
41
42
  CLAUDE_EXTRA_ARGS: [],
42
43
  DEBUG_TTY_INPUT: false,
43
44
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-remote",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Remote control bridge for Claude Code REPL - drive from phone/WebUI",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -10,7 +10,8 @@
10
10
  "server.js",
11
11
  "lib/",
12
12
  "hooks/",
13
- "bin/"
13
+ "bin/",
14
+ "web/"
14
15
  ],
15
16
  "scripts": {
16
17
  "start": "node server.js"
package/server.js CHANGED
@@ -16,6 +16,7 @@ state.PORT = config.PORT;
16
16
  state.CWD = config.CWD;
17
17
  state.AUTH_TOKEN = config.AUTH_TOKEN;
18
18
  state.AUTH_DISABLED = config.AUTH_DISABLED;
19
+ state.ENABLE_WEB = config.ENABLE_WEB;
19
20
  state.CLAUDE_EXTRA_ARGS = config.CLAUDE_EXTRA_ARGS;
20
21
  state.DEBUG_TTY_INPUT = config.DEBUG_TTY_INPUT;
21
22
 
@@ -54,6 +55,11 @@ server.listen(state.PORT, '0.0.0.0', () => {
54
55
  } else {
55
56
  banner += ` Token: ${config.AUTH_TOKEN}\n`;
56
57
  }
58
+ if (config.ENABLE_WEB) {
59
+ banner += ` WebUI: ENABLED\n`;
60
+ } else {
61
+ banner += ` WebUI: disabled (set ENABLE_WEB=1 to enable)\n`;
62
+ }
57
63
  if (config.unusedLegacyTokenEnv) {
58
64
  banner += ` Note: Ignoring legacy ${config.LEGACY_AUTH_TOKEN_ENV_VAR}; use ${config.AUTH_TOKEN_ENV_VAR} instead\n`;
59
65
  }
package/web/index.html ADDED
@@ -0,0 +1,346 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
6
+ <title>Claude Remote</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
8
+ <link rel="stylesheet" href="styles.css">
9
+ </head>
10
+ <body>
11
+
12
+ <!-- ===== Hub Screen ===== -->
13
+ <div id="connect-screen">
14
+ <div class="hub-header">
15
+ <div class="hub-header-left">
16
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="hub-logo-icon">
17
+ <circle cx="12" cy="12" r="10"/><path d="M8 12h8M12 8v8"/>
18
+ </svg>
19
+ <span class="hub-header-title">Claude Remote</span>
20
+ </div>
21
+ <button class="hub-add-btn" id="hub-add-btn" title="Add server">
22
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg>
23
+ </button>
24
+ </div>
25
+ <div class="hub-body" id="hub-body">
26
+ <div class="hub-empty" id="hub-empty">
27
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2"><circle cx="12" cy="12" r="10"/><path d="M8 12h8M12 8v8"/></svg>
28
+ <p>No servers yet</p>
29
+ <p class="hub-empty-sub">Tap + to add a server</p>
30
+ </div>
31
+ <div class="hub-section" id="hub-section-online" style="display:none">
32
+ <div class="hub-section-header">Online</div>
33
+ <div class="hub-card-list" id="hub-list-online"></div>
34
+ </div>
35
+ <div class="hub-section" id="hub-section-offline" style="display:none">
36
+ <div class="hub-section-header">Offline</div>
37
+ <div class="hub-card-list" id="hub-list-offline"></div>
38
+ </div>
39
+ </div>
40
+
41
+ <div id="hub-connect-overlay">
42
+ <div class="hub-connect-card">
43
+ <div class="hub-connect-spinner"></div>
44
+ <div class="hub-connect-title">Connecting...</div>
45
+ <div class="hub-connect-sub" id="hub-connect-sub">Preparing server session</div>
46
+ </div>
47
+ </div>
48
+
49
+ <!-- Add/Edit Server Dialog -->
50
+ <div id="hub-add-overlay">
51
+ <div id="hub-add-dialog">
52
+ <div class="hub-dialog-title" id="hub-dialog-title">Add Server</div>
53
+ <label class="hub-dialog-label">Server Address</label>
54
+ <input id="hub-dialog-addr" type="text" placeholder="192.168.1.100:3100 or wss://example.com"
55
+ inputmode="url" autocomplete="off" autocapitalize="off" spellcheck="false">
56
+ <label class="hub-dialog-label">Alias (optional)</label>
57
+ <input id="hub-dialog-alias" type="text" placeholder="e.g. Home Server"
58
+ autocomplete="off" autocapitalize="off" spellcheck="false">
59
+ <label class="hub-dialog-label">Token (optional)</label>
60
+ <div class="hub-dialog-token-row">
61
+ <input id="hub-dialog-token" type="password" placeholder="Auth token"
62
+ autocomplete="off" autocapitalize="off" spellcheck="false">
63
+ <button type="button" class="hub-dialog-token-toggle" id="hub-dialog-token-toggle" title="Show/hide token">
64
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
65
+ </button>
66
+ </div>
67
+ <div class="hub-dialog-error" id="hub-dialog-error"></div>
68
+ <div class="hub-dialog-actions">
69
+ <button class="hub-dialog-btn cancel" id="hub-dialog-cancel">Cancel</button>
70
+ <button class="hub-dialog-btn delete" id="hub-dialog-delete" style="display:none">Delete</button>
71
+ <button class="hub-dialog-btn save" id="hub-dialog-save">Save</button>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ <div id="toast-container"></div>
78
+
79
+ <!-- ===== Main App ===== -->
80
+ <div id="app" class="hidden">
81
+ <div id="conn-banner"><span class="banner-dot"></span><span id="conn-text">Disconnected</span></div>
82
+
83
+ <header>
84
+ <div class="header-left">
85
+ <button class="hbtn" id="btn-back" title="Disconnect">
86
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
87
+ </button>
88
+ <span class="status-dot starting" id="status-dot"></span>
89
+ <div class="header-info">
90
+ <h1 id="title">Claude Remote</h1>
91
+ <span class="header-meta" id="header-meta">
92
+ <span id="header-model"></span>
93
+ </span>
94
+ </div>
95
+ </div>
96
+ <div class="header-right">
97
+ <button class="hbtn" id="btn-settings" title="设置">
98
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
99
+ </button>
100
+ <button class="hbtn" id="btn-sessions" title="Sessions">
101
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12h18M3 6h18M3 18h18"/></svg>
102
+ </button>
103
+ </div>
104
+ </header>
105
+
106
+ <div id="chat-area">
107
+ <div id="messages">
108
+ <div class="welcome" id="welcome">
109
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2"><circle cx="12" cy="12" r="10"/><path d="M8 12h8M12 8v8"/></svg>
110
+ <h2>Claude Remote Control</h2>
111
+ <p>Connected. Send a message below to start.</p>
112
+ </div>
113
+ </div>
114
+ </div>
115
+
116
+ <!-- Todo Panel (above input, extends from chat box) -->
117
+ <div id="todo-panel">
118
+ <div id="todo-header" onclick="toggleTodoPanel()">
119
+ <div class="todo-header-left">
120
+ <svg class="todo-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M9 18l6-6-6-6"/></svg>
121
+ <span class="todo-title">Tasks</span>
122
+ <span class="todo-badge" id="todo-badge">0</span>
123
+ </div>
124
+ <div class="todo-progress-mini" id="todo-progress-mini">
125
+ <div class="todo-progress-bar" id="todo-progress-bar"></div>
126
+ </div>
127
+ <span class="todo-summary" id="todo-summary"></span>
128
+ </div>
129
+ <div id="todo-list"></div>
130
+ </div>
131
+
132
+ <div id="input-area">
133
+ <div id="cmd-menu"></div>
134
+ <div id="image-preview" class="hidden">
135
+ <div id="image-preview-inner">
136
+ <img id="image-preview-img">
137
+ <div id="image-upload-overlay" class="hidden">
138
+ <div class="upload-ring-wrap">
139
+ <svg viewBox="0 0 36 36" class="upload-ring">
140
+ <path class="upload-ring-track" d="M18 2.5a15.5 15.5 0 1 1 0 31a15.5 15.5 0 1 1 0-31"/>
141
+ <path id="image-upload-ring" class="upload-ring-progress" d="M18 2.5a15.5 15.5 0 1 1 0 31a15.5 15.5 0 1 1 0-31"/>
142
+ </svg>
143
+ <span id="image-upload-text">0%</span>
144
+ </div>
145
+ </div>
146
+ <button id="image-preview-remove" title="Remove">&times;</button>
147
+ </div>
148
+ </div>
149
+ <div id="input-row">
150
+ <div class="input-pill">
151
+ <textarea id="input" placeholder="Reply..." rows="1"></textarea>
152
+ <button id="btn-image" class="pill-icon-btn" title="Upload image">
153
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>
154
+ </button>
155
+ <button id="btn-send" title="Send">
156
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 19V5M5 12l7-7 7 7"/></svg>
157
+ </button>
158
+ </div>
159
+ <input type="file" id="image-file-input" accept="image/*" style="display:none">
160
+ <button id="btn-scroll" title="Scroll to bottom">
161
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12l7 7 7-7"/></svg>
162
+ </button>
163
+ </div>
164
+ </div>
165
+
166
+ <!-- Model Picker -->
167
+ <div id="model-picker">
168
+ <div id="model-dialog">
169
+ <div class="model-title">Switch Model</div>
170
+ <div id="model-list"></div>
171
+ </div>
172
+ </div>
173
+
174
+ <!-- Permission Overlay -->
175
+ <div id="perm-overlay">
176
+ <div id="perm-dialog">
177
+ <div class="perm-header">
178
+ <div class="perm-icon">
179
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 9v4M12 17h.01"/><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></svg>
180
+ </div>
181
+ <div>
182
+ <div class="perm-title" id="perm-tool-name">Bash</div>
183
+ <div class="perm-sub">Requesting permission</div>
184
+ </div>
185
+ </div>
186
+ <div class="perm-detail" id="perm-detail"></div>
187
+ <div class="perm-actions">
188
+ <button class="perm-btn-deny" id="perm-deny">Deny</button>
189
+ <button class="perm-btn-allow" id="perm-allow">Allow</button>
190
+ </div>
191
+ </div>
192
+ </div>
193
+
194
+ <!-- Question Overlay (AskUserQuestion) -->
195
+ <div id="question-overlay">
196
+ <div id="question-dialog">
197
+ <div class="question-header">
198
+ <div class="question-icon">
199
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3"/><circle cx="12" cy="17" r=".5"/></svg>
200
+ </div>
201
+ <div>
202
+ <div class="question-title" id="question-header-text">Question</div>
203
+ <div class="question-sub">Claude needs your input</div>
204
+ </div>
205
+ </div>
206
+ <div class="question-text" id="question-text"></div>
207
+ <div class="question-options" id="question-options"></div>
208
+ <div class="question-other-row">
209
+ <input class="question-other-input" id="question-other-input" type="text" placeholder="Other...">
210
+ <button class="question-other-btn" id="question-other-btn">Send</button>
211
+ </div>
212
+ </div>
213
+ </div>
214
+
215
+ <!-- Plan Approval Overlay (ExitPlanMode) -->
216
+ <div id="plan-overlay">
217
+ <div id="plan-dialog">
218
+ <div class="plan-header">
219
+ <div class="plan-icon">
220
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/><path d="M9 12l2 2 4-4"/></svg>
221
+ </div>
222
+ <div>
223
+ <div class="plan-title">Plan Ready</div>
224
+ <div class="plan-sub">Would you like to proceed?</div>
225
+ </div>
226
+ </div>
227
+ <div class="plan-content" id="plan-content"></div>
228
+ <div class="plan-options" id="plan-options"></div>
229
+ <div class="plan-other-row">
230
+ <input class="plan-other-input" id="plan-other-input" type="text" placeholder="Tell Claude what to change...">
231
+ <button class="plan-other-btn" id="plan-other-btn">Send</button>
232
+ </div>
233
+ </div>
234
+ </div>
235
+
236
+ <!-- Settings Overlay -->
237
+ <div id="settings-overlay">
238
+ <div id="settings-dialog">
239
+ <div class="settings-header">
240
+ <div class="settings-title">设置</div>
241
+ <button class="settings-close" id="settings-close">
242
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
243
+ </button>
244
+ </div>
245
+ <div class="settings-section">
246
+ <div class="settings-label">命令审批</div>
247
+ <div class="settings-desc">控制哪些工具需要手动审批</div>
248
+ <div class="settings-options" id="approval-options">
249
+ <label class="settings-opt" data-mode="default">
250
+ <input type="radio" name="approval-mode" value="default" checked>
251
+ <div class="settings-opt-body">
252
+ <div class="settings-opt-label">默认</div>
253
+ <div class="settings-opt-desc">所有命令都需要手动审批</div>
254
+ </div>
255
+ </label>
256
+ <label class="settings-opt" data-mode="partial">
257
+ <input type="radio" name="approval-mode" value="partial">
258
+ <div class="settings-opt-body">
259
+ <div class="settings-opt-label">部分自动审批</div>
260
+ <div class="settings-opt-desc">Read / Write / Edit / Glob / Grep 自动放行,其他需要审批</div>
261
+ </div>
262
+ </label>
263
+ <label class="settings-opt" data-mode="all">
264
+ <input type="radio" name="approval-mode" value="all">
265
+ <div class="settings-opt-body">
266
+ <div class="settings-opt-label">全部自动审批</div>
267
+ <div class="settings-opt-desc">所有命令自动放行(包括 Bash)</div>
268
+ </div>
269
+ </label>
270
+ </div>
271
+ </div>
272
+ <div class="settings-section">
273
+ <div class="settings-label">工作目录</div>
274
+ <div class="settings-desc">切换 Claude 的工作目录(将重启进程)</div>
275
+ <div class="settings-cwd-current" id="settings-cwd-display"></div>
276
+ <div class="settings-cwd-list" id="settings-cwd-list"></div>
277
+ <div class="settings-cwd-input-row">
278
+ <input id="settings-cwd-input" type="text" placeholder="Select a folder..." autocomplete="off" autocapitalize="off" spellcheck="false" readonly>
279
+ <button id="btn-change-cwd" class="settings-cwd-btn">Browse</button>
280
+ </div>
281
+ </div>
282
+ </div>
283
+ </div>
284
+
285
+ <!-- Session Drawer -->
286
+ <div id="session-overlay">
287
+ <div id="session-drawer">
288
+ <div class="drawer-header">
289
+ <span class="drawer-title">Sessions</span>
290
+ <button class="drawer-close" id="session-drawer-close">
291
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
292
+ </button>
293
+ </div>
294
+ <div id="session-list" class="drawer-body">
295
+ <div class="drawer-loading" id="drawer-loading">Loading...</div>
296
+ </div>
297
+ <div class="drawer-footer">
298
+ <button class="drawer-btn-new" id="btn-new-session">+ New Session</button>
299
+ </div>
300
+ </div>
301
+ </div>
302
+
303
+ <!-- Directory Picker -->
304
+ <div id="dir-overlay">
305
+ <div id="dir-drawer">
306
+ <div class="drawer-header">
307
+ <span class="drawer-title">Folders</span>
308
+ <button class="drawer-close" id="dir-drawer-close">
309
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
310
+ </button>
311
+ </div>
312
+ <div class="dir-toolbar">
313
+ <div class="dir-current" id="dir-current-path"></div>
314
+ <div class="dir-roots" id="dir-roots"></div>
315
+ </div>
316
+ <div id="dir-list" class="drawer-body">
317
+ <div class="drawer-loading">Loading...</div>
318
+ </div>
319
+ <div class="drawer-footer">
320
+ <button class="drawer-btn-select" id="btn-select-cwd">Use This Folder</button>
321
+ </div>
322
+ </div>
323
+ </div>
324
+
325
+
326
+ </div>
327
+
328
+ <!-- Confirm Dialog (outside #app so it's visible on connect screen too) -->
329
+ <div id="confirm-overlay">
330
+ <div id="confirm-dialog">
331
+ <div class="confirm-icon">
332
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 9v4M12 17h.01"/><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></svg>
333
+ </div>
334
+ <div class="confirm-title">警告</div>
335
+ <div class="confirm-text" id="confirm-text"></div>
336
+ <div class="confirm-actions">
337
+ <button class="confirm-btn-cancel" id="confirm-cancel">取消</button>
338
+ <button class="confirm-btn-ok" id="confirm-ok">确认</button>
339
+ </div>
340
+ </div>
341
+ </div>
342
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
343
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
344
+ <script type="module" src="main.js"></script>
345
+ </body>
346
+ </html>
package/web/main.js ADDED
@@ -0,0 +1,68 @@
1
+ // ============================================================
2
+ // Claude Remote — Android Client (Tauri 2.0)
3
+ // Entry point — imports and initializes all modules
4
+ // ============================================================
5
+
6
+ import { initConfirm } from './modules/confirm.js';
7
+ import { initHub } from './modules/hub.js';
8
+ import { initRenderer } from './modules/renderer.js';
9
+ import { initInput } from './modules/input.js';
10
+ import { initImageUpload } from './modules/image-upload.js';
11
+ import { initPermissions } from './modules/permissions.js';
12
+ import { initInteractions } from './modules/interactions.js';
13
+ import { initSettings, initSettingsValues } from './modules/settings.js';
14
+ import { initSessions } from './modules/sessions.js';
15
+ import { initDirPicker } from './modules/dir-picker.js';
16
+ import { initModelPicker } from './modules/model-picker.js';
17
+ import { initKeyboard } from './modules/keyboard.js';
18
+ import { initWaiting } from './modules/waiting.js';
19
+
20
+ const INIT_STEPS = [
21
+ ['confirm', initConfirm],
22
+ ['renderer', initRenderer],
23
+ ['waiting', initWaiting],
24
+ ['hub', initHub],
25
+ ['input', initInput],
26
+ ['image-upload', initImageUpload],
27
+ ['permissions', initPermissions],
28
+ ['interactions', initInteractions],
29
+ ['settings-values', initSettingsValues],
30
+ ['settings', initSettings],
31
+ ['sessions', initSessions],
32
+ ['dir-picker', initDirPicker],
33
+ ['model-picker', initModelPicker],
34
+ ['keyboard', initKeyboard],
35
+ ];
36
+
37
+ let bootstrapped = false;
38
+
39
+ function runInitSteps() {
40
+ const failures = [];
41
+ for (const [name, init] of INIT_STEPS) {
42
+ try {
43
+ init();
44
+ } catch (err) {
45
+ failures.push(name);
46
+ console.error(`[bootstrap] init failed: ${name}`, err);
47
+ }
48
+ }
49
+ if (failures.length) {
50
+ console.error(`[bootstrap] completed with failures: ${failures.join(', ')}`);
51
+ }
52
+ }
53
+
54
+ export function bootstrapApp() {
55
+ if (bootstrapped) return;
56
+ bootstrapped = true;
57
+ runInitSteps();
58
+ }
59
+
60
+ function startWhenReady() {
61
+ if (document.readyState === 'loading') {
62
+ document.addEventListener('DOMContentLoaded', bootstrapApp, { once: true });
63
+ return;
64
+ }
65
+ bootstrapApp();
66
+ }
67
+
68
+ startWhenReady();
@@ -0,0 +1,118 @@
1
+ // ============================================================
2
+ // Chat Cache (IndexedDB) — pure data layer
3
+ // ============================================================
4
+ import {
5
+ CHAT_CACHE_DB, CHAT_CACHE_STORE, CHAT_CACHE_MAX_SESSIONS,
6
+ CHAT_CACHE_MAX_TOTAL_BYTES, CHAT_CACHE_MAX_SESSION_BYTES,
7
+ } from './constants.js';
8
+
9
+ let chatCacheDbPromise = null;
10
+
11
+ export function openChatCacheDb() {
12
+ if (chatCacheDbPromise) return chatCacheDbPromise;
13
+ chatCacheDbPromise = new Promise((resolve, reject) => {
14
+ if (typeof indexedDB === 'undefined') {
15
+ reject(new Error('IndexedDB unavailable'));
16
+ return;
17
+ }
18
+ const req = indexedDB.open(CHAT_CACHE_DB, 1);
19
+ req.onerror = () => reject(req.error || new Error('Failed to open chat cache'));
20
+ req.onupgradeneeded = () => {
21
+ const db = req.result;
22
+ if (!db.objectStoreNames.contains(CHAT_CACHE_STORE)) {
23
+ const store = db.createObjectStore(CHAT_CACHE_STORE, { keyPath: 'cacheKey' });
24
+ store.createIndex('updatedAt', 'updatedAt', { unique: false });
25
+ }
26
+ };
27
+ req.onsuccess = () => resolve(req.result);
28
+ }).catch(err => {
29
+ console.warn('[chat-cache]', err);
30
+ chatCacheDbPromise = null;
31
+ throw err;
32
+ });
33
+ return chatCacheDbPromise;
34
+ }
35
+
36
+ export async function chatCacheRead(cacheKey) {
37
+ const db = await openChatCacheDb();
38
+ return new Promise((resolve, reject) => {
39
+ const tx = db.transaction(CHAT_CACHE_STORE, 'readonly');
40
+ const req = tx.objectStore(CHAT_CACHE_STORE).get(cacheKey);
41
+ req.onerror = () => reject(req.error || new Error('Failed to read chat cache'));
42
+ req.onsuccess = () => resolve(req.result || null);
43
+ });
44
+ }
45
+
46
+ export async function chatCacheWrite(record) {
47
+ const db = await openChatCacheDb();
48
+ return new Promise((resolve, reject) => {
49
+ const tx = db.transaction(CHAT_CACHE_STORE, 'readwrite');
50
+ tx.oncomplete = () => resolve();
51
+ tx.onerror = () => reject(tx.error || new Error('Failed to write chat cache'));
52
+ tx.objectStore(CHAT_CACHE_STORE).put(record);
53
+ });
54
+ }
55
+
56
+ export async function chatCacheDelete(cacheKey) {
57
+ const db = await openChatCacheDb();
58
+ return new Promise((resolve, reject) => {
59
+ const tx = db.transaction(CHAT_CACHE_STORE, 'readwrite');
60
+ tx.oncomplete = () => resolve();
61
+ tx.onerror = () => reject(tx.error || new Error('Failed to delete chat cache'));
62
+ tx.objectStore(CHAT_CACHE_STORE).delete(cacheKey);
63
+ });
64
+ }
65
+
66
+ export async function chatCacheReadAll() {
67
+ const db = await openChatCacheDb();
68
+ return new Promise((resolve, reject) => {
69
+ const tx = db.transaction(CHAT_CACHE_STORE, 'readonly');
70
+ const req = tx.objectStore(CHAT_CACHE_STORE).getAll();
71
+ req.onerror = () => reject(req.error || new Error('Failed to list chat cache'));
72
+ req.onsuccess = () => resolve(Array.isArray(req.result) ? req.result : []);
73
+ });
74
+ }
75
+
76
+ export function buildCacheKey(addr, sessionId) {
77
+ return `${addr}::${sessionId}`;
78
+ }
79
+
80
+ export function estimateCacheBytes(record) {
81
+ const payload = JSON.stringify({
82
+ sessionId: record.sessionId || '',
83
+ serverAddr: record.serverAddr || '',
84
+ html: record.html || '',
85
+ seenUuids: Array.isArray(record.seenUuids) ? record.seenUuids : [],
86
+ todoTasks: Array.isArray(record.todoTasks) ? record.todoTasks : [],
87
+ todoPanelOpen: !!record.todoPanelOpen,
88
+ cwd: record.cwd || '',
89
+ model: record.model || '',
90
+ lastSeq: record.lastSeq || 0,
91
+ updatedAt: record.updatedAt || 0,
92
+ });
93
+ return payload.length;
94
+ }
95
+
96
+ export async function pruneChatCache() {
97
+ let records;
98
+ try {
99
+ records = await chatCacheReadAll();
100
+ } catch {
101
+ return;
102
+ }
103
+
104
+ records.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
105
+ let totalBytes = 0;
106
+ const removals = [];
107
+
108
+ records.forEach((record, idx) => {
109
+ const sizeBytes = Number.isFinite(record.sizeBytes) ? record.sizeBytes : estimateCacheBytes(record);
110
+ totalBytes += sizeBytes;
111
+ const overCount = idx >= CHAT_CACHE_MAX_SESSIONS;
112
+ const overTotal = totalBytes > CHAT_CACHE_MAX_TOTAL_BYTES;
113
+ const overSingle = sizeBytes > CHAT_CACHE_MAX_SESSION_BYTES;
114
+ if (overCount || overTotal || overSingle) removals.push(record.cacheKey);
115
+ });
116
+
117
+ await Promise.all(removals.map(cacheKey => chatCacheDelete(cacheKey).catch(() => {})));
118
+ }
@@ -0,0 +1,25 @@
1
+ // ============================================================
2
+ // Confirm Dialog
3
+ // ============================================================
4
+ import { $ } from './utils.js';
5
+
6
+ let confirmResolve = null;
7
+
8
+ export function showConfirm(text) {
9
+ return new Promise(resolve => {
10
+ $('confirm-text').textContent = text;
11
+ $('confirm-overlay').classList.add('visible');
12
+ confirmResolve = resolve;
13
+ });
14
+ }
15
+
16
+ export function initConfirm() {
17
+ $('confirm-ok').addEventListener('click', () => {
18
+ $('confirm-overlay').classList.remove('visible');
19
+ if (confirmResolve) { confirmResolve(true); confirmResolve = null; }
20
+ });
21
+ $('confirm-cancel').addEventListener('click', () => {
22
+ $('confirm-overlay').classList.remove('visible');
23
+ if (confirmResolve) { confirmResolve(false); confirmResolve = null; }
24
+ });
25
+ }