claude-remote-cli 1.7.1 → 1.8.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/README.md +30 -16
- package/dist/server/index.js +7 -2
- package/dist/server/sessions.js +2 -2
- package/package.json +1 -1
- package/public/app.js +263 -1
- package/public/index.html +13 -2
- package/public/manifest.json +8 -0
- package/public/style.css +45 -13
package/README.md
CHANGED
|
@@ -17,14 +17,14 @@ claude-remote-cli
|
|
|
17
17
|
git clone https://github.com/donovan-yohan/claude-remote-cli.git
|
|
18
18
|
cd claude-remote-cli
|
|
19
19
|
npm install
|
|
20
|
-
|
|
20
|
+
npm start
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
On first launch you'll be prompted to set a PIN. Then open `http://localhost:3456` in your browser.
|
|
24
24
|
|
|
25
25
|
## Prerequisites
|
|
26
26
|
|
|
27
|
-
- **Node.js
|
|
27
|
+
- **Node.js 24+**
|
|
28
28
|
- **Claude Code CLI** installed and available in your PATH (or configure `claudeCommand` in config)
|
|
29
29
|
|
|
30
30
|
## Platform Support
|
|
@@ -38,6 +38,7 @@ Usage: claude-remote-cli [options]
|
|
|
38
38
|
claude-remote-cli <command>
|
|
39
39
|
|
|
40
40
|
Commands:
|
|
41
|
+
update Update to the latest version from npm
|
|
41
42
|
install Install as a background service (survives reboot)
|
|
42
43
|
uninstall Stop and remove the background service
|
|
43
44
|
status Show whether the service is running
|
|
@@ -106,32 +107,45 @@ The PIN hash is stored in config under `pinHash`. To reset:
|
|
|
106
107
|
|
|
107
108
|
- **PIN-protected access** with rate limiting
|
|
108
109
|
- **Worktree isolation** — each session runs in its own Claude Code `--worktree`
|
|
109
|
-
- **Resume sessions** — click inactive worktrees to reconnect
|
|
110
|
+
- **Resume sessions** — click inactive worktrees to reconnect with `--continue`
|
|
111
|
+
- **Persistent session names** — display names and timestamps survive server restarts
|
|
112
|
+
- **Clipboard image paste** — paste screenshots directly into remote terminal sessions (macOS clipboard + xclip on Linux)
|
|
113
|
+
- **Yolo mode** — skip permission prompts with `--dangerously-skip-permissions` (per-session checkbox or context menu)
|
|
114
|
+
- **Worktree cleanup** — delete inactive worktrees from the context menu (removes worktree, prunes refs, deletes branch)
|
|
110
115
|
- **Sidebar filters** — filter by root directory, repo, or text search
|
|
111
|
-
- **Inline rename** — rename sessions with the pencil icon
|
|
116
|
+
- **Inline rename** — rename sessions with the pencil icon
|
|
112
117
|
- **Scrollback buffer** — reconnect to a session and see prior output
|
|
113
|
-
- **Touch toolbar** — mobile-friendly buttons for special keys (
|
|
118
|
+
- **Touch toolbar** — mobile-friendly buttons for special keys (hidden on desktop)
|
|
114
119
|
- **Responsive layout** — works on desktop and mobile with slide-out sidebar
|
|
115
120
|
- **Real-time updates** — worktree changes on disk are pushed to the browser instantly via WebSocket
|
|
121
|
+
- **Update notifications** — toast notification when a new version is available, with one-click update
|
|
122
|
+
- **CLI self-update** — `claude-remote-cli update` to update from npm
|
|
116
123
|
|
|
117
124
|
## Architecture
|
|
118
125
|
|
|
126
|
+
TypeScript + ESM backend compiled to `dist/`. Vanilla JS frontend (no build step).
|
|
127
|
+
|
|
119
128
|
```
|
|
120
129
|
claude-remote-cli/
|
|
121
130
|
├── bin/
|
|
122
|
-
│ └── claude-remote-cli.
|
|
131
|
+
│ └── claude-remote-cli.ts # CLI entry point
|
|
123
132
|
├── server/
|
|
124
|
-
│ ├── index.
|
|
125
|
-
│ ├── sessions.
|
|
126
|
-
│ ├── ws.
|
|
127
|
-
│ ├── watcher.
|
|
128
|
-
│ ├── auth.
|
|
129
|
-
│
|
|
133
|
+
│ ├── index.ts # Express server, REST API routes
|
|
134
|
+
│ ├── sessions.ts # PTY session manager (node-pty)
|
|
135
|
+
│ ├── ws.ts # WebSocket relay (PTY ↔ browser)
|
|
136
|
+
│ ├── watcher.ts # File watcher for .claude/worktrees/ changes
|
|
137
|
+
│ ├── auth.ts # PIN hashing, verification, rate limiting
|
|
138
|
+
│ ├── config.ts # Config loading/saving, worktree metadata
|
|
139
|
+
│ ├── clipboard.ts # System clipboard operations (image paste)
|
|
140
|
+
│ ├── service.ts # Background service management (launchd/systemd)
|
|
141
|
+
│ └── types.ts # Shared TypeScript interfaces
|
|
130
142
|
├── public/
|
|
131
|
-
│ ├── index.html
|
|
132
|
-
│ ├── app.js
|
|
133
|
-
│ ├── style.css
|
|
134
|
-
│ └── vendor/
|
|
143
|
+
│ ├── index.html # Single-page app
|
|
144
|
+
│ ├── app.js # Frontend logic (ES5, no build step)
|
|
145
|
+
│ ├── style.css # Styles (dark theme)
|
|
146
|
+
│ └── vendor/ # Self-hosted xterm.js + addon-fit
|
|
147
|
+
├── test/ # Unit tests (node:test)
|
|
148
|
+
├── dist/ # Compiled output (gitignored)
|
|
135
149
|
├── config.example.json
|
|
136
150
|
└── package.json
|
|
137
151
|
```
|
package/dist/server/index.js
CHANGED
|
@@ -332,21 +332,26 @@ async function main() {
|
|
|
332
332
|
let args;
|
|
333
333
|
let cwd;
|
|
334
334
|
let worktreeName;
|
|
335
|
+
let sessionRepoPath;
|
|
335
336
|
if (worktreePath) {
|
|
336
337
|
// Resume existing worktree — run claude --continue inside the worktree directory
|
|
337
338
|
args = ['--continue', ...baseArgs];
|
|
338
339
|
cwd = worktreePath;
|
|
340
|
+
sessionRepoPath = worktreePath;
|
|
339
341
|
worktreeName = worktreePath.split('/').pop() || '';
|
|
340
342
|
}
|
|
341
343
|
else {
|
|
342
|
-
// New worktree
|
|
344
|
+
// New worktree — PTY spawns in the repo root (so `claude --worktree X` works),
|
|
345
|
+
// but repoPath points to the expected worktree dir for identity/metadata matching
|
|
343
346
|
worktreeName = 'mobile-' + name + '-' + Date.now().toString(36);
|
|
344
347
|
args = ['--worktree', worktreeName, ...baseArgs];
|
|
345
348
|
cwd = repoPath;
|
|
349
|
+
sessionRepoPath = path.join(repoPath, '.claude', 'worktrees', worktreeName);
|
|
346
350
|
}
|
|
347
351
|
const session = sessions.create({
|
|
348
352
|
repoName: name,
|
|
349
|
-
repoPath:
|
|
353
|
+
repoPath: sessionRepoPath,
|
|
354
|
+
cwd,
|
|
350
355
|
root,
|
|
351
356
|
worktreeName,
|
|
352
357
|
displayName: worktreeName,
|
package/dist/server/sessions.js
CHANGED
|
@@ -6,7 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import { readMeta, writeMeta } from './config.js';
|
|
7
7
|
// In-memory registry: id -> Session
|
|
8
8
|
const sessions = new Map();
|
|
9
|
-
function create({ repoName, repoPath, root, worktreeName, displayName, command, args = [], cols = 80, rows = 24, configPath }) {
|
|
9
|
+
function create({ repoName, repoPath, cwd, root, worktreeName, displayName, command, args = [], cols = 80, rows = 24, configPath }) {
|
|
10
10
|
const id = crypto.randomBytes(8).toString('hex');
|
|
11
11
|
const createdAt = new Date().toISOString();
|
|
12
12
|
// Strip CLAUDECODE env var to allow spawning claude inside a claude-managed server
|
|
@@ -16,7 +16,7 @@ function create({ repoName, repoPath, root, worktreeName, displayName, command,
|
|
|
16
16
|
name: 'xterm-256color',
|
|
17
17
|
cols,
|
|
18
18
|
rows,
|
|
19
|
-
cwd: repoPath,
|
|
19
|
+
cwd: cwd || repoPath,
|
|
20
20
|
env,
|
|
21
21
|
});
|
|
22
22
|
// Scrollback buffer: stores all PTY output so we can replay on WebSocket (re)connect
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -50,6 +50,13 @@
|
|
|
50
50
|
var imageToastText = document.getElementById('image-toast-text');
|
|
51
51
|
var imageToastInsert = document.getElementById('image-toast-insert');
|
|
52
52
|
var imageToastDismiss = document.getElementById('image-toast-dismiss');
|
|
53
|
+
var imageFileInput = document.getElementById('image-file-input');
|
|
54
|
+
var uploadImageBtn = document.getElementById('upload-image-btn');
|
|
55
|
+
var terminalScrollbar = document.getElementById('terminal-scrollbar');
|
|
56
|
+
var terminalScrollbarThumb = document.getElementById('terminal-scrollbar-thumb');
|
|
57
|
+
var mobileInput = document.getElementById('mobile-input');
|
|
58
|
+
var mobileHeader = document.getElementById('mobile-header');
|
|
59
|
+
var isMobileDevice = 'ontouchstart' in window;
|
|
53
60
|
|
|
54
61
|
// Context menu state
|
|
55
62
|
var contextMenuTarget = null; // stores { worktreePath, repoPath, name }
|
|
@@ -149,6 +156,9 @@
|
|
|
149
156
|
term.open(terminalContainer);
|
|
150
157
|
fitAddon.fit();
|
|
151
158
|
|
|
159
|
+
term.onScroll(updateScrollbar);
|
|
160
|
+
term.onWriteParsed(updateScrollbar);
|
|
161
|
+
|
|
152
162
|
term.onData(function (data) {
|
|
153
163
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
154
164
|
ws.send(data);
|
|
@@ -212,6 +222,7 @@
|
|
|
212
222
|
var resizeObserver = new ResizeObserver(function () {
|
|
213
223
|
fitAddon.fit();
|
|
214
224
|
sendResize();
|
|
225
|
+
updateScrollbar();
|
|
215
226
|
});
|
|
216
227
|
resizeObserver.observe(terminalContainer);
|
|
217
228
|
}
|
|
@@ -222,6 +233,82 @@
|
|
|
222
233
|
}
|
|
223
234
|
}
|
|
224
235
|
|
|
236
|
+
// ── Terminal Scrollbar ──────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
var scrollbarDragging = false;
|
|
239
|
+
var scrollbarDragStartY = 0;
|
|
240
|
+
var scrollbarDragStartTop = 0;
|
|
241
|
+
|
|
242
|
+
function updateScrollbar() {
|
|
243
|
+
if (!term || !terminalScrollbar || terminalScrollbar.style.display === 'none') return;
|
|
244
|
+
var buf = term.buffer.active;
|
|
245
|
+
var totalLines = buf.baseY + term.rows;
|
|
246
|
+
var viewportTop = buf.viewportY;
|
|
247
|
+
var trackHeight = terminalScrollbar.clientHeight;
|
|
248
|
+
|
|
249
|
+
if (totalLines <= term.rows) {
|
|
250
|
+
terminalScrollbarThumb.style.display = 'none';
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
terminalScrollbarThumb.style.display = 'block';
|
|
255
|
+
var thumbHeight = Math.max(20, (term.rows / totalLines) * trackHeight);
|
|
256
|
+
var thumbTop = (viewportTop / (totalLines - term.rows)) * (trackHeight - thumbHeight);
|
|
257
|
+
|
|
258
|
+
terminalScrollbarThumb.style.height = thumbHeight + 'px';
|
|
259
|
+
terminalScrollbarThumb.style.top = thumbTop + 'px';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function scrollbarScrollToY(clientY) {
|
|
263
|
+
var rect = terminalScrollbar.getBoundingClientRect();
|
|
264
|
+
var buf = term.buffer.active;
|
|
265
|
+
var totalLines = buf.baseY + term.rows;
|
|
266
|
+
if (totalLines <= term.rows) return;
|
|
267
|
+
|
|
268
|
+
var thumbHeight = Math.max(20, (term.rows / totalLines) * terminalScrollbar.clientHeight);
|
|
269
|
+
var trackUsable = terminalScrollbar.clientHeight - thumbHeight;
|
|
270
|
+
var relativeY = clientY - rect.top - thumbHeight / 2;
|
|
271
|
+
var ratio = Math.max(0, Math.min(1, relativeY / trackUsable));
|
|
272
|
+
var targetLine = Math.round(ratio * (totalLines - term.rows));
|
|
273
|
+
|
|
274
|
+
term.scrollToLine(targetLine);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
terminalScrollbarThumb.addEventListener('touchstart', function (e) {
|
|
278
|
+
e.preventDefault();
|
|
279
|
+
scrollbarDragging = true;
|
|
280
|
+
scrollbarDragStartY = e.touches[0].clientY;
|
|
281
|
+
scrollbarDragStartTop = parseInt(terminalScrollbarThumb.style.top, 10) || 0;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (isMobileDevice) {
|
|
285
|
+
document.addEventListener('touchmove', function (e) {
|
|
286
|
+
if (!scrollbarDragging) return;
|
|
287
|
+
e.preventDefault();
|
|
288
|
+
var deltaY = e.touches[0].clientY - scrollbarDragStartY;
|
|
289
|
+
var buf = term.buffer.active;
|
|
290
|
+
var totalLines = buf.baseY + term.rows;
|
|
291
|
+
if (totalLines <= term.rows) return;
|
|
292
|
+
|
|
293
|
+
var thumbHeight = Math.max(20, (term.rows / totalLines) * terminalScrollbar.clientHeight);
|
|
294
|
+
var trackUsable = terminalScrollbar.clientHeight - thumbHeight;
|
|
295
|
+
var newTop = Math.max(0, Math.min(trackUsable, scrollbarDragStartTop + deltaY));
|
|
296
|
+
var ratio = newTop / trackUsable;
|
|
297
|
+
var targetLine = Math.round(ratio * (totalLines - term.rows));
|
|
298
|
+
|
|
299
|
+
term.scrollToLine(targetLine);
|
|
300
|
+
}, { passive: false });
|
|
301
|
+
|
|
302
|
+
document.addEventListener('touchend', function () {
|
|
303
|
+
scrollbarDragging = false;
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
terminalScrollbar.addEventListener('click', function (e) {
|
|
308
|
+
if (e.target === terminalScrollbarThumb) return;
|
|
309
|
+
scrollbarScrollToY(e.clientY);
|
|
310
|
+
});
|
|
311
|
+
|
|
225
312
|
// ── WebSocket / Session Connection ──────────────────────────────────────────
|
|
226
313
|
|
|
227
314
|
function connectToSession(sessionId) {
|
|
@@ -922,9 +1009,13 @@
|
|
|
922
1009
|
|
|
923
1010
|
// ── Touch Toolbar ───────────────────────────────────────────────────────────
|
|
924
1011
|
|
|
925
|
-
|
|
1012
|
+
function handleToolbarButton(e) {
|
|
926
1013
|
var btn = e.target.closest('button');
|
|
927
1014
|
if (!btn) return;
|
|
1015
|
+
// Skip the upload button (handled separately)
|
|
1016
|
+
if (btn.id === 'upload-image-btn') return;
|
|
1017
|
+
|
|
1018
|
+
e.preventDefault();
|
|
928
1019
|
|
|
929
1020
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
930
1021
|
|
|
@@ -936,6 +1027,19 @@
|
|
|
936
1027
|
} else if (key !== undefined) {
|
|
937
1028
|
ws.send(key);
|
|
938
1029
|
}
|
|
1030
|
+
|
|
1031
|
+
// Re-focus the mobile input to keep keyboard open
|
|
1032
|
+
if (isMobileDevice) {
|
|
1033
|
+
mobileInput.focus();
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
toolbar.addEventListener('touchstart', handleToolbarButton, { passive: false });
|
|
1038
|
+
|
|
1039
|
+
toolbar.addEventListener('click', function (e) {
|
|
1040
|
+
// On non-touch devices, handle normally
|
|
1041
|
+
if (isMobileDevice) return; // already handled by touchstart
|
|
1042
|
+
handleToolbarButton(e);
|
|
939
1043
|
});
|
|
940
1044
|
|
|
941
1045
|
// ── Image Paste Handling ─────────────────────────────────────────────────────
|
|
@@ -1062,6 +1166,162 @@
|
|
|
1062
1166
|
hideImageToast();
|
|
1063
1167
|
});
|
|
1064
1168
|
|
|
1169
|
+
// ── Image Upload Button (mobile) ──────────────────────────────────────────
|
|
1170
|
+
|
|
1171
|
+
uploadImageBtn.addEventListener('click', function (e) {
|
|
1172
|
+
e.preventDefault();
|
|
1173
|
+
if (!activeSessionId) return;
|
|
1174
|
+
imageFileInput.click();
|
|
1175
|
+
if (isMobileDevice) {
|
|
1176
|
+
mobileInput.focus();
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
imageFileInput.addEventListener('change', function () {
|
|
1181
|
+
var file = imageFileInput.files[0];
|
|
1182
|
+
if (file && file.type.indexOf('image/') === 0) {
|
|
1183
|
+
uploadImage(file, file.type);
|
|
1184
|
+
}
|
|
1185
|
+
imageFileInput.value = '';
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
// ── Mobile Input Proxy ──────────────────────────────────────────────────────
|
|
1189
|
+
|
|
1190
|
+
(function () {
|
|
1191
|
+
if (!isMobileDevice) return;
|
|
1192
|
+
|
|
1193
|
+
var lastInputValue = '';
|
|
1194
|
+
|
|
1195
|
+
function focusMobileInput() {
|
|
1196
|
+
if (document.activeElement !== mobileInput) {
|
|
1197
|
+
mobileInput.focus();
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Tap on terminal area focuses the hidden input (opens keyboard)
|
|
1202
|
+
terminalContainer.addEventListener('touchend', function (e) {
|
|
1203
|
+
// Don't interfere with scrollbar drag or selection
|
|
1204
|
+
if (scrollbarDragging) return;
|
|
1205
|
+
if (e.target === terminalScrollbarThumb || e.target === terminalScrollbar) return;
|
|
1206
|
+
focusMobileInput();
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
// When xterm would receive focus, redirect to hidden input
|
|
1210
|
+
terminalContainer.addEventListener('focus', function () {
|
|
1211
|
+
focusMobileInput();
|
|
1212
|
+
}, true);
|
|
1213
|
+
|
|
1214
|
+
// Compute the common prefix length between two strings
|
|
1215
|
+
function commonPrefixLength(a, b) {
|
|
1216
|
+
var len = 0;
|
|
1217
|
+
while (len < a.length && len < b.length && a[len] === b[len]) {
|
|
1218
|
+
len++;
|
|
1219
|
+
}
|
|
1220
|
+
return len;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Count Unicode code points in a string (handles surrogate pairs)
|
|
1224
|
+
function codepointCount(str) {
|
|
1225
|
+
var count = 0;
|
|
1226
|
+
for (var i = 0; i < str.length; i++) {
|
|
1227
|
+
count++;
|
|
1228
|
+
if (str.charCodeAt(i) >= 0xD800 && str.charCodeAt(i) <= 0xDBFF) {
|
|
1229
|
+
i++; // skip low surrogate
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return count;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Send the diff between lastInputValue and currentValue to the terminal.
|
|
1236
|
+
// Handles autocorrect expansions, deletions, and same-length replacements.
|
|
1237
|
+
function sendInputDiff(currentValue) {
|
|
1238
|
+
if (currentValue === lastInputValue) return;
|
|
1239
|
+
|
|
1240
|
+
var commonLen = commonPrefixLength(lastInputValue, currentValue);
|
|
1241
|
+
var deletedSlice = lastInputValue.slice(commonLen);
|
|
1242
|
+
var charsToDelete = codepointCount(deletedSlice);
|
|
1243
|
+
var newChars = currentValue.slice(commonLen);
|
|
1244
|
+
|
|
1245
|
+
for (var i = 0; i < charsToDelete; i++) {
|
|
1246
|
+
ws.send('\x7f'); // backspace
|
|
1247
|
+
}
|
|
1248
|
+
if (newChars) {
|
|
1249
|
+
ws.send(newChars);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Handle text input with autocorrect
|
|
1254
|
+
var clearTimer = null;
|
|
1255
|
+
mobileInput.addEventListener('input', function () {
|
|
1256
|
+
// Reset the auto-clear timer to prevent unbounded growth
|
|
1257
|
+
if (clearTimer) clearTimeout(clearTimer);
|
|
1258
|
+
clearTimer = setTimeout(function () {
|
|
1259
|
+
mobileInput.value = '';
|
|
1260
|
+
lastInputValue = '';
|
|
1261
|
+
}, 5000);
|
|
1262
|
+
|
|
1263
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
1264
|
+
|
|
1265
|
+
var currentValue = mobileInput.value;
|
|
1266
|
+
sendInputDiff(currentValue);
|
|
1267
|
+
lastInputValue = currentValue;
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
// Handle special keys (Enter, Backspace, Escape, arrows, Tab)
|
|
1271
|
+
mobileInput.addEventListener('keydown', function (e) {
|
|
1272
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
1273
|
+
|
|
1274
|
+
var handled = true;
|
|
1275
|
+
|
|
1276
|
+
switch (e.key) {
|
|
1277
|
+
case 'Enter':
|
|
1278
|
+
ws.send('\r');
|
|
1279
|
+
mobileInput.value = '';
|
|
1280
|
+
lastInputValue = '';
|
|
1281
|
+
break;
|
|
1282
|
+
case 'Backspace':
|
|
1283
|
+
if (mobileInput.value.length === 0) {
|
|
1284
|
+
// Input is empty, send backspace directly
|
|
1285
|
+
ws.send('\x7f');
|
|
1286
|
+
}
|
|
1287
|
+
// Otherwise, let the input event handle it via diff
|
|
1288
|
+
handled = false;
|
|
1289
|
+
break;
|
|
1290
|
+
case 'Escape':
|
|
1291
|
+
ws.send('\x1b');
|
|
1292
|
+
mobileInput.value = '';
|
|
1293
|
+
lastInputValue = '';
|
|
1294
|
+
break;
|
|
1295
|
+
case 'Tab':
|
|
1296
|
+
e.preventDefault();
|
|
1297
|
+
ws.send('\t');
|
|
1298
|
+
break;
|
|
1299
|
+
case 'ArrowUp':
|
|
1300
|
+
e.preventDefault();
|
|
1301
|
+
ws.send('\x1b[A');
|
|
1302
|
+
break;
|
|
1303
|
+
case 'ArrowDown':
|
|
1304
|
+
e.preventDefault();
|
|
1305
|
+
ws.send('\x1b[B');
|
|
1306
|
+
break;
|
|
1307
|
+
case 'ArrowLeft':
|
|
1308
|
+
// Let input handle cursor movement for autocorrect
|
|
1309
|
+
handled = false;
|
|
1310
|
+
break;
|
|
1311
|
+
case 'ArrowRight':
|
|
1312
|
+
handled = false;
|
|
1313
|
+
break;
|
|
1314
|
+
default:
|
|
1315
|
+
handled = false;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
if (handled) {
|
|
1319
|
+
e.preventDefault();
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
})();
|
|
1324
|
+
|
|
1065
1325
|
// ── Keyboard-Aware Viewport ─────────────────────────────────────────────────
|
|
1066
1326
|
|
|
1067
1327
|
(function () {
|
|
@@ -1073,8 +1333,10 @@
|
|
|
1073
1333
|
var keyboardHeight = window.innerHeight - vv.height;
|
|
1074
1334
|
if (keyboardHeight > 50) {
|
|
1075
1335
|
mainApp.style.height = vv.height + 'px';
|
|
1336
|
+
mobileHeader.style.display = 'none';
|
|
1076
1337
|
} else {
|
|
1077
1338
|
mainApp.style.height = '';
|
|
1339
|
+
mobileHeader.style.display = '';
|
|
1078
1340
|
}
|
|
1079
1341
|
if (fitAddon) {
|
|
1080
1342
|
fitAddon.fit();
|
package/public/index.html
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
|
6
6
|
<title>Claude Remote CLI</title>
|
|
7
|
+
<link rel="manifest" href="/manifest.json" />
|
|
8
|
+
<meta name="mobile-web-app-capable" content="yes" />
|
|
9
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
10
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
11
|
+
<meta name="theme-color" content="#1a1a1a" />
|
|
7
12
|
<link rel="stylesheet" href="/vendor/xterm.css" />
|
|
8
13
|
<link rel="stylesheet" href="/style.css" />
|
|
9
14
|
</head>
|
|
@@ -59,20 +64,23 @@
|
|
|
59
64
|
<button id="menu-btn" class="icon-btn" aria-label="Open sessions menu">☰</button>
|
|
60
65
|
<span id="session-title" class="mobile-title">No session</span>
|
|
61
66
|
</div>
|
|
67
|
+
<input type="text" id="mobile-input" autocomplete="on" autocorrect="on" autocapitalize="sentences" spellcheck="true" aria-label="Terminal input" />
|
|
62
68
|
<div id="terminal-container"></div>
|
|
69
|
+
<div id="terminal-scrollbar"><div id="terminal-scrollbar-thumb"></div></div>
|
|
63
70
|
<div id="no-session-msg">No active session. Create or select a session to begin.</div>
|
|
64
71
|
|
|
65
72
|
<!-- Touch Toolbar -->
|
|
66
73
|
<div id="toolbar">
|
|
67
74
|
<div class="toolbar-grid">
|
|
68
75
|
<button class="tb-btn" data-key="	" aria-label="Tab">Tab</button>
|
|
69
|
-
<button class="tb-btn tb-arrow" data-key="[A" aria-label="Up arrow">↑</button>
|
|
70
76
|
<button class="tb-btn" data-key="[Z" aria-label="Shift+Tab">⇧Tab</button>
|
|
77
|
+
<button class="tb-btn tb-arrow" data-key="[A" aria-label="Up arrow">↑</button>
|
|
71
78
|
<button class="tb-btn" data-key="" aria-label="Escape">Esc</button>
|
|
79
|
+
<button class="tb-btn" id="upload-image-btn" aria-label="Upload image">📷</button>
|
|
80
|
+
<button class="tb-btn" data-key="" aria-label="Ctrl+C">^C</button>
|
|
72
81
|
<button class="tb-btn tb-arrow" data-key="[D" aria-label="Left arrow">←</button>
|
|
73
82
|
<button class="tb-btn tb-arrow" data-key="[B" aria-label="Down arrow">↓</button>
|
|
74
83
|
<button class="tb-btn tb-arrow" data-key="[C" aria-label="Right arrow">→</button>
|
|
75
|
-
<button class="tb-btn" data-key="" aria-label="Ctrl+C">^C</button>
|
|
76
84
|
<button class="tb-btn tb-enter" data-key="
" aria-label="Enter">⏎</button>
|
|
77
85
|
</div>
|
|
78
86
|
</div>
|
|
@@ -89,6 +97,9 @@
|
|
|
89
97
|
</div>
|
|
90
98
|
</div>
|
|
91
99
|
|
|
100
|
+
<!-- Hidden file input for mobile image upload -->
|
|
101
|
+
<input type="file" id="image-file-input" accept="image/*" hidden />
|
|
102
|
+
|
|
92
103
|
<!-- Image Paste Toast -->
|
|
93
104
|
<div id="image-toast" hidden>
|
|
94
105
|
<div id="image-toast-content">
|
package/public/style.css
CHANGED
|
@@ -428,6 +428,40 @@ html, body {
|
|
|
428
428
|
pointer-events: none;
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
+
#mobile-input {
|
|
432
|
+
position: absolute;
|
|
433
|
+
top: -9999px;
|
|
434
|
+
left: -9999px;
|
|
435
|
+
width: 1px;
|
|
436
|
+
height: 1px;
|
|
437
|
+
opacity: 0;
|
|
438
|
+
font-size: 16px; /* prevents iOS zoom on focus */
|
|
439
|
+
z-index: -1;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/* ===== Terminal Scrollbar ===== */
|
|
443
|
+
#terminal-scrollbar {
|
|
444
|
+
display: none;
|
|
445
|
+
position: absolute;
|
|
446
|
+
top: 0;
|
|
447
|
+
right: 2px;
|
|
448
|
+
bottom: 0;
|
|
449
|
+
width: 6px;
|
|
450
|
+
z-index: 10;
|
|
451
|
+
pointer-events: auto;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
#terminal-scrollbar-thumb {
|
|
455
|
+
position: absolute;
|
|
456
|
+
right: 0;
|
|
457
|
+
width: 6px;
|
|
458
|
+
min-height: 20px;
|
|
459
|
+
background: var(--border);
|
|
460
|
+
border-radius: 3px;
|
|
461
|
+
opacity: 0.7;
|
|
462
|
+
touch-action: none;
|
|
463
|
+
}
|
|
464
|
+
|
|
431
465
|
/* ===== Touch Toolbar ===== */
|
|
432
466
|
#toolbar {
|
|
433
467
|
display: none;
|
|
@@ -440,10 +474,10 @@ html, body {
|
|
|
440
474
|
|
|
441
475
|
.toolbar-grid {
|
|
442
476
|
display: grid;
|
|
443
|
-
grid-template-columns:
|
|
444
|
-
grid-template-rows: auto auto
|
|
477
|
+
grid-template-columns: repeat(5, 1fr);
|
|
478
|
+
grid-template-rows: auto auto;
|
|
445
479
|
gap: 4px;
|
|
446
|
-
max-width:
|
|
480
|
+
max-width: 500px;
|
|
447
481
|
margin: 0 auto;
|
|
448
482
|
}
|
|
449
483
|
|
|
@@ -473,12 +507,6 @@ html, body {
|
|
|
473
507
|
font-size: 1.1rem;
|
|
474
508
|
}
|
|
475
509
|
|
|
476
|
-
/* Enter button: bottom-right cell only */
|
|
477
|
-
.tb-btn.tb-enter {
|
|
478
|
-
grid-column: 4;
|
|
479
|
-
grid-row: 3;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
510
|
/* ===== Dialog ===== */
|
|
483
511
|
dialog#new-session-dialog {
|
|
484
512
|
background: var(--surface);
|
|
@@ -999,18 +1027,22 @@ dialog#delete-worktree-dialog h2 {
|
|
|
999
1027
|
}
|
|
1000
1028
|
|
|
1001
1029
|
.tb-btn {
|
|
1002
|
-
padding:
|
|
1003
|
-
font-size: 0.8rem;
|
|
1004
|
-
min-height:
|
|
1030
|
+
padding: 14px 2px;
|
|
1031
|
+
font-size: min(0.8rem, 3.5vw);
|
|
1032
|
+
min-height: 44px;
|
|
1005
1033
|
}
|
|
1006
1034
|
|
|
1007
1035
|
.tb-btn.tb-arrow {
|
|
1008
|
-
font-size: 1.
|
|
1036
|
+
font-size: min(1.1rem, 4.5vw);
|
|
1009
1037
|
}
|
|
1010
1038
|
|
|
1011
1039
|
#toolbar {
|
|
1012
1040
|
display: block;
|
|
1013
1041
|
}
|
|
1042
|
+
|
|
1043
|
+
#terminal-scrollbar {
|
|
1044
|
+
display: block;
|
|
1045
|
+
}
|
|
1014
1046
|
}
|
|
1015
1047
|
|
|
1016
1048
|
/* ===== Scrollbar ===== */
|