aigo 1.0.1 → 1.1.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/Makefile +4 -0
- package/README.md +85 -5
- package/lib/server.js +26 -0
- package/package.json +1 -1
- package/public/index.html +130 -0
- package/public/terminal.js +79 -3
- package/specs/history-view-button.md +64 -0
package/Makefile
ADDED
package/README.md
CHANGED
|
@@ -29,7 +29,8 @@ aigo --tunnel none claude
|
|
|
29
29
|
- **tmux** installed and in PATH
|
|
30
30
|
- **Claude Code CLI** installed (`claude` command) - for Claude Code support
|
|
31
31
|
- **Cursor Agent CLI** installed (`agent` command) - for Cursor support
|
|
32
|
-
- **ngrok** (
|
|
32
|
+
- **ngrok** (default for remote access) - sign up at [ngrok.com](https://ngrok.com)
|
|
33
|
+
- **cloudflared** (alternative to ngrok) - free, no signup required
|
|
33
34
|
|
|
34
35
|
### Installing tmux
|
|
35
36
|
|
|
@@ -56,6 +57,23 @@ brew install ngrok
|
|
|
56
57
|
ngrok config add-authtoken YOUR_TOKEN
|
|
57
58
|
```
|
|
58
59
|
|
|
60
|
+
### Installing cloudflared
|
|
61
|
+
|
|
62
|
+
cloudflared is a free alternative to ngrok that doesn't require signup:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# macOS
|
|
66
|
+
brew install cloudflared
|
|
67
|
+
|
|
68
|
+
# Ubuntu/Debian
|
|
69
|
+
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb
|
|
70
|
+
sudo dpkg -i cloudflared.deb
|
|
71
|
+
|
|
72
|
+
# Or download from https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
No authentication required for quick tunnels (trycloudflare.com).
|
|
76
|
+
|
|
59
77
|
### Installing Cursor Agent CLI
|
|
60
78
|
|
|
61
79
|
```bash
|
|
@@ -144,13 +162,38 @@ export const config = {
|
|
|
144
162
|
};
|
|
145
163
|
```
|
|
146
164
|
|
|
165
|
+
## Tunnel Options
|
|
166
|
+
|
|
167
|
+
aigo supports two tunnel providers for remote access:
|
|
168
|
+
|
|
169
|
+
| Feature | ngrok | cloudflared |
|
|
170
|
+
|---------|-------|-------------|
|
|
171
|
+
| Signup required | Yes | No |
|
|
172
|
+
| Free tier limits | 1 tunnel, rate limits | Unlimited quick tunnels |
|
|
173
|
+
| Custom domains | Paid plans | Requires Cloudflare account |
|
|
174
|
+
| URL format | `*.ngrok-free.app` | `*.trycloudflare.com` |
|
|
175
|
+
| Authentication | Required (authtoken) | Not required |
|
|
176
|
+
|
|
177
|
+
**Recommendation:** Use cloudflared for quick testing (no signup needed). Use ngrok if you need stable URLs or have an existing account.
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# Use cloudflared (no signup required)
|
|
181
|
+
aigo --tunnel cloudflared claude
|
|
182
|
+
|
|
183
|
+
# Use ngrok (default, requires signup)
|
|
184
|
+
aigo claude
|
|
185
|
+
|
|
186
|
+
# Local only (no tunnel)
|
|
187
|
+
aigo --tunnel none claude
|
|
188
|
+
```
|
|
189
|
+
|
|
147
190
|
## How It Works
|
|
148
191
|
|
|
149
192
|
1. **aigo** creates a tmux session and runs `claude` or `agent` inside it
|
|
150
193
|
2. A local web server starts, serving an xterm.js terminal
|
|
151
194
|
3. The server attaches to the tmux session via node-pty
|
|
152
195
|
4. Your browser connects via WebSocket for real-time terminal streaming
|
|
153
|
-
5. ngrok creates a public HTTPS
|
|
196
|
+
5. A tunnel (ngrok or cloudflared) creates a public HTTPS URL for remote access
|
|
154
197
|
|
|
155
198
|
```
|
|
156
199
|
┌────────────────────────────────────────┐
|
|
@@ -194,10 +237,10 @@ aigo uses multiple layers of security:
|
|
|
194
237
|
- Password uses unambiguous characters (no 0/O, 1/l/I)
|
|
195
238
|
- Lock screen requires re-entering password after inactivity (when enabled)
|
|
196
239
|
- Session auto-terminates and kills tmux after extended inactivity (when enabled)
|
|
197
|
-
- HTTPS via
|
|
240
|
+
- HTTPS via tunnel (encrypted in transit)
|
|
198
241
|
|
|
199
242
|
**Recommendations:**
|
|
200
|
-
- Use HTTPS (ngrok
|
|
243
|
+
- Use HTTPS (both ngrok and cloudflared provide this automatically)
|
|
201
244
|
- Don't share your access URL or password publicly
|
|
202
245
|
- Use strong custom passwords for sensitive sessions
|
|
203
246
|
- Enable timeouts (`-T 15 -E 30`) for shared or sensitive environments
|
|
@@ -231,6 +274,7 @@ When accessing aigo from a mobile device (phone or tablet), control buttons appe
|
|
|
231
274
|
|
|
232
275
|
| Button | Action | Description |
|
|
233
276
|
|--------|--------|-------------|
|
|
277
|
+
| **History** | View Buffer | Opens a fullscreen modal showing the terminal scrollback buffer (useful when TUI scrolling doesn't work on mobile) |
|
|
234
278
|
| **Mode** | `Shift+Tab` | Toggle between chat/edit modes in Claude Code or switch modes in Cursor Agent |
|
|
235
279
|
| **Enter** | `Enter` | Send Enter key to confirm prompts or submit input |
|
|
236
280
|
| **Stop** | `Ctrl+C` | Interrupt the current operation (stop running commands or cancel AI responses) |
|
|
@@ -238,6 +282,17 @@ When accessing aigo from a mobile device (phone or tablet), control buttons appe
|
|
|
238
282
|
|
|
239
283
|
These buttons are useful because mobile keyboards don't easily support modifier key combinations like Ctrl+C or Shift+Tab.
|
|
240
284
|
|
|
285
|
+
### History Button
|
|
286
|
+
|
|
287
|
+
The **History** button is particularly useful on mobile devices where terminal scrolling often doesn't work properly with TUI applications like Claude Code or Cursor Agent. When pressed:
|
|
288
|
+
|
|
289
|
+
1. Opens a fullscreen modal overlay
|
|
290
|
+
2. Captures the entire terminal scrollback buffer from tmux
|
|
291
|
+
3. Displays the content in a scrollable view with ANSI codes stripped for readability
|
|
292
|
+
4. Press **Close** or the **ESC** key to return to the terminal
|
|
293
|
+
|
|
294
|
+
This provides an alternative way to review previous AI responses and conversation history without relying on terminal scrolling.
|
|
295
|
+
|
|
241
296
|
The buttons are automatically shown based on:
|
|
242
297
|
- Touch screen capability
|
|
243
298
|
- Mobile user agent detection
|
|
@@ -259,7 +314,7 @@ tmux attach -t aigo-server
|
|
|
259
314
|
pm2 start "aigo claude" --name aigo-session
|
|
260
315
|
```
|
|
261
316
|
|
|
262
|
-
When the aigo server stays running, your
|
|
317
|
+
When the aigo server stays running, your tunnel URL remains stable and you can return to the same URL anytime. Note: cloudflared quick tunnel URLs change on restart, while ngrok URLs persist within a session.
|
|
263
318
|
|
|
264
319
|
## Auto Port Selection
|
|
265
320
|
|
|
@@ -427,6 +482,21 @@ kill <pid>
|
|
|
427
482
|
pkill -f "ngrok http"
|
|
428
483
|
```
|
|
429
484
|
|
|
485
|
+
### cloudflared connection issues
|
|
486
|
+
|
|
487
|
+
If cloudflared fails to connect:
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
# Check if cloudflared is installed
|
|
491
|
+
which cloudflared
|
|
492
|
+
|
|
493
|
+
# Test a quick tunnel manually
|
|
494
|
+
cloudflared tunnel --url http://localhost:3000
|
|
495
|
+
|
|
496
|
+
# Check for firewall issues (verbose output)
|
|
497
|
+
cloudflared tunnel --url http://localhost:3000 --loglevel debug
|
|
498
|
+
```
|
|
499
|
+
|
|
430
500
|
### WebSocket connection fails
|
|
431
501
|
|
|
432
502
|
- Check that the server is running
|
|
@@ -448,6 +518,16 @@ node bin/aigo.js cursor
|
|
|
448
518
|
npx nodemon bin/aigo.js claude
|
|
449
519
|
```
|
|
450
520
|
|
|
521
|
+
### Publishing
|
|
522
|
+
|
|
523
|
+
Bump the minor version and publish to npm:
|
|
524
|
+
|
|
525
|
+
```bash
|
|
526
|
+
make publish-minor
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
This runs `npm version minor` (updates `package.json` and creates a git commit/tag) then `npm publish`.
|
|
530
|
+
|
|
451
531
|
## License
|
|
452
532
|
|
|
453
533
|
MIT
|
package/lib/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createServer } from 'http';
|
|
|
3
3
|
import { WebSocketServer } from 'ws';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
6
7
|
import pty from 'node-pty';
|
|
7
8
|
import { getTmuxPath, killSession } from './tmux.js';
|
|
8
9
|
|
|
@@ -234,6 +235,31 @@ export function startServer({ port, token, password, session, lockTimeout, exitT
|
|
|
234
235
|
}, 500);
|
|
235
236
|
return;
|
|
236
237
|
}
|
|
238
|
+
|
|
239
|
+
// Handle history capture request
|
|
240
|
+
if (parsed.type === 'capture_history') {
|
|
241
|
+
updateActivity();
|
|
242
|
+
try {
|
|
243
|
+
const tmuxBin = getTmuxPath() || 'tmux';
|
|
244
|
+
// Capture entire scrollback buffer from tmux session
|
|
245
|
+
// -p: print to stdout, -S -: start from beginning, -E -: end at bottom
|
|
246
|
+
const historyData = execSync(
|
|
247
|
+
`${tmuxBin} capture-pane -t "${session}:0" -p -S - -E -`,
|
|
248
|
+
{ encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 } // 10MB max
|
|
249
|
+
);
|
|
250
|
+
ws.send(JSON.stringify({
|
|
251
|
+
type: 'history_data',
|
|
252
|
+
data: historyData
|
|
253
|
+
}));
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.error('Failed to capture history:', err.message);
|
|
256
|
+
ws.send(JSON.stringify({
|
|
257
|
+
type: 'history_error',
|
|
258
|
+
error: 'Failed to capture terminal history'
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
237
263
|
} catch {
|
|
238
264
|
// Not JSON - treat as terminal input
|
|
239
265
|
}
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -478,6 +478,120 @@
|
|
|
478
478
|
background: #ff5555;
|
|
479
479
|
color: white;
|
|
480
480
|
}
|
|
481
|
+
|
|
482
|
+
/* History button */
|
|
483
|
+
#mobile-history-btn {
|
|
484
|
+
color: #00ffff;
|
|
485
|
+
border-color: #00ffff;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
#mobile-history-btn:hover,
|
|
489
|
+
#mobile-history-btn:active {
|
|
490
|
+
background: #00ffff;
|
|
491
|
+
color: var(--bg-primary);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/* History modal overlay */
|
|
495
|
+
#history-overlay {
|
|
496
|
+
display: none;
|
|
497
|
+
position: fixed;
|
|
498
|
+
top: 0;
|
|
499
|
+
left: 0;
|
|
500
|
+
right: 0;
|
|
501
|
+
bottom: 0;
|
|
502
|
+
background: var(--bg-primary);
|
|
503
|
+
z-index: 300;
|
|
504
|
+
flex-direction: column;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
#history-overlay.visible {
|
|
508
|
+
display: flex;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.history-header {
|
|
512
|
+
display: flex;
|
|
513
|
+
align-items: center;
|
|
514
|
+
justify-content: space-between;
|
|
515
|
+
padding: 12px 16px;
|
|
516
|
+
background: var(--bg-secondary);
|
|
517
|
+
border-bottom: 1px solid var(--border);
|
|
518
|
+
flex-shrink: 0;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.history-title {
|
|
522
|
+
color: #00ffff;
|
|
523
|
+
font-size: 16px;
|
|
524
|
+
font-weight: 600;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.history-close-btn {
|
|
528
|
+
padding: 8px 16px;
|
|
529
|
+
background: transparent;
|
|
530
|
+
color: var(--text-secondary);
|
|
531
|
+
border: 1px solid var(--border);
|
|
532
|
+
border-radius: 4px;
|
|
533
|
+
font-family: inherit;
|
|
534
|
+
font-size: 13px;
|
|
535
|
+
cursor: pointer;
|
|
536
|
+
transition: all 0.2s;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.history-close-btn:hover {
|
|
540
|
+
color: var(--text-primary);
|
|
541
|
+
border-color: var(--text-primary);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.history-content {
|
|
545
|
+
flex: 1;
|
|
546
|
+
overflow-y: auto;
|
|
547
|
+
padding: 16px;
|
|
548
|
+
-webkit-overflow-scrolling: touch;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.history-text {
|
|
552
|
+
white-space: pre-wrap;
|
|
553
|
+
word-wrap: break-word;
|
|
554
|
+
font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
|
|
555
|
+
font-size: 13px;
|
|
556
|
+
line-height: 1.5;
|
|
557
|
+
color: var(--text-primary);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.history-loading {
|
|
561
|
+
display: flex;
|
|
562
|
+
justify-content: center;
|
|
563
|
+
align-items: center;
|
|
564
|
+
height: 100%;
|
|
565
|
+
color: var(--text-secondary);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.history-loading .spinner {
|
|
569
|
+
width: 24px;
|
|
570
|
+
height: 24px;
|
|
571
|
+
border: 2px solid var(--border);
|
|
572
|
+
border-top-color: #00ffff;
|
|
573
|
+
border-radius: 50%;
|
|
574
|
+
animation: spin 1s linear infinite;
|
|
575
|
+
margin-right: 12px;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/* Custom scrollbar for history */
|
|
579
|
+
.history-content::-webkit-scrollbar {
|
|
580
|
+
width: 8px;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.history-content::-webkit-scrollbar-track {
|
|
584
|
+
background: var(--bg-primary);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.history-content::-webkit-scrollbar-thumb {
|
|
588
|
+
background: var(--border);
|
|
589
|
+
border-radius: 4px;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.history-content::-webkit-scrollbar-thumb:hover {
|
|
593
|
+
background: #2a2a35;
|
|
594
|
+
}
|
|
481
595
|
</style>
|
|
482
596
|
</head>
|
|
483
597
|
<body>
|
|
@@ -528,11 +642,27 @@
|
|
|
528
642
|
|
|
529
643
|
<!-- Mobile control buttons -->
|
|
530
644
|
<div id="mobile-controls">
|
|
645
|
+
<button id="mobile-history-btn" class="mobile-btn" title="View terminal history">History</button>
|
|
531
646
|
<button id="mobile-mode-toggle" class="mobile-btn" title="Switch mode (Shift+Tab)">Mode</button>
|
|
532
647
|
<button id="mobile-enter-btn" class="mobile-btn" title="Send Enter key">Enter</button>
|
|
533
648
|
<button id="mobile-stop-btn" class="mobile-btn" title="Stop (Ctrl+C)">Stop</button>
|
|
534
649
|
<button id="mobile-exit-btn" class="mobile-btn" title="Exit session">Exit</button>
|
|
535
650
|
</div>
|
|
651
|
+
|
|
652
|
+
<!-- History modal overlay -->
|
|
653
|
+
<div id="history-overlay">
|
|
654
|
+
<div class="history-header">
|
|
655
|
+
<div class="history-title">Terminal History</div>
|
|
656
|
+
<button id="history-close-btn" class="history-close-btn">Close</button>
|
|
657
|
+
</div>
|
|
658
|
+
<div class="history-content">
|
|
659
|
+
<div class="history-loading">
|
|
660
|
+
<div class="spinner"></div>
|
|
661
|
+
<span>Loading history...</span>
|
|
662
|
+
</div>
|
|
663
|
+
<pre class="history-text" style="display: none;"></pre>
|
|
664
|
+
</div>
|
|
665
|
+
</div>
|
|
536
666
|
</div>
|
|
537
667
|
|
|
538
668
|
<!-- xterm.js scripts from CDN (non-scoped package names for UMD globals) -->
|
package/public/terminal.js
CHANGED
|
@@ -52,6 +52,12 @@
|
|
|
52
52
|
const mobileEnterBtn = document.getElementById('mobile-enter-btn');
|
|
53
53
|
const mobileStopBtn = document.getElementById('mobile-stop-btn');
|
|
54
54
|
const mobileExitBtn = document.getElementById('mobile-exit-btn');
|
|
55
|
+
const mobileHistoryBtn = document.getElementById('mobile-history-btn');
|
|
56
|
+
const historyOverlay = document.getElementById('history-overlay');
|
|
57
|
+
const historyCloseBtn = document.getElementById('history-close-btn');
|
|
58
|
+
const historyContent = historyOverlay.querySelector('.history-content');
|
|
59
|
+
const historyLoading = historyOverlay.querySelector('.history-loading');
|
|
60
|
+
const historyText = historyOverlay.querySelector('.history-text');
|
|
55
61
|
|
|
56
62
|
// Terminal setup
|
|
57
63
|
const terminal = new Terminal({
|
|
@@ -453,10 +459,70 @@
|
|
|
453
459
|
}
|
|
454
460
|
});
|
|
455
461
|
|
|
456
|
-
//
|
|
462
|
+
// Strip ANSI escape codes from text for clean display
|
|
463
|
+
function stripAnsiCodes(text) {
|
|
464
|
+
// Remove ANSI escape sequences (colors, cursor movement, etc.)
|
|
465
|
+
return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
|
|
466
|
+
.replace(/\x1b\][^\x07]*\x07/g, '') // OSC sequences
|
|
467
|
+
.replace(/\x1b[PX^_].*?\x1b\\/g, '') // DCS, SOS, PM, APC sequences
|
|
468
|
+
.replace(/\x1b./g, ''); // Other escape sequences
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Show history modal
|
|
472
|
+
function showHistoryModal() {
|
|
473
|
+
historyOverlay.classList.add('visible');
|
|
474
|
+
historyLoading.style.display = 'flex';
|
|
475
|
+
historyText.style.display = 'none';
|
|
476
|
+
historyText.textContent = '';
|
|
477
|
+
|
|
478
|
+
// Request history from server
|
|
479
|
+
if (ws && ws.readyState === WebSocket.OPEN && authenticated) {
|
|
480
|
+
ws.send(JSON.stringify({ type: 'capture_history' }));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Hide history modal
|
|
485
|
+
function hideHistoryModal() {
|
|
486
|
+
historyOverlay.classList.remove('visible');
|
|
487
|
+
terminal.focus();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Display history data in the modal
|
|
491
|
+
function displayHistoryData(data) {
|
|
492
|
+
historyLoading.style.display = 'none';
|
|
493
|
+
historyText.style.display = 'block';
|
|
494
|
+
// Strip ANSI codes for clean, readable text
|
|
495
|
+
historyText.textContent = stripAnsiCodes(data);
|
|
496
|
+
// Scroll to bottom to show most recent content
|
|
497
|
+
historyContent.scrollTop = historyContent.scrollHeight;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Display history error
|
|
501
|
+
function displayHistoryError(error) {
|
|
502
|
+
historyLoading.style.display = 'none';
|
|
503
|
+
historyText.style.display = 'block';
|
|
504
|
+
historyText.textContent = `Error: ${error}`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Mobile history button handler
|
|
508
|
+
mobileHistoryBtn.addEventListener('click', () => {
|
|
509
|
+
if (ws && ws.readyState === WebSocket.OPEN && authenticated && !isLocked) {
|
|
510
|
+
mobileButtonFeedback(mobileHistoryBtn);
|
|
511
|
+
showHistoryModal();
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// History close button handler
|
|
516
|
+
historyCloseBtn.addEventListener('click', hideHistoryModal);
|
|
517
|
+
|
|
518
|
+
// ESC to close exit confirm or history
|
|
457
519
|
document.addEventListener('keydown', (e) => {
|
|
458
|
-
if (e.key === 'Escape'
|
|
459
|
-
|
|
520
|
+
if (e.key === 'Escape') {
|
|
521
|
+
if (historyOverlay.classList.contains('visible')) {
|
|
522
|
+
hideHistoryModal();
|
|
523
|
+
} else if (exitOverlay.classList.contains('visible')) {
|
|
524
|
+
hideExitConfirm();
|
|
525
|
+
}
|
|
460
526
|
}
|
|
461
527
|
});
|
|
462
528
|
|
|
@@ -606,6 +672,16 @@
|
|
|
606
672
|
showCleanupComplete();
|
|
607
673
|
return;
|
|
608
674
|
}
|
|
675
|
+
|
|
676
|
+
if (msg.type === 'history_data') {
|
|
677
|
+
displayHistoryData(msg.data);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (msg.type === 'history_error') {
|
|
682
|
+
displayHistoryError(msg.error);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
609
685
|
} catch {
|
|
610
686
|
// Not JSON, treat as terminal data
|
|
611
687
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# History View Button for Mobile
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
Terminal scrolling doesn't work well on mobile devices with TUI applications like Claude Code and Cursor Agent, making it difficult to view previous messages.
|
|
5
|
+
|
|
6
|
+
## Solution
|
|
7
|
+
Add a "History" button that opens a modal overlay displaying the terminal scrollback buffer captured directly from tmux using `tmux capture-pane`.
|
|
8
|
+
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+
```mermaid
|
|
12
|
+
sequenceDiagram
|
|
13
|
+
participant User
|
|
14
|
+
participant Frontend as WebClient
|
|
15
|
+
participant Server as Node Server
|
|
16
|
+
participant Tmux
|
|
17
|
+
|
|
18
|
+
User->>Frontend: Clicks History button
|
|
19
|
+
Frontend->>Server: WebSocket message type: "capture_history"
|
|
20
|
+
Server->>Tmux: capture-pane -t session:0 -p -S - -E -
|
|
21
|
+
Tmux-->>Server: Scrollback buffer text
|
|
22
|
+
Server-->>Frontend: WebSocket message type: "history_data"
|
|
23
|
+
Frontend->>User: Shows modal with scrollable history
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Implementation
|
|
27
|
+
|
|
28
|
+
### 1. Backend - Add history capture endpoint ([lib/server.js](lib/server.js))
|
|
29
|
+
|
|
30
|
+
Add a new WebSocket message handler for `capture_history` that runs:
|
|
31
|
+
```bash
|
|
32
|
+
tmux capture-pane -t <session>:0 -p -S - -E -
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This captures the entire scrollback buffer (tested and confirmed working - returns all terminal output including Claude/Cursor responses).
|
|
36
|
+
|
|
37
|
+
### 2. Frontend - Add History button ([public/index.html](public/index.html))
|
|
38
|
+
|
|
39
|
+
Add a new mobile button styled like the existing controls:
|
|
40
|
+
- Button label: "History"
|
|
41
|
+
- Color: Use a distinct color (cyan `#00ffff`)
|
|
42
|
+
- Position: Add to the existing mobile controls row
|
|
43
|
+
|
|
44
|
+
### 3. Frontend - Add modal overlay ([public/index.html](public/index.html))
|
|
45
|
+
|
|
46
|
+
Create a fullscreen modal that:
|
|
47
|
+
- Has a dark background matching the terminal theme
|
|
48
|
+
- Contains a scrollable text area with monospace font
|
|
49
|
+
- Has a "Close" button at the top
|
|
50
|
+
- Strips ANSI escape codes for clean display
|
|
51
|
+
|
|
52
|
+
### 4. Frontend - Handle history messages ([public/terminal.js](public/terminal.js))
|
|
53
|
+
|
|
54
|
+
- Send `capture_history` request when History button is clicked
|
|
55
|
+
- Receive `history_data` response and display in modal
|
|
56
|
+
- Strip ANSI escape codes for readable plain text display
|
|
57
|
+
|
|
58
|
+
## Key Files Modified
|
|
59
|
+
- [lib/server.js](lib/server.js) - Add tmux capture-pane handler
|
|
60
|
+
- [public/index.html](public/index.html) - Add History button and modal HTML/CSS
|
|
61
|
+
- [public/terminal.js](public/terminal.js) - Add click handler and modal logic
|
|
62
|
+
- [README.md](README.md) - Document the new History feature
|
|
63
|
+
|
|
64
|
+
## Status: Completed
|