| ... | @@ -1,8 +1,5 @@ | ... | @@ -1,8 +1,5 @@ |
| 1 | /** | 1 | /** |
| 2 | * Shared thread terminal state and reducer. Used by both the main app store | 2 | * Shared thread terminal state utilities used by the terminal Zustand store. |
| 3 | * (persisted threads) and the composer draft store (draft threads), so we can | | |
| 4 | * reuse the same shape and helpers and easily copy terminal state from draft | | |
| 5 | * to thread when reconciling server state. | | |
| 6 | */ | 3 | */ |
| 7 | | 4 | |
| 8 | import { | 5 | import { |
| ... | @@ -23,16 +20,7 @@ export interface ThreadTerminalState { | ... | @@ -23,16 +20,7 @@ export interface ThreadTerminalState { |
| 23 | activeTerminalGroupId: string; | 20 | activeTerminalGroupId: string; |
| 24 | } | 21 | } |
| 25 | | 22 | |
| 26 | export type ThreadTerminalAction = | 23 | // ─── Helpers shared by terminal updaters ─────────────────────────────────── |
| 27 | | { type: "set-open"; open: boolean } | | |
| 28 | | { type: "set-height"; height: number } | | |
| 29 | | { type: "split"; terminalId: string } | | |
| 30 | | { type: "new"; terminalId: string } | | |
| 31 | | { type: "set-active"; terminalId: string } | | |
| 32 | | { type: "close"; terminalId: string } | | |
| 33 | | { type: "set-activity"; terminalId: string; hasRunningSubprocess: boolean }; | | |
| 34 | | | |
| 35 | // ─── Helpers (exported for store reducers that need to work with Thread) ─── | | |
| 36 | | 24 | |
| 37 | export function normalizeTerminalIds(terminalIds: string[]): string[] { | 25 | export function normalizeTerminalIds(terminalIds: string[]): string[] { |
| 38 | const ids = [...new Set(terminalIds.map((id) => id.trim()).filter((id) => id.length > 0))].slice( | 26 | const ids = [...new Set(terminalIds.map((id) => id.trim()).filter((id) => id.length > 0))].slice( |
| ... | @@ -129,6 +117,38 @@ export function normalizeTerminalGroups( | ... | @@ -129,6 +117,38 @@ export function normalizeTerminalGroups( |
| 129 | return nextGroups; | 117 | return nextGroups; |
| 130 | } | 118 | } |
| 131 | | 119 | |
| | 120 | function arraysEqual(a: string[], b: string[]): boolean { |
| | 121 | if (a.length !== b.length) return false; |
| | 122 | for (let index = 0; index < a.length; index += 1) { |
| | 123 | if (a[index] !== b[index]) return false; |
| | 124 | } |
| | 125 | return true; |
| | 126 | } |
| | 127 | |
| | 128 | function terminalGroupsEqual(left: ThreadTerminalGroup[], right: ThreadTerminalGroup[]): boolean { |
| | 129 | if (left.length !== right.length) return false; |
| | 130 | for (let index = 0; index < left.length; index += 1) { |
| | 131 | const leftGroup = left[index]; |
| | 132 | const rightGroup = right[index]; |
| | 133 | if (!leftGroup || !rightGroup) return false; |
| | 134 | if (leftGroup.id !== rightGroup.id) return false; |
| | 135 | if (!arraysEqual(leftGroup.terminalIds, rightGroup.terminalIds)) return false; |
| | 136 | } |
| | 137 | return true; |
| | 138 | } |
| | 139 | |
| | 140 | function threadTerminalStateEqual(left: ThreadTerminalState, right: ThreadTerminalState): boolean { |
| | 141 | return ( |
| | 142 | left.terminalOpen === right.terminalOpen && |
| | 143 | left.terminalHeight === right.terminalHeight && |
| | 144 | left.activeTerminalId === right.activeTerminalId && |
| | 145 | left.activeTerminalGroupId === right.activeTerminalGroupId && |
| | 146 | arraysEqual(left.terminalIds, right.terminalIds) && |
| | 147 | arraysEqual(left.runningTerminalIds, right.runningTerminalIds) && |
| | 148 | terminalGroupsEqual(left.terminalGroups, right.terminalGroups) |
| | 149 | ); |
| | 150 | } |
| | 151 | |
| 132 | // ─── Default & normalize ───────────────────────────────────────────────── | 152 | // ─── Default & normalize ───────────────────────────────────────────────── |
| 133 | | 153 | |
| 134 | const DEFAULT_THREAD_TERMINAL_STATE: ThreadTerminalState = Object.freeze({ | 154 | const DEFAULT_THREAD_TERMINAL_STATE: ThreadTerminalState = Object.freeze({ |
| ... | @@ -171,7 +191,7 @@ export function normalizeThreadTerminalState(state: ThreadTerminalState): Thread | ... | @@ -171,7 +191,7 @@ export function normalizeThreadTerminalState(state: ThreadTerminalState): Thread |
| 171 | const activeGroupIdFromTerminal = | 191 | const activeGroupIdFromTerminal = |
| 172 | terminalGroups.find((group) => group.terminalIds.includes(activeTerminalId))?.id ?? null; | 192 | terminalGroups.find((group) => group.terminalIds.includes(activeTerminalId))?.id ?? null; |
| 173 | | 193 | |
| 174 | return { | 194 | const normalized: ThreadTerminalState = { |
| 175 | terminalOpen: state.terminalOpen, | 195 | terminalOpen: state.terminalOpen, |
| 176 | terminalHeight: | 196 | terminalHeight: |
| 177 | Number.isFinite(state.terminalHeight) && state.terminalHeight > 0 | 197 | Number.isFinite(state.terminalHeight) && state.terminalHeight > 0 |
| ... | @@ -187,52 +207,16 @@ export function normalizeThreadTerminalState(state: ThreadTerminalState): Thread | ... | @@ -187,52 +207,16 @@ export function normalizeThreadTerminalState(state: ThreadTerminalState): Thread |
| 187 | terminalGroups[0]?.id ?? | 207 | terminalGroups[0]?.id ?? |
| 188 | fallbackGroupId(DEFAULT_THREAD_TERMINAL_ID), | 208 | fallbackGroupId(DEFAULT_THREAD_TERMINAL_ID), |
| 189 | }; | 209 | }; |
| | 210 | return threadTerminalStateEqual(state, normalized) ? state : normalized; |
| 190 | } | 211 | } |
| 191 | | 212 | |
| 192 | // ─── Close terminal (shared) ───────────────────────────────────────────── | 213 | export function isDefaultThreadTerminalState(state: ThreadTerminalState): boolean { |
| 193 | | 214 | const normalized = normalizeThreadTerminalState(state); |
| 194 | function closeTerminal( | 215 | return threadTerminalStateEqual(normalized, DEFAULT_THREAD_TERMINAL_STATE); |
| 195 | state: ThreadTerminalState, | 216 | } |
| 196 | terminalId: string, | | |
| 197 | ): ThreadTerminalState { | | |
| 198 | if (!state.terminalIds.includes(terminalId)) { | | |
| 199 | return state; | | |
| 200 | } | | |
| 201 | | | |
| 202 | const remainingTerminalIds = state.terminalIds.filter((id) => id !== terminalId); | | |
| 203 | if (remainingTerminalIds.length === 0) { | | |
| 204 | return createDefaultThreadTerminalState(); | | |
| 205 | } | | |
| 206 | | | |
| 207 | const closedTerminalIndex = state.terminalIds.indexOf(terminalId); | | |
| 208 | const nextActiveTerminalId = | | |
| 209 | state.activeTerminalId === terminalId | | |
| 210 | ? (remainingTerminalIds[Math.min(closedTerminalIndex, remainingTerminalIds.length - 1)] ?? | | |
| 211 | remainingTerminalIds[0] ?? | | |
| 212 | DEFAULT_THREAD_TERMINAL_ID) | | |
| 213 | : state.activeTerminalId; | | |
| 214 | | | |
| 215 | const terminalGroups = state.terminalGroups | | |
| 216 | .map((group) => ({ | | |
| 217 | ...group, | | |
| 218 | terminalIds: group.terminalIds.filter((id) => id !== terminalId), | | |
| 219 | })) | | |
| 220 | .filter((group) => group.terminalIds.length > 0); | | |
| 221 | | | |
| 222 | const nextActiveTerminalGroupId = | | |
| 223 | terminalGroups.find((group) => group.terminalIds.includes(nextActiveTerminalId))?.id ?? | | |
| 224 | terminalGroups[0]?.id ?? | | |
| 225 | fallbackGroupId(nextActiveTerminalId); | | |
| 226 | | 217 | |
| 227 | return normalizeThreadTerminalState({ | 218 | function isValidTerminalId(terminalId: string): boolean { |
| 228 | terminalOpen: state.terminalOpen, | 219 | return terminalId.trim().length > 0; |
| 229 | terminalHeight: state.terminalHeight, | | |
| 230 | terminalIds: remainingTerminalIds, | | |
| 231 | runningTerminalIds: state.runningTerminalIds.filter((id) => id !== terminalId), | | |
| 232 | activeTerminalId: nextActiveTerminalId, | | |
| 233 | terminalGroups, | | |
| 234 | activeTerminalGroupId: nextActiveTerminalGroupId, | | |
| 235 | }); | | |
| 236 | } | 220 | } |
| 237 | | 221 | |
| 238 | function copyTerminalGroups(groups: ThreadTerminalGroup[]): ThreadTerminalGroup[] { | 222 | function copyTerminalGroups(groups: ThreadTerminalGroup[]): ThreadTerminalGroup[] { |
| ... | @@ -242,109 +226,55 @@ function copyTerminalGroups(groups: ThreadTerminalGroup[]): ThreadTerminalGroup[ | ... | @@ -242,109 +226,55 @@ function copyTerminalGroups(groups: ThreadTerminalGroup[]): ThreadTerminalGroup[ |
| 242 | })); | 226 | })); |
| 243 | } | 227 | } |
| 244 | | 228 | |
| 245 | // ─── Single reducer for both draft and persisted thread ──────────────────── | 229 | function upsertTerminalIntoGroups( |
| 246 | | | |
| 247 | export function reduceThreadTerminalState( | | |
| 248 | state: ThreadTerminalState, | 230 | state: ThreadTerminalState, |
| 249 | action: ThreadTerminalAction, | 231 | terminalId: string, |
| | 232 | mode: "split" | "new", |
| 250 | ): ThreadTerminalState { | 233 | ): ThreadTerminalState { |
| 251 | const normalized = normalizeThreadTerminalState(state); | 234 | const normalized = normalizeThreadTerminalState(state); |
| 252 | | 235 | if (!isValidTerminalId(terminalId)) { |
| 253 | if (action.type === "set-open") { | | |
| 254 | if (normalized.terminalOpen === action.open) return normalized; | | |
| 255 | return { ...normalized, terminalOpen: action.open }; | | |
| 256 | } | | |
| 257 | | | |
| 258 | if (action.type === "set-height") { | | |
| 259 | if ( | | |
| 260 | !Number.isFinite(action.height) || | | |
| 261 | action.height <= 0 || | | |
| 262 | normalized.terminalHeight === action.height | | |
| 263 | ) { | | |
| 264 | return normalized; | | |
| 265 | } | | |
| 266 | return { ...normalized, terminalHeight: action.height }; | | |
| 267 | } | | |
| 268 | | | |
| 269 | if (action.type === "set-active") { | | |
| 270 | if (!normalized.terminalIds.includes(action.terminalId)) { | | |
| 271 | return normalized; | | |
| 272 | } | | |
| 273 | const activeTerminalGroupId = | | |
| 274 | normalized.terminalGroups.find((group) => group.terminalIds.includes(action.terminalId))?.id ?? | | |
| 275 | normalized.activeTerminalGroupId; | | |
| 276 | return { | | |
| 277 | ...normalized, | | |
| 278 | activeTerminalId: action.terminalId, | | |
| 279 | activeTerminalGroupId, | | |
| 280 | }; | | |
| 281 | } | | |
| 282 | | | |
| 283 | if (action.type === "close") { | | |
| 284 | return closeTerminal(normalized, action.terminalId); | | |
| 285 | } | | |
| 286 | | | |
| 287 | if (action.type === "set-activity") { | | |
| 288 | if (!normalized.terminalIds.includes(action.terminalId)) { | | |
| 289 | return normalized; | | |
| 290 | } | | |
| 291 | const runningTerminalIds = new Set(normalized.runningTerminalIds); | | |
| 292 | if (action.hasRunningSubprocess) { | | |
| 293 | runningTerminalIds.add(action.terminalId); | | |
| 294 | } else { | | |
| 295 | runningTerminalIds.delete(action.terminalId); | | |
| 296 | } | | |
| 297 | return { ...normalized, runningTerminalIds: [...runningTerminalIds] }; | | |
| 298 | } | | |
| 299 | | | |
| 300 | if (!action.terminalId || action.terminalId.trim().length === 0) { | | |
| 301 | return normalized; | 236 | return normalized; |
| 302 | } | 237 | } |
| 303 | | 238 | |
| 304 | const isNewTerminal = !normalized.terminalIds.includes(action.terminalId); | 239 | const isNewTerminal = !normalized.terminalIds.includes(terminalId); |
| 305 | if (isNewTerminal && normalized.terminalIds.length >= MAX_THREAD_TERMINAL_COUNT) { | 240 | if (isNewTerminal && normalized.terminalIds.length >= MAX_THREAD_TERMINAL_COUNT) { |
| 306 | return normalized; | 241 | return normalized; |
| 307 | } | 242 | } |
| 308 | | 243 | |
| 309 | const terminalIds = isNewTerminal | 244 | const terminalIds = isNewTerminal ? [...normalized.terminalIds, terminalId] : normalized.terminalIds; |
| 310 | ? [...normalized.terminalIds, action.terminalId] | | |
| 311 | : normalized.terminalIds; | | |
| 312 | const terminalGroups = copyTerminalGroups(normalized.terminalGroups); | 245 | const terminalGroups = copyTerminalGroups(normalized.terminalGroups); |
| 313 | const existingGroupIndex = findGroupIndexByTerminalId(terminalGroups, action.terminalId); | 246 | |
| | 247 | const existingGroupIndex = findGroupIndexByTerminalId(terminalGroups, terminalId); |
| 314 | if (existingGroupIndex >= 0) { | 248 | if (existingGroupIndex >= 0) { |
| 315 | terminalGroups[existingGroupIndex]!.terminalIds = terminalGroups[ | 249 | terminalGroups[existingGroupIndex]!.terminalIds = terminalGroups[existingGroupIndex]!.terminalIds.filter( |
| 316 | existingGroupIndex | 250 | (id) => id !== terminalId, |
| 317 | ]!.terminalIds.filter((id) => id !== action.terminalId); | 251 | ); |
| 318 | if (terminalGroups[existingGroupIndex]!.terminalIds.length === 0) { | 252 | if (terminalGroups[existingGroupIndex]!.terminalIds.length === 0) { |
| 319 | terminalGroups.splice(existingGroupIndex, 1); | 253 | terminalGroups.splice(existingGroupIndex, 1); |
| 320 | } | 254 | } |
| 321 | } | 255 | } |
| 322 | | 256 | |
| 323 | if (action.type === "new") { | 257 | if (mode === "new") { |
| 324 | const usedGroupIds = new Set(terminalGroups.map((group) => group.id)); | 258 | const usedGroupIds = new Set(terminalGroups.map((group) => group.id)); |
| 325 | const nextGroupId = assignUniqueGroupId(fallbackGroupId(action.terminalId), usedGroupIds); | 259 | const nextGroupId = assignUniqueGroupId(fallbackGroupId(terminalId), usedGroupIds); |
| 326 | terminalGroups.push({ id: nextGroupId, terminalIds: [action.terminalId] }); | 260 | terminalGroups.push({ id: nextGroupId, terminalIds: [terminalId] }); |
| 327 | return normalizeThreadTerminalState({ | 261 | return normalizeThreadTerminalState({ |
| 328 | ...normalized, | 262 | ...normalized, |
| 329 | terminalOpen: true, | 263 | terminalOpen: true, |
| 330 | terminalIds, | 264 | terminalIds, |
| 331 | activeTerminalId: action.terminalId, | 265 | activeTerminalId: terminalId, |
| 332 | terminalGroups, | 266 | terminalGroups, |
| 333 | activeTerminalGroupId: nextGroupId, | 267 | activeTerminalGroupId: nextGroupId, |
| 334 | }); | 268 | }); |
| 335 | } | 269 | } |
| 336 | | 270 | |
| 337 | // split | | |
| 338 | let activeGroupIndex = terminalGroups.findIndex((group) => group.id === normalized.activeTerminalGroupId); | 271 | let activeGroupIndex = terminalGroups.findIndex((group) => group.id === normalized.activeTerminalGroupId); |
| 339 | if (activeGroupIndex < 0) { | 272 | if (activeGroupIndex < 0) { |
| 340 | activeGroupIndex = findGroupIndexByTerminalId(terminalGroups, normalized.activeTerminalId); | 273 | activeGroupIndex = findGroupIndexByTerminalId(terminalGroups, normalized.activeTerminalId); |
| 341 | } | 274 | } |
| 342 | if (activeGroupIndex < 0) { | 275 | if (activeGroupIndex < 0) { |
| 343 | const usedGroupIds = new Set(terminalGroups.map((group) => group.id)); | 276 | const usedGroupIds = new Set(terminalGroups.map((group) => group.id)); |
| 344 | const nextGroupId = assignUniqueGroupId( | 277 | const nextGroupId = assignUniqueGroupId(fallbackGroupId(normalized.activeTerminalId), usedGroupIds); |
| 345 | fallbackGroupId(normalized.activeTerminalId), | | |
| 346 | usedGroupIds, | | |
| 347 | ); | | |
| 348 | terminalGroups.push({ id: nextGroupId, terminalIds: [normalized.activeTerminalId] }); | 278 | terminalGroups.push({ id: nextGroupId, terminalIds: [normalized.activeTerminalId] }); |
| 349 | activeGroupIndex = terminalGroups.length - 1; | 279 | activeGroupIndex = terminalGroups.length - 1; |
| 350 | } | 280 | } |
| ... | @@ -354,12 +284,12 @@ export function reduceThreadTerminalState( | ... | @@ -354,12 +284,12 @@ export function reduceThreadTerminalState( |
| 354 | return normalized; | 284 | return normalized; |
| 355 | } | 285 | } |
| 356 | | 286 | |
| 357 | if (!destinationGroup.terminalIds.includes(action.terminalId)) { | 287 | if (!destinationGroup.terminalIds.includes(terminalId)) { |
| 358 | const anchorIndex = destinationGroup.terminalIds.indexOf(normalized.activeTerminalId); | 288 | const anchorIndex = destinationGroup.terminalIds.indexOf(normalized.activeTerminalId); |
| 359 | if (anchorIndex >= 0) { | 289 | if (anchorIndex >= 0) { |
| 360 | destinationGroup.terminalIds.splice(anchorIndex + 1, 0, action.terminalId); | 290 | destinationGroup.terminalIds.splice(anchorIndex + 1, 0, terminalId); |
| 361 | } else { | 291 | } else { |
| 362 | destinationGroup.terminalIds.push(action.terminalId); | 292 | destinationGroup.terminalIds.push(terminalId); |
| 363 | } | 293 | } |
| 364 | } | 294 | } |
| 365 | | 295 | |
| ... | @@ -367,8 +297,132 @@ export function reduceThreadTerminalState( | ... | @@ -367,8 +297,132 @@ export function reduceThreadTerminalState( |
| 367 | ...normalized, | 297 | ...normalized, |
| 368 | terminalOpen: true, | 298 | terminalOpen: true, |
| 369 | terminalIds, | 299 | terminalIds, |
| 370 | activeTerminalId: action.terminalId, | 300 | activeTerminalId: terminalId, |
| 371 | terminalGroups, | 301 | terminalGroups, |
| 372 | activeTerminalGroupId: destinationGroup.id, | 302 | activeTerminalGroupId: destinationGroup.id, |
| 373 | }); | 303 | }); |
| 374 | } | 304 | } |
| | 305 | |
| | 306 | // ─── Pure terminal operations ────────────────────────────────────────────── |
| | 307 | |
| | 308 | export function setThreadTerminalOpen(state: ThreadTerminalState, open: boolean): ThreadTerminalState { |
| | 309 | const normalized = normalizeThreadTerminalState(state); |
| | 310 | if (normalized.terminalOpen === open) return normalized; |
| | 311 | return { ...normalized, terminalOpen: open }; |
| | 312 | } |
| | 313 | |
| | 314 | export function setThreadTerminalHeight( |
| | 315 | state: ThreadTerminalState, |
| | 316 | height: number, |
| | 317 | ): ThreadTerminalState { |
| | 318 | const normalized = normalizeThreadTerminalState(state); |
| | 319 | if (!Number.isFinite(height) || height <= 0 || normalized.terminalHeight === height) { |
| | 320 | return normalized; |
| | 321 | } |
| | 322 | return { ...normalized, terminalHeight: height }; |
| | 323 | } |
| | 324 | |
| | 325 | export function splitThreadTerminal( |
| | 326 | state: ThreadTerminalState, |
| | 327 | terminalId: string, |
| | 328 | ): ThreadTerminalState { |
| | 329 | return upsertTerminalIntoGroups(state, terminalId, "split"); |
| | 330 | } |
| | 331 | |
| | 332 | export function newThreadTerminal( |
| | 333 | state: ThreadTerminalState, |
| | 334 | terminalId: string, |
| | 335 | ): ThreadTerminalState { |
| | 336 | return upsertTerminalIntoGroups(state, terminalId, "new"); |
| | 337 | } |
| | 338 | |
| | 339 | export function setThreadActiveTerminal( |
| | 340 | state: ThreadTerminalState, |
| | 341 | terminalId: string, |
| | 342 | ): ThreadTerminalState { |
| | 343 | const normalized = normalizeThreadTerminalState(state); |
| | 344 | if (!normalized.terminalIds.includes(terminalId)) { |
| | 345 | return normalized; |
| | 346 | } |
| | 347 | const activeTerminalGroupId = |
| | 348 | normalized.terminalGroups.find((group) => group.terminalIds.includes(terminalId))?.id ?? |
| | 349 | normalized.activeTerminalGroupId; |
| | 350 | if ( |
| | 351 | normalized.activeTerminalId === terminalId && |
| | 352 | normalized.activeTerminalGroupId === activeTerminalGroupId |
| | 353 | ) { |
| | 354 | return normalized; |
| | 355 | } |
| | 356 | return { |
| | 357 | ...normalized, |
| | 358 | activeTerminalId: terminalId, |
| | 359 | activeTerminalGroupId, |
| | 360 | }; |
| | 361 | } |
| | 362 | |
| | 363 | export function closeThreadTerminal( |
| | 364 | state: ThreadTerminalState, |
| | 365 | terminalId: string, |
| | 366 | ): ThreadTerminalState { |
| | 367 | const normalized = normalizeThreadTerminalState(state); |
| | 368 | if (!normalized.terminalIds.includes(terminalId)) { |
| | 369 | return normalized; |
| | 370 | } |
| | 371 | |
| | 372 | const remainingTerminalIds = normalized.terminalIds.filter((id) => id !== terminalId); |
| | 373 | if (remainingTerminalIds.length === 0) { |
| | 374 | return createDefaultThreadTerminalState(); |
| | 375 | } |
| | 376 | |
| | 377 | const closedTerminalIndex = normalized.terminalIds.indexOf(terminalId); |
| | 378 | const nextActiveTerminalId = |
| | 379 | normalized.activeTerminalId === terminalId |
| | 380 | ? (remainingTerminalIds[Math.min(closedTerminalIndex, remainingTerminalIds.length - 1)] ?? |
| | 381 | remainingTerminalIds[0] ?? |
| | 382 | DEFAULT_THREAD_TERMINAL_ID) |
| | 383 | : normalized.activeTerminalId; |
| | 384 | |
| | 385 | const terminalGroups = normalized.terminalGroups |
| | 386 | .map((group) => ({ |
| | 387 | ...group, |
| | 388 | terminalIds: group.terminalIds.filter((id) => id !== terminalId), |
| | 389 | })) |
| | 390 | .filter((group) => group.terminalIds.length > 0); |
| | 391 | |
| | 392 | const nextActiveTerminalGroupId = |
| | 393 | terminalGroups.find((group) => group.terminalIds.includes(nextActiveTerminalId))?.id ?? |
| | 394 | terminalGroups[0]?.id ?? |
| | 395 | fallbackGroupId(nextActiveTerminalId); |
| | 396 | |
| | 397 | return normalizeThreadTerminalState({ |
| | 398 | terminalOpen: normalized.terminalOpen, |
| | 399 | terminalHeight: normalized.terminalHeight, |
| | 400 | terminalIds: remainingTerminalIds, |
| | 401 | runningTerminalIds: normalized.runningTerminalIds.filter((id) => id !== terminalId), |
| | 402 | activeTerminalId: nextActiveTerminalId, |
| | 403 | terminalGroups, |
| | 404 | activeTerminalGroupId: nextActiveTerminalGroupId, |
| | 405 | }); |
| | 406 | } |
| | 407 | |
| | 408 | export function setThreadTerminalActivity( |
| | 409 | state: ThreadTerminalState, |
| | 410 | terminalId: string, |
| | 411 | hasRunningSubprocess: boolean, |
| | 412 | ): ThreadTerminalState { |
| | 413 | const normalized = normalizeThreadTerminalState(state); |
| | 414 | if (!normalized.terminalIds.includes(terminalId)) { |
| | 415 | return normalized; |
| | 416 | } |
| | 417 | const alreadyRunning = normalized.runningTerminalIds.includes(terminalId); |
| | 418 | if (hasRunningSubprocess === alreadyRunning) { |
| | 419 | return normalized; |
| | 420 | } |
| | 421 | const runningTerminalIds = new Set(normalized.runningTerminalIds); |
| | 422 | if (hasRunningSubprocess) { |
| | 423 | runningTerminalIds.add(terminalId); |
| | 424 | } else { |
| | 425 | runningTerminalIds.delete(terminalId); |
| | 426 | } |
| | 427 | return { ...normalized, runningTerminalIds: [...runningTerminalIds] }; |
| | 428 | } |