@@ -3,7 +3,7 @@ import type { TerminalSession } from "./api-types.js"
33import type { DashboardProject } from "./app-ready-ssh-link-core.js"
44import { browserMenuIndex } from "./menu.js"
55import { projectPickerScreen } from "./screen.js"
6- import { type ActiveTerminalSession , buildProjectActiveTerminalSession } from "./terminal.js"
6+ import { type ActiveTerminalSession , buildProjectActiveTerminalSession , projectSshRoutePath } from "./terminal.js"
77
88type ProjectTerminalAttachArgs = {
99 readonly actionContext : Pick <
@@ -14,6 +14,44 @@ type ProjectTerminalAttachArgs = {
1414 readonly selectTerminalSession : ( sessionId : string ) => void
1515}
1616
17+ export type LoadedSshSessionLink = {
18+ readonly projectDisplayName : string
19+ readonly projectKey : string
20+ readonly session : TerminalSession
21+ }
22+
23+ type LoadedSshSessionAttachArgs = {
24+ readonly actionContext : Pick <
25+ BrowserActionContext ,
26+ "reloadDashboard" | "setActiveScreen" | "setMessage" | "setSelectedMenuIndex" | "setSelectedProjectId"
27+ >
28+ readonly addTerminalSession : ( session : ActiveTerminalSession ) => void
29+ }
30+
31+ /**
32+ * Opens the project terminal screen for a selected project.
33+ *
34+ * @param actionContext - Browser screen/menu setters.
35+ * @param projectId - Project id to select.
36+ * @returns Nothing; state is written through actionContext.
37+ * @pure false
38+ * @effect BrowserActionContext screen/menu selection setters.
39+ * @invariant selected menu is Select and selected project id equals projectId.
40+ * @precondition actionContext is live and projectId is non-empty.
41+ * @postcondition project picker screen is active for projectId.
42+ * @complexity O(1)
43+ * @throws Never
44+ */
45+ // CHANGE: keep SSH link screen selection in a focused shell helper
46+ // WHY: async link handlers need one shared mutation path for project terminal focus
47+ // QUOTE(ТЗ): n/a
48+ // REF: PR #342 CodeRabbit review
49+ // SOURCE: n/a
50+ // FORMAT THEOREM: focus(projectId) -> selectedProjectId = projectId
51+ // PURITY: SHELL
52+ // EFFECT: BrowserActionContext -> setSelectedMenuIndex, setActiveScreen, setSelectedProjectId
53+ // INVARIANT: menu Select and project picker screen are selected together
54+ // COMPLEXITY: O(1)
1755export const showProjectTerminalScreen = (
1856 actionContext : ProjectTerminalAttachArgs [ "actionContext" ] ,
1957 projectId : string
@@ -23,6 +61,72 @@ export const showProjectTerminalScreen = (
2361 actionContext . setSelectedProjectId ( projectId )
2462}
2563
64+ /**
65+ * Attaches a loaded legacy SSH session link to browser terminal state.
66+ *
67+ * @param args - Browser action sinks used for route, terminal, and message updates.
68+ * @param link - Loaded terminal session plus project display metadata.
69+ * @returns Nothing; route and terminal state are written through shell dependencies.
70+ * @pure false
71+ * @effect globalThis.history plus BrowserActionContext setters and addTerminalSession.
72+ * @invariant The browser route points to the attached session id.
73+ * @precondition link.session belongs to link.projectKey.
74+ * @postcondition The project terminal screen is focused and the loaded session is added.
75+ * @complexity O(1)
76+ * @throws Never
77+ */
78+ // CHANGE: isolate loaded legacy session attach side effects
79+ // WHY: stale-link guards in the hook should precede one compact shell transition
80+ // QUOTE(ТЗ): n/a
81+ // REF: PR #342 CodeRabbit review
82+ // SOURCE: n/a
83+ // FORMAT THEOREM: attach(link) -> route = projectSshRoutePath(link.projectKey, link.session.id)
84+ // PURITY: SHELL
85+ // EFFECT: History, BrowserActionContext, addTerminalSession
86+ // INVARIANT: added terminal session is built from the loaded backend session
87+ // COMPLEXITY: O(1)
88+ export const attachLoadedSshSessionLink = (
89+ args : LoadedSshSessionAttachArgs ,
90+ { projectDisplayName, projectKey, session } : LoadedSshSessionLink
91+ ) : void => {
92+ globalThis . history . replaceState ( globalThis . history . state , "" , projectSshRoutePath ( projectKey , session . id ) )
93+ showProjectTerminalScreen ( args . actionContext , session . projectId )
94+ args . addTerminalSession ( buildProjectActiveTerminalSession ( {
95+ onExit : args . actionContext . reloadDashboard ,
96+ onReady : args . actionContext . reloadDashboard ,
97+ projectDisplayName,
98+ projectId : session . projectId ,
99+ projectKey,
100+ session
101+ } ) )
102+ args . actionContext . setMessage ( `Attached SSH terminal for ${ projectDisplayName } .` )
103+ }
104+
105+ /**
106+ * Builds an active terminal session from a backend terminal session.
107+ *
108+ * @param args - Browser callbacks used by the terminal session lifecycle.
109+ * @param project - Dashboard project owning the session.
110+ * @param session - Backend terminal session to wrap.
111+ * @returns Active terminal session ready for the browser tab list.
112+ * @pure true
113+ * @effect n/a
114+ * @invariant session.projectId is preserved in the ActiveTerminalSession.
115+ * @precondition project and session describe the same project.
116+ * @postcondition onExit and onReady both reload the dashboard.
117+ * @complexity O(1)
118+ * @throws Never
119+ */
120+ // CHANGE: isolate ActiveTerminalSession construction for SSH-link workspace attach
121+ // WHY: route attach and workspace attach must use identical terminal metadata
122+ // QUOTE(ТЗ): n/a
123+ // REF: PR #342 CodeRabbit review
124+ // SOURCE: n/a
125+ // FORMAT THEOREM: build(project, session).session = session
126+ // PURITY: CORE
127+ // EFFECT: n/a
128+ // INVARIANT: project display/key/id are copied without mutation
129+ // COMPLEXITY: O(1)
26130const buildProjectTerminalSession = (
27131 args : ProjectTerminalAttachArgs ,
28132 project : DashboardProject ,
@@ -37,6 +141,32 @@ const buildProjectTerminalSession = (
37141 session
38142 } )
39143
144+ /**
145+ * Adds workspace terminal sessions in created-at order and selects the requested one.
146+ *
147+ * @param args - Browser terminal-session sinks.
148+ * @param project - Dashboard project owning the sessions.
149+ * @param sessions - Workspace terminal sessions returned by the backend.
150+ * @param selectedSession - Session that must become active after attach.
151+ * @returns Nothing; terminal tabs and selection are written through args.
152+ * @pure false
153+ * @effect args.addTerminalSession and args.selectTerminalSession.
154+ * @invariant selectedSession is added after all other sessions.
155+ * @precondition selectedSession belongs to sessions.
156+ * @postcondition args.selectTerminalSession is called with selectedSession.id.
157+ * @complexity O(n log n) time and O(n) space where n = sessions.length.
158+ * @throws Never
159+ */
160+ // CHANGE: preserve deterministic workspace session attach ordering
161+ // WHY: opening selected session last keeps browser tab ordering stable while selecting the intended terminal
162+ // QUOTE(ТЗ): n/a
163+ // REF: PR #342 CodeRabbit review
164+ // SOURCE: n/a
165+ // FORMAT THEOREM: attach(sessions, selected) -> selectedSession.id is selected
166+ // PURITY: SHELL
167+ // EFFECT: ProjectTerminalAttachArgs -> addTerminalSession*, selectTerminalSession
168+ // INVARIANT: non-selected sessions are added by ascending createdAt before selectedSession
169+ // COMPLEXITY: O(n log n) time and O(n) space
40170export const attachProjectWorkspaceSessions = (
41171 args : ProjectTerminalAttachArgs ,
42172 project : DashboardProject ,
0 commit comments