diff --git a/src/app/TerminalApp.ts b/src/app/TerminalApp.ts index 2cf3ca81..b2631b64 100644 --- a/src/app/TerminalApp.ts +++ b/src/app/TerminalApp.ts @@ -345,7 +345,7 @@ import {liveSessionRows} from '../sessions/LiveSessionView.js'; import {createResumeBrowser, describeArchivedRow, describeLiveRow, LIVE_ROW_LABELS, liveRowAgent, liveRowState, navigateResume, resumeDayLabel, resumeRowCount, resumeSelection, visibleLiveSessions, visibleResumeSessions, type ResumeBrowserState} from '../sessions/ResumeBrowser.js'; import {dismissSessionNotice, listLiveSessions, listSessionNotices} from '../session/connectSession.js'; -import {OLDER_SERVICE_SWITCH, serviceBuildNotice} from '../session/SocketSessionClient.js'; +import {OLDER_SERVICE_SWITCH, serviceBuildNotice, serviceInputNotice} from '../session/SocketSessionClient.js'; import {killAndArchive} from '../session/liveSessions.js'; import {recoverEndedSessions} from '../session/recovery.js'; import {AgentSessions, normalizeProfiles, type AgentProfile} from '../agents/sessions/manager.js'; @@ -1086,6 +1086,8 @@ export class TerminalApp { if (connection?.notice) this.output.addFrontendInteraction('session', connection.notice, ERROR); const stale = connection?.mode === 'service' ? serviceBuildNotice(this.session.serviceBuild, this.buildIdentity) : undefined; if (stale) this.output.addFrontendInteraction('session', stale, INFO); + const routing = connection?.mode === 'service' ? serviceInputNotice(this.session.features) : undefined; + if (routing) this.output.addFrontendInteraction('session', routing, WARNING); this.beginStartupWatch(connection?.attached); this.session.start(); void this.loadInstalledPacks(); diff --git a/src/session/SocketSessionClient.ts b/src/session/SocketSessionClient.ts index 287b10d9..ee11c314 100644 --- a/src/session/SocketSessionClient.ts +++ b/src/session/SocketSessionClient.ts @@ -144,6 +144,18 @@ export function serviceBuildNotice(serviceBuild: string | undefined, own: {versi + 'so fixes to how shells start are not in it yet. The service restarts by itself once every NMSh window and live session has ended.'; } +/** + * A session service without this release's look result (an older NMSh's, e.g. v0.20.0's) still decides who gets what + * is typed while a command runs, the way it did then: #426's and #436's protections are not active in its sessions. + * Said once per window, with when it ends; undefined for a service that has them. + */ +export function serviceInputNotice(features: ReadonlySet): string | undefined { + if (features.has('input-look-result')) return undefined; + return 'This session\'s service is from an older NMSh, so what you type while a command runs is routed as that version did: ' + + 'a password typed before a program asks for it can be shown, queued or run as a command. ' + + 'The service restarts by itself once every NMSh window and live session has ended; new sessions then get the current protections.'; +} + export const OLDER_SERVICE_SWITCH = 'The session service running this session is an older NMSh build without shell switching. ' + 'Its live sessions keep running; once they end, the next nmsh launch starts the current service.'; diff --git a/tests/sessionService.test.ts b/tests/sessionService.test.ts index e1cd94ca..249843b4 100644 --- a/tests/sessionService.test.ts +++ b/tests/sessionService.test.ts @@ -277,6 +277,18 @@ test('a shell from an older session service is called out, because the service b assert.match(serviceBuildNotice(undefined, own) ?? '', /build not reported/u, 'a service too old to report its build'); }); +test('a session service without this release\'s typing protections says so, and when that ends (#426)', async () => { + const {serviceInputNotice} = await import('../src/session/SocketSessionClient.js'); + const {SERVICE_FEATURES} = await import('../src/session/SessionProtocol.js'); + assert.equal(serviceInputNotice(new Set(SERVICE_FEATURES)), undefined, 'this release\'s service: nothing to say'); + // v0.20.0's service: the same protocol, without the look result #426's protections are built on. + const v020 = new Set(['shell-switch', 'shell-backends', 'notices', 'input-state', 'queue', 'queue-conditions', 'background-tasks', 'input-look', 'queue-timeline', 'queue-history']); + const notice = serviceInputNotice(v020) ?? ''; + assert.match(notice, /what you type while a command runs is routed as that version did/u); + assert.match(notice, /password/u, 'says what can happen, plainly'); + assert.match(notice, /restarts by itself once every NMSh window and live session has ended/u); +}); + test('transported queue reviews expire at manual submission and controller reattachment', async () => { const runtimeDir = scratch('nmsh-queue-review-');