From 106f8ce12531f3328dd428e8abb56db9464be36a Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 20 Mar 2026 11:53:21 -0500 Subject: [PATCH 01/10] backend: align session token env and extend agents list JSON --- backend/models/terminal-session.js | 5 ++ backend/routes/sessions.js | 1 + backend/services/auto-start.js | 1 + .../tests/terminal-session-none-env.test.mjs | 53 +++++++++++++++++++ backend/tools/agents/agents.mjs | 28 +++++++++- 5 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 backend/tests/terminal-session-none-env.test.mjs diff --git a/backend/models/terminal-session.js b/backend/models/terminal-session.js index fe280e8..5b60f58 100644 --- a/backend/models/terminal-session.js +++ b/backend/models/terminal-session.js @@ -68,6 +68,7 @@ export class TerminalSession { this.workspace_service_port = Number.isFinite(Number(options.workspace_service_port)) ? Math.floor(Number(options.workspace_service_port)) : null; + this.session_token = typeof options.session_token === 'string' ? options.session_token : ''; // Fork metadata this.is_fork = options.is_fork === true; this.forked_from_session_id = options.forked_from_session_id || null; @@ -224,6 +225,10 @@ export class TerminalSession { TERMSTATION_USER: this.created_by, SESSIONS_BASE_URL: config.SESSIONS_BASE_URL }; + if (String(this.isolation_mode || 'none') === 'none') { + if (this.session_token) env.SESSION_TOK = this.session_token; + if (config.SESSIONS_API_BASE_URL) env.SESSIONS_API_BASE_URL = config.SESSIONS_API_BASE_URL; + } // For host sessions (non-container) without a per-session bootstrap, make // backend-managed bootstrap tools available by appending backend/bootstrap/bin diff --git a/backend/routes/sessions.js b/backend/routes/sessions.js index 3546eb7..38ab47d 100644 --- a/backend/routes/sessions.js +++ b/backend/routes/sessions.js @@ -919,6 +919,7 @@ router.post('/', async (req, res) => { workspace_service_port: workspaceServicePort, // Persist effective parameters (provided + defaults) on the session template_parameters: resolvedTemplateParameters, + session_token: sessionUnifiedToken || '', session_id: initialSessionId, // Optional alias (safe slug only) ...(computedAlias ? { session_alias: computedAlias } : {}) diff --git a/backend/services/auto-start.js b/backend/services/auto-start.js index 409935b..8e417f2 100644 --- a/backend/services/auto-start.js +++ b/backend/services/auto-start.js @@ -227,6 +227,7 @@ export async function runAutoStartTemplates({ logger } = {}) { workspace_service_enabled_for_session: workspaceServiceEnabledForSession, workspace_service_port: workspaceServicePort, template_parameters: resolveWithDefaults(tplForRun, paramValues), + session_token: sessionUnifiedToken || '', // Pass alias to SessionManager so it registers the mapping ...(computedAlias ? { session_alias: computedAlias } : {}) }; diff --git a/backend/tests/terminal-session-none-env.test.mjs b/backend/tests/terminal-session-none-env.test.mjs new file mode 100644 index 0000000..92dc783 --- /dev/null +++ b/backend/tests/terminal-session-none-env.test.mjs @@ -0,0 +1,53 @@ +import { beforeEach, afterEach, test, expect, vi } from 'vitest'; +import { createTestConfig, cleanupTestConfig } from './helpers/test-utils.mjs'; + +let configDir; +let TerminalSession; +let config; +let spawnMock; + +beforeEach(async () => { + configDir = createTestConfig(); + process.env.TERMSTATION_CONFIG_DIR = configDir; + vi.resetModules(); + + spawnMock = vi.fn(() => ({ + pid: 1234, + onData: () => {}, + onExit: () => {}, + write: () => {}, + resize: () => {}, + kill: () => {} + })); + + vi.doMock('node-pty', () => ({ + spawn: spawnMock + })); + + ({ TerminalSession } = await import('../models/terminal-session.js')); + ({ config } = await import('../config-loader.js')); +}); + +afterEach(() => { + cleanupTestConfig(configDir); + delete process.env.TERMSTATION_CONFIG_DIR; + vi.resetModules(); +}); + +test('createPtyProcess exports SESSION_TOK and SESSIONS_API_BASE_URL for none isolation', async () => { + const session = new TerminalSession({ + session_id: 'host-none-session', + isolation_mode: 'none', + session_token: 'tok-123', + working_directory: process.cwd(), + save_session_history: false + }); + + await session.createPtyProcess(); + + expect(spawnMock).toHaveBeenCalledTimes(1); + const [, , spawnOptions] = spawnMock.mock.calls[0]; + expect(spawnOptions.env.SESSION_TOK).toBe('tok-123'); + expect(spawnOptions.env.SESSIONS_API_BASE_URL).toBe(config.SESSIONS_API_BASE_URL); + expect(spawnOptions.env.SESSIONS_BASE_URL).toBe(config.SESSIONS_BASE_URL); +}); diff --git a/backend/tools/agents/agents.mjs b/backend/tools/agents/agents.mjs index 1eac1f1..f67ba74 100644 --- a/backend/tools/agents/agents.mjs +++ b/backend/tools/agents/agents.mjs @@ -7,6 +7,23 @@ import { ApiClient } from './lib/apiClient.mjs'; import { CliError } from './lib/errors.mjs'; import { getMessageArgOrStdin, prefixMessage } from './lib/io.mjs'; +function toListJsonEntry(session) { + if (!session || typeof session !== 'object') return null; + const sessionId = session.session_id || session.id || ''; + if (!sessionId) return null; + return { + session_id: sessionId, + created_at: Object.prototype.hasOwnProperty.call(session, 'created_at') ? session.created_at : null, + workspace: Object.prototype.hasOwnProperty.call(session, 'workspace') ? session.workspace : null, + title: Object.prototype.hasOwnProperty.call(session, 'title') ? session.title : '', + dynamic_title: Object.prototype.hasOwnProperty.call(session, 'dynamic_title') ? session.dynamic_title : '', + template_id: Object.prototype.hasOwnProperty.call(session, 'template_id') ? session.template_id : null, + template_name: Object.prototype.hasOwnProperty.call(session, 'template_name') ? session.template_name : null, + last_output_at: Object.prototype.hasOwnProperty.call(session, 'last_output_at') ? session.last_output_at : null, + output_active: Object.prototype.hasOwnProperty.call(session, 'output_active') ? session.output_active : null, + }; +} + async function main() { const program = new Command(); program @@ -17,8 +34,9 @@ async function main() { program .command('list') + .option('--json', 'Output session details as JSON') .description('List active peer agent sessions (excluding your own)') - .action(async () => { + .action(async (cmdOpts) => { const opts = program.opts(); const cfg = loadConfig(); const api = new ApiClient(cfg.SESSIONS_API_BASE_URL, { debug: opts.debug || cfg.DEBUG, token: cfg.SESSION_TOK }); @@ -38,6 +56,14 @@ async function main() { return ta - tb; }); + if (cmdOpts && cmdOpts.json) { + const rows = filtered + .map(toListJsonEntry) + .filter(Boolean); + process.stdout.write(JSON.stringify(rows, null, 2) + '\n'); + return; + } + for (const s of filtered) { const id = s.session_id || s.id || ''; if (id) process.stdout.write(id + '\n'); From 25fc9e098067ddb77e7f822a05b77d8074cbca35 Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 20 Mar 2026 11:55:02 -0500 Subject: [PATCH 02/10] changelog: document backend env and agents list JSON updates --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3448199..a548b40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,13 @@ ## [Unreleased] -_No unreleased changes._ +### Added + +- Backend tools: add `agents.js list --json` support with expanded session metadata (`session_id`, `created_at`, `workspace`, `title`, `dynamic_title`, `template_id`, `template_name`, `last_output_at`, `output_active`). ([#31](https://github.com/kcosr/termstation/pull/31)) + +### Fixed + +- Backend: ensure `SESSION_TOK` and `SESSIONS_API_BASE_URL` are injected for `isolation_mode=none` sessions, including route-created and auto-started sessions. ([#31](https://github.com/kcosr/termstation/pull/31)) ## [0.0.8] - 2026-03-13 From 6a7d95aa8c01ec6d8d874cf0103c14c0489e30d7 Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 20 Mar 2026 17:48:17 -0500 Subject: [PATCH 03/10] agents: add --cwd for create --- backend/tools/agents/agents.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/tools/agents/agents.mjs b/backend/tools/agents/agents.mjs index f67ba74..c1046f7 100644 --- a/backend/tools/agents/agents.mjs +++ b/backend/tools/agents/agents.mjs @@ -118,6 +118,7 @@ async function main() { .argument('[message]', 'Optional prompt (or read from stdin)') .option('--post-create-delay ', 'Seconds to wait after successful creation (default: 10)') .option('--description ', 'Short description to append to the session title') + .option('--cwd ', 'Override working directory for the created session') .description('Create a new peer agent session') .action(async (agent, messageArg, cmd) => { const opts = program.opts(); @@ -156,6 +157,8 @@ async function main() { code_review: true, as_user: cfg.TERMSTATION_USER, }; + const cwd = (typeof cmd?.cwd === 'string') ? cmd.cwd.trim() : ''; + if (cwd) payload.working_directory = cwd; const resp = await api.createSession(payload); const id = resp?.session_id || resp?.id; From dc9e2ba0de65f7f893700c0c0aa08ab36fd6033c Mon Sep 17 00:00:00 2001 From: Kevin Date: Sat, 28 Mar 2026 17:31:25 -0500 Subject: [PATCH 04/10] Frontend trace/debug improvements and dedicated window fullscreen fixes --- CHANGELOG.md | 4 + desktop/main.js | 18 +++ frontend/public/css/style.css | 49 +++++++ frontend/public/index.html | 24 +++ frontend/public/js/core/app.js | 56 +++++-- .../js/modules/settings/settings-manager.js | 105 ++++++++++++++ .../public/js/modules/terminal/manager.js | 64 ++++++-- .../modules/terminal/session-tabs-manager.js | 11 +- .../public/js/modules/terminal/session.js | 109 ++++++++++---- .../websocket/handlers/stdout-handler.js | 5 + .../public/js/services/websocket.service.js | 137 ++++++++++++++++++ frontend/public/js/utils/ws-session-trace.js | 105 ++++++++++++++ 12 files changed, 635 insertions(+), 52 deletions(-) create mode 100644 frontend/public/js/utils/ws-session-trace.js diff --git a/CHANGELOG.md b/CHANGELOG.md index a548b40..31c8bc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,14 @@ ### Added - Backend tools: add `agents.js list --json` support with expanded session metadata (`session_id`, `created_at`, `workspace`, `title`, `dynamic_title`, `template_id`, `template_name`, `last_output_at`, `output_active`). ([#31](https://github.com/kcosr/termstation/pull/31)) +- Frontend: add a Developer WebSocket Session Trace viewer (in-memory ring buffer) with refresh, clear, and copy actions for client-side session/websocket event debugging. ([#31](https://github.com/kcosr/termstation/pull/31)) ### Fixed - Backend: ensure `SESSION_TOK` and `SESSIONS_API_BASE_URL` are injected for `isolation_mode=none` sessions, including route-created and auto-started sessions. ([#31](https://github.com/kcosr/termstation/pull/31)) +- Frontend desktop: keep keyboard session-tab navigation local to the current window instead of focusing a dedicated session window. ([#31](https://github.com/kcosr/termstation/pull/31)) +- Frontend desktop/macOS: remove dedicated-window header/sidebar toggle inset in fullscreen by syncing renderer fullscreen state reliably for child windows. ([#31](https://github.com/kcosr/termstation/pull/31)) +- Frontend mobile: reduce Android dictation-triggered horizontal panning by constraining terminal container overflow and adjusting xterm helper textarea placement on coarse-pointer devices. ([#31](https://github.com/kcosr/termstation/pull/31)) ## [0.0.8] - 2026-03-13 diff --git a/desktop/main.js b/desktop/main.js index 86bfeeb..f751fea 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -1333,6 +1333,17 @@ function createSessionWindow({ sessionId, title }) { titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default' }); + // Reflect fullscreen state into the child renderer DOM so CSS can + // remove macOS traffic-light inset when the window is fullscreen. + const updateChildFullscreenClass = () => { + try { + if (!child || child.isDestroyed() || !child.webContents) return; + const isFs = child.isFullScreen(); + const js = `try { document.documentElement.classList.toggle('is-fullscreen', ${isFs}); } catch (e) {}`; + child.webContents.executeJavaScript(js).catch(() => {}); + } catch (_) { /* ignore */ } + }; + if (title && typeof title === 'string') { try { child.setTitle(`TermStation — ${title}`); } catch (_) {} } else { @@ -1480,6 +1491,13 @@ function createSessionWindow({ sessionId, title }) { }, 2000); }); + // Keep renderer fullscreen class in sync for dedicated windows. + child.on('enter-full-screen', () => updateChildFullscreenClass()); + child.on('leave-full-screen', () => updateChildFullscreenClass()); + child.on('show', () => updateChildFullscreenClass()); + child.webContents.on('dom-ready', () => updateChildFullscreenClass()); + child.webContents.on('did-finish-load', () => updateChildFullscreenClass()); + // Apply current effects (opacity) to new window as well child.webContents.on('did-finish-load', () => { try { applyWindowEffects(_currentWindowEffects); } catch (_) {} diff --git a/frontend/public/css/style.css b/frontend/public/css/style.css index 221f704..dd73e15 100644 --- a/frontend/public/css/style.css +++ b/frontend/public/css/style.css @@ -995,6 +995,30 @@ textarea { background-color: inherit !important; } +/* Mobile dictation: avoid Android viewport pan toward off-screen IME helper textarea */ +@media (hover: none) and (pointer: coarse) { + html, + body, + #app, + .app-content, + .main-layout, + .terminal-container, + .terminal-content-area, + .terminal-view { + max-width: 100%; + overflow-x: hidden !important; + } + + .xterm .xterm-helper-textarea { + left: 0 !important; + top: 0 !important; + width: 1px !important; + height: 1px !important; + opacity: 0 !important; + z-index: -1 !important; + } +} + /* Debug markers overlay removed */ /* Minimal visual hint when dragging over xterm */ @@ -3615,6 +3639,23 @@ body.session-selected #text-input-btn { flex-direction: column; } +.ws-session-trace-output { + margin: 0; + min-height: 220px; + max-height: 55vh; + overflow: auto; + padding: 10px; + border: 1px solid var(--border-color, #444); + border-radius: 6px; + background: rgba(0, 0, 0, 0.45); + color: var(--text-color, #eee); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 12px; + line-height: 1.4; + white-space: pre-wrap; + word-break: break-word; +} + /* Make new session modal specifically taller */ #new-session-modal .modal-content { max-height: 95vh; /* Even taller for new session modal */ @@ -4374,6 +4415,10 @@ body.responsive-window .terminal-header .dropdown-menu { .is-electron.platform-mac.is-fullscreen body.responsive-window .terminal-header { padding-left: 1rem; } +/* Fallback selector when fullscreen class is present on */ +.is-electron.platform-mac body.is-fullscreen.responsive-window .terminal-header { + padding-left: 1rem; +} /* Ensure xterm viewport uses full height */ body.responsive-minimal .terminal-view .xterm .xterm-viewport { @@ -4530,6 +4575,10 @@ body[data-sidebar-layout="overlay"]:not(.responsive-minimal) .mobile-sidebar-clo .is-electron.platform-mac body.responsive-window .session-info-toolbar { padding-left: var(--mac-header-left-inset, 80px); } +.is-electron.platform-mac.is-fullscreen body.responsive-window .session-info-toolbar, +.is-electron.platform-mac body.is-fullscreen.responsive-window .session-info-toolbar { + padding-left: 1rem; +} /* When in compact height (toolbars reduced), hide session tabs wrappers to eliminate extra borders/space */ body.responsive-compact .session-tabs-bar, diff --git a/frontend/public/index.html b/frontend/public/index.html index 66373ba..513c39f 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -1347,6 +1347,10 @@

Developer

Opens the native DevTools window (Electron desktop only). +
+ + Shows recent client-side websocket/session trace events with copy support. +
@@ -1440,6 +1444,26 @@

State File

+ +