Conversation
Move session ownership from EmbeddableTerminal composable to
EmbeddableTerminalState. The terminal process now survives when
EmbeddableTerminal leaves the composition tree.
Changes:
- EmbeddableTerminalState now owns session lifecycle (create/dispose)
- Add initializeSession() internal method for lazy initialization
- Add dispose() public method for explicit cleanup
- Add isDisposed property to check session state
- Add autoDispose parameter to rememberEmbeddableTerminalState()
- true (default): Session disposed when state is forgotten
- false: Session preserved, user must call dispose()
- EmbeddableTerminal becomes a "view" that attaches to state.session
- Backward compatible: state param remains optional
Usage patterns:
1. Auto-dispose (default behavior):
val state = rememberEmbeddableTerminalState()
EmbeddableTerminal(state = state)
2. Preserved across navigation:
val state = rememberEmbeddableTerminalState(autoDispose = false)
if (showTerminal) EmbeddableTerminal(state = state)
// Terminal survives hide/show!
DisposableEffect(Unit) { onDispose { state.dispose() } }
3. App-level state:
val terminalState = EmbeddableTerminalState()
EmbeddableTerminal(state = terminalState)
Closes #104
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Code Review - PR #113: Preserve terminal session across recompositionThank you for this well-designed feature implementation! The architectural change to move session ownership from the composable to the state holder is exactly the right approach for solving #104. Here's my detailed review: ✅ Strengths
🐛 Critical Issues1. Race Condition in Session Initialization (compose-ui/src/desktopMain/kotlin/ai/rever/bossterm/compose/EmbeddableTerminal.kt:114-125)Problem: The LaunchedEffect(effectiveState, resolvedSettings, effectiveCommand) {
if (effectiveState.session == null) {
effectiveState.initializeSession(
// ... uses resolvedSettings and effectiveCommand
)
}
}Impact: If a user changes settings or command at runtime, the session won't update. Recommendation: Either:
I'd recommend Option A since the feature is about session preservation. Settings changes could be documented as requiring explicit 2. Memory Leak with Non-Auto-Dispose State (compose-ui/src/desktopMain/kotlin/ai/rever/bossterm/compose/EmbeddableTerminal.kt:255-259)Problem: When Current Code: fun dispose() {
session?.dispose()
session = null
initialized = false
}Missing: No warning in logs or runtime checks to help developers catch this common mistake. Recommendations:
Example Addition: class EmbeddableTerminalState {
private var disposed = false
fun dispose() {
if (disposed) {
println("WARNING: dispose() called multiple times on EmbeddableTerminalState")
return
}
disposed = true
session?.dispose()
session = null
initialized = false
}
protected fun finalize() {
if (initialized && !disposed) {
println("WARNING: EmbeddableTerminalState finalized without dispose() call - process may leak")
}
}
}3. Callback Staleness (compose-ui/src/desktopMain/kotlin/ai/rever/bossterm/compose/EmbeddableTerminal.kt:231, 246)Problem: The Current Code: internal fun initializeSession(
// ...
onOutput: ((String) -> Unit)?,
onExit: ((Int) -> Unit)?
) {
if (initialized) return
// Callbacks are captured here and never updated
}Impact: If a parent composable updates Recommendation: Store callbacks as mutable state in class EmbeddableTerminalState {
internal var onOutputCallback: ((String) -> Unit)? = null
internal var onExitCallback: ((Int) -> Unit)? = null
internal fun updateCallbacks(
onOutput: ((String) -> Unit)?,
onExit: ((Int) -> Unit)?
) {
onOutputCallback = onOutput
onExitCallback = onExit
}
}
// In EmbeddableTerminal composable
LaunchedEffect(onOutput, onExit) {
effectiveState.updateCallbacks(onOutput, onExit)
}
|
Show how to use autoDispose = false to preserve terminal session across navigation/visibility changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Code Review: Preserve terminal session across recomposition (#104)SummaryThis PR implements a significant architectural improvement by moving session lifecycle ownership from the EmbeddableTerminal composable to EmbeddableTerminalState. This enables terminal processes to survive navigation/visibility changes while maintaining backward compatibility. Overall, this is a well-designed and thoughtfully implemented feature. ✅ StrengthsArchitecture
Documentation
Safety
🔍 Issues & Recommendations1. CRITICAL: Race Condition in Session InitializationLocation: EmbeddableTerminal.kt:114-125 The LaunchedEffect key includes effectiveState, resolvedSettings, and effectiveCommand. If any of these change (e.g., user changes settings), the effect will restart and call initializeSession() again. While the initialized flag prevents duplicate initialization, this could lead to parameter mismatch issues where new settings are ignored. Recommendation: Remove dependencies that should not trigger reinitialization. Only depend on the state instance itself, or add validation to warn when parameters change on an initialized state. 2. Memory Leak Risk: Callback RetentionLocation: EmbeddableTerminal.kt:236-248 The initializeSession() method captures onOutput and onExit callbacks and stores them indirectly. If a user passes lambdas that capture the composable scope, the session will hold references to potentially stale/disposed UI components when using autoDispose=false. Recommendations:
3. Inconsistent Lifecycle with autoDispose=falseLocation: EmbeddableTerminal.kt:350-356 When autoDispose=false, the user must manually call state.dispose(). However:
Recommendation: Add a finalizer to log warnings about undisposed sessions and document prominently in KDoc that forgetting dispose() creates orphan processes. 4. Double Coroutine Scope Potential IssueLocation: EmbeddableTerminal.kt:240 initializeSession() launches process initialization inside session.coroutineScope, but createTerminalSession() already creates a coroutine scope. Then initializeProcess() launches 3 more coroutines in the same scope. Concern: If initializeSession() is somehow called twice (despite guards), you would have duplicate coroutines running. Recommendation: Move the process launch outside of a nested coroutine to avoid potential duplicate launches. 5. Missing State ValidationLocation: EmbeddableTerminalState.kt (various methods) Methods like write(), paste(), scrollBy(), etc., silently no-op when session == null. This could hide bugs where code expects the terminal to be ready. Recommendation: Add explicit checks or logging, or throw exceptions for programmer errors to make issues more visible. 6. Title Change Callback WiringLocation: EmbeddableTerminal.kt:131-139 The title change callback is wired up via LaunchedEffect that depends on session and onTitleChange. If onTitleChange changes during recomposition, the effect restarts but the old collectLatest might still be running briefly. Recommendation: Use rememberUpdatedState to avoid effect restarts when callbacks change. 7. Render-Before-Ready RiskLocation: EmbeddableTerminal.kt:150-159 The ProperTerminal is rendered as soon as session != null, but the process might not be started yet (initialization could fail asynchronously). This means UI could render an "empty" terminal before connection. Recommendation: Consider showing a loading indicator until isConnected == true. 🧪 Testing RecommendationsThe PR description includes a good test plan. I would add:
🔒 Security ConsiderationsNo obvious security issues. The PR maintains existing process isolation and does not introduce new user input paths. ⚡ Performance Considerations
📝 Suggested Improvements
ConclusionThis is a well-architected feature that solves a real usability problem. The main concerns are:
Recommendation: Fix #1, document #2, and consider improvements for #3-7 before merging. Great work! 🎉 Reviewed with BossTerm CLAUDE.md conventions and Compose best practices |
PR Review: Preserve terminal session across recompositionOverviewThis PR successfully addresses issue #104 by moving session ownership from EmbeddableTerminal to EmbeddableTerminalState, allowing terminal processes to survive when the composable leaves composition. Strengths
Critical Issues1. Race Condition in Session Initialization (EmbeddableTerminal.kt:114-121) 2. Resource Leak in Dispose (EmbeddableTerminalState.kt:255-258) 3. Connection State Observation Issue (EmbeddableTerminal.kt:142-143) 4. Documentation Gap SummaryWell-designed feature with excellent API, but needs thread-safety fixes before merging. The race condition (#1) is critical and must be addressed. Great work overall! The architectural direction is sound, just needs thread-safety polish. |
feat: Preserve terminal session across recomposition (#104)
Summary
EmbeddableTerminalcomposable toEmbeddableTerminalStateEmbeddableTerminalleaves the composition treeautoDisposeparameter torememberEmbeddableTerminalState()for lifecycle controlChanges
EmbeddableTerminalStatenow owns session lifecycle (create/dispose)dispose()public method for explicit cleanupisDisposedproperty to check session stateEmbeddableTerminalbecomes a "view" that attaches tostate.sessionstateparam remains optional (defaults to auto-dispose)Usage Patterns
1. Auto-dispose (default - current behavior):
2. Preserved across navigation (new capability):
3. App-level state:
Test plan
Closes #104
🤖 Generated with Claude Code