@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.
Files changed (4) hide show
  1. package/cli.js +2 -8
  2. package/daemon.js +1 -2
  3. package/package.json +1 -1
  4. 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
- if (reconnectAttempts > 0) {
784
- // Silent reconnect no console output to avoid breaking TUI rendering
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
- // Trigger CLI redraw + set tab title via kitty after short delay
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.1.59",
3
+ "version": "0.1.61",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",
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
  }