@dmsdc-ai/aigentry-telepty 0.1.59 → 0.1.61
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/cli.js +2 -8
- package/daemon.js +1 -2
- package/package.json +1 -1
- package/tui.js +190 -1
package/cli.js
CHANGED
|
@@ -780,14 +780,8 @@ async function main() {
|
|
|
780
780
|
|
|
781
781
|
daemonWs.on('open', () => {
|
|
782
782
|
wsReady = true;
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
// Force CLI redraw by triggering SIGWINCH (resize +1/-1)
|
|
786
|
-
const origCols = child.cols || process.stdout.columns || 80;
|
|
787
|
-
const origRows = child.rows || process.stdout.rows || 30;
|
|
788
|
-
child.resize(origCols - 1, origRows);
|
|
789
|
-
setTimeout(() => child.resize(origCols, origRows), 150);
|
|
790
|
-
}
|
|
783
|
+
// No resize trick on reconnect — it causes visible flickering across all
|
|
784
|
+
// terminals when the daemon restarts and multiple sessions reconnect at once.
|
|
791
785
|
reconnectAttempts = 0;
|
|
792
786
|
});
|
|
793
787
|
|
package/daemon.js
CHANGED
|
@@ -1389,9 +1389,8 @@ wss.on('connection', (ws, req) => {
|
|
|
1389
1389
|
};
|
|
1390
1390
|
sessions[sessionId] = autoSession;
|
|
1391
1391
|
console.log(`[WS] Auto-registered wrapped session ${sessionId} on reconnect`);
|
|
1392
|
-
//
|
|
1392
|
+
// Set tab title via kitty (no \x0c redraw — it causes flickering on multi-session reconnect)
|
|
1393
1393
|
setTimeout(() => {
|
|
1394
|
-
sendViaKitty(sessionId, '\x0c');
|
|
1395
1394
|
const sock = findKittySocket();
|
|
1396
1395
|
const wid = findKittyWindowId(sock, sessionId);
|
|
1397
1396
|
if (sock && wid) {
|
package/package.json
CHANGED
package/tui.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const blessed = require('blessed');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { execSync, execFileSync } = require('child_process');
|
|
4
8
|
const { getConfig } = require('./auth');
|
|
5
9
|
|
|
6
10
|
const PORT = process.env.PORT || 3848;
|
|
7
11
|
const DAEMON_URL = `http://localhost:${PORT}`;
|
|
8
12
|
const POLL_INTERVAL = 2000;
|
|
9
13
|
const STALE_THRESHOLD = 120; // seconds idle before "stale"
|
|
14
|
+
const PROJECTS_DIR = path.join(os.homedir(), 'projects');
|
|
15
|
+
const DEFAULT_CLI = 'claude --dangerously-skip-permissions';
|
|
10
16
|
|
|
11
17
|
class TuiDashboard {
|
|
12
18
|
constructor() {
|
|
@@ -73,6 +79,175 @@ class TuiDashboard {
|
|
|
73
79
|
}
|
|
74
80
|
}
|
|
75
81
|
|
|
82
|
+
// ── Session lifecycle (P1) ──────────────────────────────────
|
|
83
|
+
|
|
84
|
+
findKittySocket() {
|
|
85
|
+
try {
|
|
86
|
+
const files = fs.readdirSync('/tmp').filter(f => f.startsWith('kitty-sock'));
|
|
87
|
+
return files.length > 0 ? '/tmp/' + files[0] : null;
|
|
88
|
+
} catch { return null; }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
findKittyWindowId(sock, sessionId) {
|
|
92
|
+
try {
|
|
93
|
+
const raw = execSync(`kitty @ --to unix:${sock} ls`, { timeout: 3000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
94
|
+
const data = JSON.parse(raw);
|
|
95
|
+
for (const osw of data) {
|
|
96
|
+
for (const tab of osw.tabs) {
|
|
97
|
+
for (const w of tab.windows) {
|
|
98
|
+
for (const p of (w.foreground_processes || [])) {
|
|
99
|
+
const cmd = (p.cmdline || []).join(' ');
|
|
100
|
+
if (cmd.includes('--id ' + sessionId) || cmd.includes('--id=' + sessionId)) {
|
|
101
|
+
return w.id;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Also match by window title
|
|
105
|
+
if (w.title && w.title.includes(sessionId)) return w.id;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch {}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
discoverProjects() {
|
|
114
|
+
try {
|
|
115
|
+
return fs.readdirSync(PROJECTS_DIR, { withFileTypes: true })
|
|
116
|
+
.filter(d => d.isDirectory() && d.name.startsWith('aigentry-') &&
|
|
117
|
+
fs.existsSync(path.join(PROJECTS_DIR, d.name, '.git')))
|
|
118
|
+
.map(d => ({ name: d.name, cwd: path.join(PROJECTS_DIR, d.name) }));
|
|
119
|
+
} catch { return []; }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async startSession(project) {
|
|
123
|
+
const sock = this.findKittySocket();
|
|
124
|
+
if (!sock) return this.setStatus('{red-fg}No kitty socket found{/}');
|
|
125
|
+
|
|
126
|
+
const cli = DEFAULT_CLI;
|
|
127
|
+
const cliParts = cli.split(' ');
|
|
128
|
+
let teleptyPath, cliPath;
|
|
129
|
+
try { teleptyPath = execSync('which telepty', { encoding: 'utf8' }).trim(); }
|
|
130
|
+
catch { teleptyPath = path.join(__dirname, 'cli.js'); }
|
|
131
|
+
try { cliPath = execSync(`which ${cliParts[0]}`, { encoding: 'utf8' }).trim(); }
|
|
132
|
+
catch { cliPath = cliParts[0]; }
|
|
133
|
+
const cliArgs = cliParts.slice(1).join(' ');
|
|
134
|
+
const nodePath = process.execPath;
|
|
135
|
+
|
|
136
|
+
const sessionId = `${project.name}-${cliParts[0]}`;
|
|
137
|
+
const shellCmd = `unset TELEPTY_SESSION_ID; ${nodePath} ${teleptyPath} allow --id ${sessionId} ${cliPath}${cliArgs ? ' ' + cliArgs : ''}`;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
execFileSync('kitty', ['@', '--to', `unix:${sock}`,
|
|
141
|
+
'launch', '--type=tab', '--tab-title', project.name, '--cwd', project.cwd,
|
|
142
|
+
'--env', 'TELEPTY_SESSION_ID=',
|
|
143
|
+
'--env', `PATH=${process.env.PATH}`,
|
|
144
|
+
'/bin/zsh', '-c', shellCmd
|
|
145
|
+
], { timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
146
|
+
this.setStatus(`{green-fg}Started ${sessionId}{/}`);
|
|
147
|
+
setTimeout(() => this.fetchSessions(), 2000);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
this.setStatus(`{red-fg}Start failed: ${e.message}{/}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async killSession(id) {
|
|
154
|
+
try {
|
|
155
|
+
// Send Ctrl+C to kitty window first
|
|
156
|
+
const sock = this.findKittySocket();
|
|
157
|
+
if (sock) {
|
|
158
|
+
const wid = this.findKittyWindowId(sock, id);
|
|
159
|
+
if (wid) {
|
|
160
|
+
try {
|
|
161
|
+
execSync(`kitty @ --to unix:${sock} send-text --match id:${wid} $'\\x03'`, {
|
|
162
|
+
timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
|
|
163
|
+
});
|
|
164
|
+
} catch {}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Deregister from daemon
|
|
168
|
+
await this.apiFetch(`/api/sessions/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
169
|
+
this.setStatus(`{green-fg}Killed ${id}{/}`);
|
|
170
|
+
setTimeout(() => this.fetchSessions(), 1000);
|
|
171
|
+
} catch (e) {
|
|
172
|
+
this.setStatus(`{red-fg}Kill error: ${e.message}{/}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async purgeStale() {
|
|
177
|
+
const staleSessions = this.sessions.filter(s => {
|
|
178
|
+
const idle = s.idleSeconds;
|
|
179
|
+
return (idle !== null && idle > STALE_THRESHOLD) || s.active_clients === 0;
|
|
180
|
+
});
|
|
181
|
+
if (staleSessions.length === 0) {
|
|
182
|
+
return this.setStatus('{yellow-fg}No stale sessions to purge{/}');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const sock = this.findKittySocket();
|
|
186
|
+
let purged = 0;
|
|
187
|
+
for (const s of staleSessions) {
|
|
188
|
+
try {
|
|
189
|
+
// Send Ctrl+C to the session's kitty window
|
|
190
|
+
if (sock) {
|
|
191
|
+
const wid = this.findKittyWindowId(sock, s.id);
|
|
192
|
+
if (wid) {
|
|
193
|
+
execSync(`kitty @ --to unix:${sock} send-text --match id:${wid} $'\\x03'`, {
|
|
194
|
+
timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Deregister from daemon
|
|
199
|
+
await this.apiFetch(`/api/sessions/${encodeURIComponent(s.id)}`, { method: 'DELETE' });
|
|
200
|
+
purged++;
|
|
201
|
+
} catch {}
|
|
202
|
+
}
|
|
203
|
+
this.setStatus(`{green-fg}Purged ${purged}/${staleSessions.length} stale sessions{/}`);
|
|
204
|
+
setTimeout(() => this.fetchSessions(), 1000);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
showProjectPicker() {
|
|
208
|
+
const projects = this.discoverProjects();
|
|
209
|
+
// Filter out projects that already have active sessions
|
|
210
|
+
const activeIds = new Set(this.sessions.map(s => s.id));
|
|
211
|
+
const available = projects.filter(p => {
|
|
212
|
+
const expectedId = `${p.name}-claude`;
|
|
213
|
+
return !activeIds.has(expectedId);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (available.length === 0) {
|
|
217
|
+
return this.setStatus('{yellow-fg}All projects already have active sessions{/}');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const picker = blessed.list({
|
|
221
|
+
parent: this.screen,
|
|
222
|
+
top: 'center', left: 'center',
|
|
223
|
+
width: '50%', height: Math.min(available.length + 2, 20),
|
|
224
|
+
border: { type: 'line' },
|
|
225
|
+
label: ' Start Session — Select Project ',
|
|
226
|
+
tags: true,
|
|
227
|
+
keys: true, vi: true, mouse: true,
|
|
228
|
+
items: available.map(p => ` ${p.name}`),
|
|
229
|
+
style: {
|
|
230
|
+
border: { fg: 'green' },
|
|
231
|
+
selected: { bg: 'green', fg: 'black', bold: true },
|
|
232
|
+
item: { fg: 'white' }
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
picker.focus();
|
|
237
|
+
picker.on('select', (item, index) => {
|
|
238
|
+
picker.destroy();
|
|
239
|
+
this.sessionList.focus();
|
|
240
|
+
this.screen.render();
|
|
241
|
+
this.startSession(available[index]);
|
|
242
|
+
});
|
|
243
|
+
picker.key(['escape', 'q'], () => {
|
|
244
|
+
picker.destroy();
|
|
245
|
+
this.sessionList.focus();
|
|
246
|
+
this.screen.render();
|
|
247
|
+
});
|
|
248
|
+
this.screen.render();
|
|
249
|
+
}
|
|
250
|
+
|
|
76
251
|
// ── Event Bus ────────────────────────────────────────────────
|
|
77
252
|
|
|
78
253
|
connectBus() {
|
|
@@ -166,7 +341,7 @@ class TuiDashboard {
|
|
|
166
341
|
bottom: 1, left: 0, width: '100%', height: 1,
|
|
167
342
|
tags: true,
|
|
168
343
|
style: { fg: 'white', bg: 'gray' },
|
|
169
|
-
content: ' {bold}i{/}:Inject {bold}b{/}:Broadcast {bold}r{/}:Refresh {bold}q{/}:Quit'
|
|
344
|
+
content: ' {bold}s{/}:Start {bold}k{/}:Kill {bold}i{/}:Inject {bold}b{/}:Broadcast {bold}p{/}:Purge {bold}r{/}:Refresh {bold}q{/}:Quit'
|
|
170
345
|
});
|
|
171
346
|
|
|
172
347
|
// Status bar
|
|
@@ -204,6 +379,20 @@ class TuiDashboard {
|
|
|
204
379
|
this.setStatus('{green-fg}Refreshed{/}');
|
|
205
380
|
});
|
|
206
381
|
|
|
382
|
+
this.screen.key(['s'], () => {
|
|
383
|
+
this.showProjectPicker();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
this.screen.key(['k'], () => {
|
|
387
|
+
const session = this.sessions[this.selectedIndex];
|
|
388
|
+
if (!session) return this.setStatus('{red-fg}No session selected{/}');
|
|
389
|
+
this.killSession(session.id);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
this.screen.key(['p'], () => {
|
|
393
|
+
this.purgeStale();
|
|
394
|
+
});
|
|
395
|
+
|
|
207
396
|
this.sessionList.focus();
|
|
208
397
|
this.screen.render();
|
|
209
398
|
}
|