@@ -17,7 +17,7 @@ import os from 'node:os';
1717import { resolveUserEnvironment } from '@agor/core/config' ;
1818import { type Database , WorktreeRepository } from '@agor/core/db' ;
1919import type { Application } from '@agor/core/feathers' ;
20- import type { UserID , WorktreeID } from '@agor/core/types' ;
20+ import type { AuthenticatedParams , UserID , WorktreeID } from '@agor/core/types' ;
2121import type { IPty } from '@homebridge/node-pty-prebuilt-multiarch' ;
2222import * as pty from '@homebridge/node-pty-prebuilt-multiarch' ;
2323
@@ -39,6 +39,7 @@ interface CreateTerminalData {
3939 cols ?: number ;
4040 userId ?: UserID ; // User context for env resolution
4141 worktreeId ?: WorktreeID ; // Worktree context for tmux integration
42+ useTmux ?: boolean ; // Optional flag to disable tmux even when available
4243}
4344
4445interface ResizeTerminalData {
@@ -94,6 +95,66 @@ function findTmuxWindow(sessionName: string, windowName: string): number | null
9495 }
9596}
9697
98+ /**
99+ * Ensure tmux session is configured to pass OSC hyperlinks and other rich output.
100+ */
101+ function configureTmuxSession ( sessionName : string ) : void {
102+ try {
103+ execSync ( `tmux set-option -t "${ sessionName } " -g allow-passthrough on` , { stdio : 'pipe' } ) ;
104+ } catch ( error ) {
105+ const message = error instanceof Error ? error . message : String ( error ) ;
106+ console . warn (
107+ `⚠️ Failed to enable tmux allow-passthrough for session ${ sessionName } : ${ message } `
108+ ) ;
109+ }
110+
111+ try {
112+ execSync ( `tmux set-option -t "${ sessionName } " -g default-terminal 'tmux-256color'` , {
113+ stdio : 'pipe' ,
114+ } ) ;
115+ } catch ( error ) {
116+ const message = error instanceof Error ? error . message : String ( error ) ;
117+ console . warn ( `⚠️ Failed to set tmux default-terminal for ${ sessionName } : ${ message } ` ) ;
118+ }
119+
120+ try {
121+ execSync ( `tmux set-option -ga terminal-features 'xterm*:allow-passthrough'` , { stdio : 'pipe' } ) ;
122+ } catch ( error ) {
123+ const message = error instanceof Error ? error . message : String ( error ) ;
124+ console . warn ( `⚠️ Failed to advertise tmux allow-passthrough feature: ${ message } ` ) ;
125+ }
126+
127+ try {
128+ execSync ( `tmux set-option -ga terminal-features 'tmux-256color:hyperlinks,RGB,extkeys'` , {
129+ stdio : 'pipe' ,
130+ } ) ;
131+ } catch ( error ) {
132+ const message = error instanceof Error ? error . message : String ( error ) ;
133+ console . warn ( `⚠️ Failed to advertise tmux tmux-256color features: ${ message } ` ) ;
134+ }
135+
136+ try {
137+ execSync ( `tmux set-option -ga terminal-features 'xterm*:hyperlinks'` , { stdio : 'pipe' } ) ;
138+ } catch ( error ) {
139+ const message = error instanceof Error ? error . message : String ( error ) ;
140+ console . warn ( `⚠️ Failed to enable tmux hyperlink feature: ${ message } ` ) ;
141+ }
142+
143+ try {
144+ execSync ( `tmux set-option -as terminal-overrides ',*:allow-passthrough'` , { stdio : 'pipe' } ) ;
145+ } catch ( error ) {
146+ const message = error instanceof Error ? error . message : String ( error ) ;
147+ console . warn ( `⚠️ Failed to configure tmux allow-passthrough override: ${ message } ` ) ;
148+ }
149+
150+ try {
151+ execSync ( `tmux set-option -as terminal-overrides ',*:hyperlinks'` , { stdio : 'pipe' } ) ;
152+ } catch ( error ) {
153+ const message = error instanceof Error ? error . message : String ( error ) ;
154+ console . warn ( `⚠️ Failed to configure tmux hyperlink override: ${ message } ` ) ;
155+ }
156+ }
157+
97158/**
98159 * Terminals service - manages PTY sessions
99160 */
@@ -102,13 +163,17 @@ export class TerminalsService {
102163 private app : Application ;
103164 private db : Database ;
104165 private hasTmux : boolean ;
166+ private forceTmux : boolean ;
105167
106168 constructor ( app : Application , db : Database ) {
107169 this . app = app ;
108170 this . db = db ;
109171 this . hasTmux = isTmuxAvailable ( ) ;
172+ this . forceTmux = process . env . AGOR_DISABLE_TMUX !== 'true' ;
110173
111- if ( this . hasTmux ) {
174+ if ( ! this . forceTmux ) {
175+ console . log ( 'ℹ️ tmux disabled via AGOR_DISABLE_TMUX=true - using direct PTY sessions' ) ;
176+ } else if ( this . hasTmux ) {
112177 console . log ( '\x1b[36m✅ tmux detected\x1b[0m - persistent terminal sessions enabled' ) ;
113178 } else {
114179 console . log ( 'ℹ️ tmux not found - using ephemeral terminal sessions' ) ;
@@ -118,14 +183,24 @@ export class TerminalsService {
118183 /**
119184 * Create a new terminal session
120185 */
121- async create ( data : CreateTerminalData ) : Promise < {
186+ async create (
187+ data : CreateTerminalData ,
188+ params ?: AuthenticatedParams
189+ ) : Promise < {
122190 terminalId : string ;
123191 cwd : string ;
124192 tmuxSession ?: string ;
125193 tmuxReused ?: boolean ;
126194 worktreeName ?: string ;
127195 } > {
128196 const terminalId = `term-${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . substr ( 2 , 9 ) } ` ;
197+ const authenticatedUserId = params ?. user ?. user_id as UserID | undefined ;
198+ const resolvedUserId = data . userId ?? authenticatedUserId ;
199+ const userSessionSuffix = ( ( ) => {
200+ if ( ! resolvedUserId ) return 'shared' ;
201+ const sanitized = resolvedUserId . replace ( / [ ^ a - z A - Z 0 - 9 _ - ] / g, '' ) ;
202+ return sanitized . length > 0 ? sanitized : 'user' ;
203+ } ) ( ) ;
129204
130205 // Resolve worktree context if provided
131206 let worktree = null ;
@@ -142,20 +217,25 @@ export class TerminalsService {
142217 }
143218
144219 // Determine shell and tmux configuration
145- let shell : string ;
146- let shellArgs : string [ ] = [ ] ;
220+ const defaultShell = data . shell || ( os . platform ( ) === 'win32' ? 'powershell.exe' : 'bash' ) ;
221+ const defaultShellArgs : string [ ] = [ ] ;
222+ let shell : string = defaultShell ;
223+ let shellArgs : string [ ] = [ ...defaultShellArgs ] ;
147224 let tmuxSession : string | undefined ;
148225 let tmuxReused = false ;
149226
150- if ( this . hasTmux && worktree ) {
227+ const tmuxRequested = data . useTmux !== false ;
228+
229+ if ( this . hasTmux && this . forceTmux && worktree && tmuxRequested ) {
151230 // Use single shared tmux session with one window per worktree
152- tmuxSession = ' agor' ;
231+ tmuxSession = ` agor- ${ userSessionSuffix } ` ;
153232 const sessionExists = tmuxSessionExists ( tmuxSession ) ;
154233 const windowName = worktreeName || 'unnamed' ;
155234
156235 shell = 'tmux' ;
157236
158237 if ( sessionExists ) {
238+ configureTmuxSession ( tmuxSession ) ;
159239 // Session exists - check if this worktree has a window
160240 const windowIndex = findTmuxWindow ( tmuxSession , windowName ) ;
161241
@@ -199,44 +279,135 @@ export class TerminalsService {
199279 'set-option' ,
200280 '-t' ,
201281 tmuxSession ,
282+ 'default-terminal' ,
283+ 'tmux-256color' ,
284+ ';' ,
285+ 'set-option' ,
286+ '-t' ,
287+ tmuxSession ,
202288 'status-style' ,
203289 'bg=#2e9a92,fg=#000000' ,
290+ ';' ,
291+ 'set-option' ,
292+ '-t' ,
293+ tmuxSession ,
294+ 'allow-passthrough' ,
295+ 'on' ,
296+ ';' ,
297+ 'set-option' ,
298+ '-ga' ,
299+ 'terminal-features' ,
300+ 'xterm*:hyperlinks' ,
301+ ';' ,
302+ 'set-option' ,
303+ '-ga' ,
304+ 'terminal-features' ,
305+ 'tmux-256color:hyperlinks,RGB,extkeys' ,
306+ ';' ,
307+ 'set-option' ,
308+ '-ga' ,
309+ 'terminal-features' ,
310+ 'xterm*:allow-passthrough' ,
311+ ';' ,
312+ 'set-option' ,
313+ '-as' ,
314+ 'terminal-overrides' ,
315+ ',*:allow-passthrough' ,
316+ ';' ,
317+ 'set-option' ,
318+ '-as' ,
319+ 'terminal-overrides' ,
320+ ',*:hyperlinks' ,
204321 ] ;
205322 tmuxReused = false ;
206323 console . log (
207324 `\x1b[36m🚀 Creating tmux session:\x1b[0m ${ tmuxSession } with window (${ windowName } ) + teal theme`
208325 ) ;
209326 }
210327 } else {
328+ if ( this . hasTmux && this . forceTmux && ! tmuxRequested ) {
329+ console . log ( 'ℹ️ tmux disabled for this terminal session (user toggle)' ) ;
330+ }
211331 // Fallback to regular shell
212- shell = data . shell || ( os . platform ( ) === 'win32' ? 'powershell.exe' : 'bash' ) ;
332+ shell = defaultShell ;
333+ shellArgs = [ ...defaultShellArgs ] ;
213334 }
214335
215336 // Resolve environment with user env vars if userId provided
216- let env : Record < string , string > = process . env as Record < string , string > ;
217- if ( data . userId ) {
218- env = await resolveUserEnvironment ( data . userId , this . db ) ;
337+ let env : Record < string , string > = { ... ( process . env as Record < string , string > ) } ;
338+ if ( resolvedUserId ) {
339+ const userEnv = await resolveUserEnvironment ( resolvedUserId , this . db ) ;
219340 console . log (
220- `🔐 Loaded ${ Object . keys ( env ) . length } env vars for user ${ data . userId . substring ( 0 , 8 ) } `
341+ `🔐 Loaded ${ Object . keys ( userEnv ) . length } env vars for user ${ resolvedUserId . substring ( 0 , 8 ) } `
221342 ) ;
343+ env = { ...env , ...userEnv } ;
344+ }
345+ env = { ...env } ;
346+
347+ // Ensure terminal capabilities advertised to downstream processes
348+ if ( ! env . TERM ) {
349+ env . TERM = 'xterm-256color' ;
350+ }
351+ if ( ! env . COLORTERM ) {
352+ env . COLORTERM = 'truecolor' ;
353+ }
354+ if ( ! env . ENABLE_HYPERLINKS ) {
355+ env . ENABLE_HYPERLINKS = '1' ;
356+ }
357+ if ( ! env . RICH_FORCE_COLOR ) {
358+ env . RICH_FORCE_COLOR = '1' ;
359+ }
360+ if ( ! env . RICH_FORCE_HYPERLINK ) {
361+ env . RICH_FORCE_HYPERLINK = '1' ;
362+ }
363+ if ( ! env . LANG ) {
364+ env . LANG = 'C.UTF-8' ;
365+ }
366+ if ( ! env . LC_ALL ) {
367+ env . LC_ALL = env . LANG ;
368+ }
369+ if ( ! env . LC_CTYPE ) {
370+ env . LC_CTYPE = env . LANG ;
222371 }
223372
224373 // Spawn PTY process
225- const ptyProcess = pty . spawn ( shell , shellArgs , {
226- name : 'xterm-color' ,
227- cols : data . cols || 80 ,
228- rows : data . rows || 30 ,
229- cwd,
230- env, // Use resolved environment
231- } ) ;
374+ let ptyProcess : IPty ;
375+ try {
376+ ptyProcess = pty . spawn ( shell , shellArgs , {
377+ name : 'xterm-256color' ,
378+ cols : data . cols || 80 ,
379+ rows : data . rows || 30 ,
380+ cwd,
381+ env, // Use resolved environment
382+ } ) ;
383+ } catch ( error ) {
384+ if ( shell === 'tmux' ) {
385+ const message = error instanceof Error ? error . message : String ( error ) ;
386+ console . warn ( `⚠️ Failed to launch tmux session, falling back to direct shell: ${ message } ` ) ;
387+ this . hasTmux = false ;
388+ tmuxSession = undefined ;
389+ tmuxReused = false ;
390+ shell = defaultShell ;
391+ shellArgs = [ ...defaultShellArgs ] ;
392+ ptyProcess = pty . spawn ( shell , shellArgs , {
393+ name : 'xterm-256color' ,
394+ cols : data . cols || 80 ,
395+ rows : data . rows || 30 ,
396+ cwd,
397+ env,
398+ } ) ;
399+ } else {
400+ throw error ;
401+ }
402+ }
232403
233404 // Store session
234405 this . sessions . set ( terminalId , {
235406 terminalId,
236407 pty : ptyProcess ,
237408 shell,
238409 cwd,
239- userId : data . userId ,
410+ userId : resolvedUserId ,
240411 worktreeId : data . worktreeId ,
241412 tmuxSession,
242413 createdAt : new Date ( ) ,
0 commit comments