Skip to content

Conversation

@phodal
Copy link
Member

@phodal phodal commented Dec 2, 2025

Summary

This PR adds proper handling for terminal command cancellation and cleans up ANSI escape sequences from terminal output before sending to AI.

Changes

User Cancellation Tracking

  • Add cancelledByUser flag to ManagedSession for tracking user-initiated cancellations
  • Add markSessionCancelledByUser() method to ShellSessionManager for non-suspend context
  • Register sessions to ShellSessionManager in ToolOrchestrator for cancel event handling
  • Sync output to ManagedSession in PtyShellExecutor.waitForSession()

ANSI Stripping

  • Add AnsiStripper utility to clean ANSI escape sequences from terminal output
  • Handles CSI sequences (colors, cursor movement), OSC sequences (window title), and simple escape sequences
  • Strip ANSI codes before sending output to AI for cleaner, readable text

Renderer Updates

  • Update CodingAgentRenderer interface with cancelledByUser parameter
  • Update all renderer implementations (JewelRenderer, ComposeRenderer, CodingCliRenderer)
  • Include captured output in error messages when commands timeout or fail

Skip Analysis & Error for Cancelled Commands (New)

  • Skip AnalysisAgent for user-cancelled commands in CodingAgentExecutor and DocumentAgentExecutor
  • Skip redundant error rendering for user-cancelled scenarios
  • Preserve metadata in ToolExecutionResult.failure() for cancelled flag propagation
  • Eliminates double-messaging (Summary + Error) for intentional cancellations

Before

Terminal output sent to AI contained raw ANSI escape sequences:

[2A[1m<[0;1m-------------> 0% EXECUTING [130ms][m[0K[36D[1B...

After

Clean, readable output:

> Task :bootRun
17:11:42.022 [Thread-0] DEBUG org.springframework.boot.devtools.restart.classloader.RestartClassLoader - Created RestartClassLoader...

When user cancels a command:

  • ❌ Before: AnalysisAgent triggered + "Tool execution failed" error displayed
  • ✅ After: Only concise cancellation confirmation shown

Testing

Tested with CodingCli running ./gradlew bootRun command - output is now clean and readable.


Pull Request opened by Augment Code with guidance from the PR author

Summary by CodeRabbit

  • New Features

    • Track and report user-cancelled shell sessions with a distinct cancellation status.
    • Live PTY session management and background monitoring for async shell commands.
  • Improvements

    • Strip terminal formatting for cleaner rendered output across UI, IDE and CLI.
    • Propagate cancellation flag and cleaned output to renderers; show explicit "Cancelled by user" results.
    • Preserve execution metadata in error reports and skip post-processing/analysis for cancelled runs.

✏️ Tip: You can customize this high-level summary in your review settings.

… terminal output

- Add cancelledByUser flag to ManagedSession for tracking user-initiated cancellations
- Add markSessionCancelledByUser() method to ShellSessionManager for non-suspend context
- Register sessions to ShellSessionManager in ToolOrchestrator for cancel event handling
- Sync output to ManagedSession in PtyShellExecutor.waitForSession()
- Add AnsiStripper utility to clean ANSI escape sequences from terminal output
- Strip ANSI codes before sending output to AI for cleaner, readable text
- Update CodingAgentRenderer interface with cancelledByUser parameter
- Update all renderer implementations (JewelRenderer, ComposeRenderer, CodingCliRenderer)
- Include captured output in error messages when commands timeout or fail
@coderabbitai
Copy link

coderabbitai bot commented Dec 2, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds ANSI-stripping utilities, live PTY session registration and cancellation tracking, integrates managed sessions into PTY orchestration and execution, propagates a cancelledByUser flag through execution results and renderers, and enriches error/result flows with cleaned output and cancellation metadata.

Changes

Cohort / File(s) Summary
ANSI Output Cleanup
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/AnsiStripper.kt
New public AnsiStripper with strip() and stripAndNormalize() to remove ANSI escape sequences and normalize line endings.
Session Management
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/ShellSessionManager.kt
Added markSessionCancelledByUser(sessionId: String) and var cancelledByUser: Boolean on ManagedSession to record user-initiated cancellations.
Orchestration & Dispatch
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/orchestrator/ToolOrchestrator.kt
Integrated ShellSessionManager for live sessions (registering sessions, propagating isAlive/kill handlers), strip ANSI from outputs before rendering, propagate cancelledByUser to renderers, build enriched error outputs including cleaned output and cancellation flag, broaden dispatch to run more tools via ExecutableTool, and add async session monitoring.
PTY Execution
mpp-core/src/jvmMain/kotlin/cc/unitmesh/agent/tool/shell/PtyShellExecutor.kt
Synchronizes completion with managed sessions; delegates exit code/finished state to managed session and ensures output synchronization on completion.
Renderer API (core)
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/render/CodingAgentRenderer.kt
Extended updateLiveTerminalStatus(sessionId, exitCode, executionTimeMs, output?, cancelledByUser: Boolean = false) signature.
IDE Renderer
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt
updateLiveTerminalStatus() signature updated; adds explicit CANCELLED_BY_USER result path with enriched message and metadata when cancelled.
IDE Integration
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt
Calls ShellSessionManager.markSessionCancelledByUser(sessionId) in cancellation handling before destroying the process.
Compose UI Renderer
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/ComposeRenderer.kt
updateLiveTerminalStatus() now accepts cancelledByUser; uses cancellation-aware errorType/message and includes cancelled metadata.
CLI Renderer
mpp-ui/src/jvmMain/kotlin/cc/unitmesh/server/cli/CodingCli.kt
updateLiveTerminalStatus() now accepts cancelledByUser; prints cancellation-aware status, returns CANCELLED_BY_USER error with metadata and formatted cancellation message when applicable.
Executor / Analysis
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/executor/*.kt
Executors (CodingAgentExecutor, DocumentAgentExecutor) check executionResult.metadata["cancelled"] == "true" and skip rendering/long-content analysis for user-cancelled executions.
ToolExecutionResult
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/orchestrator/ToolExecutionResult.kt
failure now constructs ToolResult.Error including provided metadata so downstream code can inspect cancellation flags and captured output.

Sequence Diagram

sequenceDiagram
    actor User
    participant IDE as IDE / ViewModel
    participant ToolOrch as ToolOrchestrator
    participant PtyExec as PtyShellExecutor
    participant SessionMgr as ShellSessionManager
    participant Ansi as AnsiStripper
    participant Renderer as Renderer

    User->>IDE: start shell command
    IDE->>ToolOrch: executeShellTool(...)
    ToolOrch->>SessionMgr: registerLiveSession(sessionId, handlers)
    ToolOrch->>PtyExec: start PTY process
    PtyExec->>SessionMgr: forward output lines to ManagedSession
    PtyExec->>ToolOrch: notify async session started (background)
    User->>IDE: cancel command
    IDE->>SessionMgr: markSessionCancelledByUser(sessionId)
    IDE->>PtyExec: kill PTY process
    PtyExec-->>ToolOrch: completion/exception + collected output
    ToolOrch->>Ansi: strip(output)
    ToolOrch->>SessionMgr: query cancelledByUser(sessionId)
    ToolOrch->>Renderer: updateLiveTerminalStatus(sessionId, exitCode, time, cleanedOutput, cancelledByUser)
    Renderer->>Renderer: build CANCELLED_BY_USER or success/error result
    Renderer-->>IDE: render result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Inspect thread-safety and mutation of ManagedSession.cancelledByUser and markSessionCancelledByUser.
  • Verify ANSI stripping correctness and edge cases in AnsiStripper.
  • Review coroutine lifecycles, output-forwarding and race conditions between PTY readers, managed session updates, and process termination in PtyShellExecutor.
  • Confirm consistent propagation of cancelledByUser and metadata across all renderer implementations and executor paths.

Possibly related PRs

Poem

🐰 I hopped through PTYs to catch each line,
I stripped the colors till outputs shined,
When humans shout "stop", I mark the run,
I fold clean logs and flag cancelled ones,
Carrots for clarity — tidy, small, and fun. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.27% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(terminal): add user cancellation tracking and ANSI stripping for terminal output' accurately summarizes the main changes: adding user cancellation tracking and ANSI stripping functionality for terminal output.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/terminal-cancel-with-output-capture

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3adf74c and 9e13797.

📒 Files selected for processing (1)
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/ShellSessionManager.kt (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.kt

📄 CodeRabbit inference engine (AGENTS.md)

**/*.kt: Use expect/actual for platform-specific code (e.g., file I/O on JVM/JS/Wasm) in Kotlin Multiplatform projects
Check export first if some functions are not working well with CLI (TypeScript)
In Kotlin/JS @JsExport: Avoid Flow, use Promise instead
In Kotlin/JS @JsExport: Use concrete classes as return types and parameter types; avoid interface types
For WASM platform, avoid using emoji and UTF-8 in code

Files:

  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/ShellSessionManager.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Test mpp-core (KMP)
  • GitHub Check: Build (241)
  • GitHub Check: Build (223)
🔇 Additional comments (1)
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/ShellSessionManager.kt (1)

119-124: cancelledByUser flag aligns with the new cancellation semantics

The additional cancelledByUser property cleanly encodes user-initiated cancellation and fits the broader renderer/orchestrator changes; once the writer path is made thread-safe (see markSessionCancelledByUser comment), this field itself doesn’t need further changes here.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt (1)

413-416: LGTM! Correct ordering of cancellation marking before process termination.

The session is properly marked as cancelled by user before destroying the process, which ensures ToolOrchestrator.startSessionMonitoring() can detect the user-initiated cancellation.

Consider adding an import for ShellSessionManager to avoid the fully-qualified name:

import cc.unitmesh.agent.tool.shell.ShellSessionManager

Then simplify line 415 to:

ShellSessionManager.markSessionCancelledByUser(cancelEvent.sessionId)
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/AnsiStripper.kt (1)

108-116: Redundant \r handling in stripAndNormalize.

The strip() function already removes all carriage return characters (\r) on lines 54-57. Therefore, calling .replace("\r\n", "\n").replace("\r", "") after strip() is redundant since the returned string will never contain \r.

If the intent is to preserve \r\n as \n (normalize before stripping), the order should be reversed:

     fun stripAndNormalize(text: String): String {
-        return strip(text)
-            .replace("\r\n", "\n")
-            .replace("\r", "")
+        return strip(
+            text.replace("\r\n", "\n")
+                .replace("\r", "")
+        )
     }

Or, if the current behavior (strip all \r) is intended, simplify to:

     fun stripAndNormalize(text: String): String {
-        return strip(text)
-            .replace("\r\n", "\n")
-            .replace("\r", "")
+        return strip(text)
     }
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/render/CodingAgentRenderer.kt (1)

53-60: KDoc comment may be misleading about exit code 137.

The comment states that cancelledByUser corresponds to exit code 137 (SIGKILL), but user cancellation could result in different exit codes depending on the signal used (e.g., SIGTERM=143, SIGINT=130). The cancelledByUser flag should be independent of the specific exit code.

Consider updating the documentation:

-     * @param cancelledByUser Whether the command was cancelled by the user (exit code 137)
+     * @param cancelledByUser Whether the command was cancelled by the user
mpp-ui/src/jvmMain/kotlin/cc/unitmesh/server/cli/CodingCli.kt (1)

294-342: Consider extracting duplicate cancellation result construction.

The cancellation error message construction (lines 300-320) and result creation is nearly identical to the code in awaitProcess (lines 418-438). This duplication could lead to inconsistencies if one is updated but not the other.

Consider extracting a helper function:

private fun createCancelledByUserResult(
    command: String,
    exitCode: Int,
    sessionId: String,
    output: String,
    executionTimeMs: Long? = null
): ToolResult.Error {
    val message = buildString {
        appendLine("⚠️ Command cancelled by user")
        appendLine()
        appendLine("Command: $command")
        appendLine("Exit code: $exitCode (SIGKILL)")
        appendLine()
        if (output.isNotEmpty()) {
            appendLine("Output before cancellation:")
            appendLine(output)
        } else {
            appendLine("(no output captured before cancellation)")
        }
    }
    val metadata = mutableMapOf(
        "exit_code" to exitCode.toString(),
        "session_id" to sessionId,
        "cancelled" to "true",
        "output" to output
    )
    executionTimeMs?.let { metadata["execution_time_ms"] = it.toString() }
    return ToolResult.Error(message, errorType = "CANCELLED_BY_USER", metadata = metadata)
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 03cadc6 and 039c09b.

📒 Files selected for processing (9)
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/orchestrator/ToolOrchestrator.kt (4 hunks)
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/render/CodingAgentRenderer.kt (1 hunks)
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/AnsiStripper.kt (1 hunks)
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/ShellSessionManager.kt (2 hunks)
  • mpp-core/src/jvmMain/kotlin/cc/unitmesh/agent/tool/shell/PtyShellExecutor.kt (2 hunks)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt (2 hunks)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt (1 hunks)
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/ComposeRenderer.kt (2 hunks)
  • mpp-ui/src/jvmMain/kotlin/cc/unitmesh/server/cli/CodingCli.kt (3 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.kt

📄 CodeRabbit inference engine (AGENTS.md)

**/*.kt: Use expect/actual for platform-specific code (e.g., file I/O on JVM/JS/Wasm) in Kotlin Multiplatform projects
Check export first if some functions are not working well with CLI (TypeScript)
In Kotlin/JS @JsExport: Avoid Flow, use Promise instead
In Kotlin/JS @JsExport: Use concrete classes as return types and parameter types; avoid interface types
For WASM platform, avoid using emoji and UTF-8 in code

Files:

  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/ShellSessionManager.kt
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/AnsiStripper.kt
  • mpp-core/src/jvmMain/kotlin/cc/unitmesh/agent/tool/shell/PtyShellExecutor.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/ComposeRenderer.kt
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/render/CodingAgentRenderer.kt
  • mpp-ui/src/jvmMain/kotlin/cc/unitmesh/server/cli/CodingCli.kt
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/orchestrator/ToolOrchestrator.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt
**/idea/**/*.kt

📄 CodeRabbit inference engine (AGENTS.md)

**/idea/**/*.kt: For SwingPanel z-index issues with Compose popups, enable Jewel's custom popup renderer: JewelFlags.useCustomPopupRenderer = true in IdeaAgentToolWindowFactory
For popup/dropdown menus in IntelliJ plugins, use Jewel's PopupMenu instead of androidx.compose.ui.window.Popup
For dialogs in IntelliJ plugins, use IntelliJ's DialogWrapper with org.jetbrains.jewel.bridge.compose instead of androidx.compose.ui.window.Dialog

Files:

  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt
**/compose/**/*.{kt,kts}

📄 CodeRabbit inference engine (AGENTS.md)

In Compose (Desktop/Android), use AutoDevColors from cc.unitmesh.devins.ui.compose.theme or MaterialTheme.colorScheme

Files:

  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/ComposeRenderer.kt
{**/compose/**/*.kt,mpp-ui/src/jsMain/typescript/**/*.{ts,tsx}}

📄 CodeRabbit inference engine (AGENTS.md)

DO NOT hardcode colors (e.g., Color(0xFF...) or #hex). Always use design tokens for consistency across platforms

Files:

  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/ComposeRenderer.kt
🧬 Code graph analysis (1)
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/orchestrator/ToolOrchestrator.kt (2)
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/logging/LoggingInitializer.kt (2)
  • getLogger (93-98)
  • getLogger (100-105)
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/logging/AutoDevLogger.kt (1)
  • getLogger (38-42)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Agent
  • GitHub Check: Test mpp-core (KMP)
  • GitHub Check: Build (223)
  • GitHub Check: Build (241)
🔇 Additional comments (10)
mpp-core/src/jvmMain/kotlin/cc/unitmesh/agent/tool/shell/PtyShellExecutor.kt (1)

307-314: LGTM! Proper completion sequence with output synchronization.

The implementation correctly:

  1. Waits for process completion with timeout
  2. Allows output reading job to finish with a short timeout
  3. Marks both the LiveShellSession and ManagedSession as completed

The 1-second timeout for output reading is a reasonable safeguard.

mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/ShellSessionManager.kt (1)

118-122: LGTM on the property addition conceptually.

The cancelledByUser flag provides a clear semantic distinction between user-initiated cancellations and other failure modes. The thread-safety concern for this property is addressed in the comment above.

mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/AnsiStripper.kt (1)

21-66: LGTM! Solid ANSI escape sequence stripping implementation.

The implementation correctly handles:

  • CSI sequences (ESC[...X) for colors, cursor movement
  • OSC sequences (ESC]...BEL/ST) for window titles
  • Character set selection (ESC(X, ESC)X)
  • Simple escape sequences

The early return optimization on line 22-24 is a nice touch for performance.

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/ComposeRenderer.kt (1)

450-501: LGTM! Clean implementation of cancellation-aware terminal status updates.

The changes properly:

  1. Add cancelledByUser parameter to distinguish user-initiated cancellations
  2. Generate appropriate error messages based on the cancellation source
  3. Include cancelled and output in the metadata for downstream consumers

The branching logic on lines 482-487 provides clear, user-friendly messaging.

mpp-ui/src/jvmMain/kotlin/cc/unitmesh/server/cli/CodingCli.kt (1)

220-238: LGTM!

The status symbol and message logic correctly handles the three states (cancelled, success, failure) with appropriate visual indicators.

mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/orchestrator/ToolOrchestrator.kt (4)

116-129: LGTM on session registration logic.

The session is properly registered with ShellSessionManager before the renderer is notified. The process handlers (isAlive, kill) are correctly delegated to the LiveShellSession, enabling proper cancellation control.


296-311: Clean implementation of ANSI stripping and cancellation propagation.

The output is properly sanitized before being sent to the renderer, and the cancellation status is correctly retrieved from the managed session and propagated.


314-355: Good defensive error handling with output capture.

The error path properly:

  1. Retrieves the managed session to check cancellation status
  2. Falls back through multiple output sources (managedSession → liveSession)
  3. Strips ANSI codes before constructing the error message
  4. Includes helpful debug logging for troubleshooting

This ensures the AI receives useful context even when errors occur.


373-394: LGTM on the when expression simplification.

The inline toolName.toToolType() call is clean and readable.

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt (1)

455-456: LGTM on the API update.

The method signature correctly accepts the new cancelledByUser parameter with proper integration into the result construction logic.

Comment on lines 56 to 65

/**
* Mark a session as cancelled by user.
* This is a non-suspend function for use in UI callbacks where coroutine context may not be available.
* Note: This accesses the sessions map without locking, which is safe for this specific use case
* because we're only setting a boolean flag on an existing session.
*/
fun markSessionCancelledByUser(sessionId: String) {
sessions[sessionId]?.cancelledByUser = true
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Data race: accessing sessions map without synchronization.

The comment claims this is safe because "we're only setting a boolean flag," but the real issue is that sessions[sessionId] performs an unsynchronized read on a mutableMapOf (backed by HashMap) which is not thread-safe. Concurrent calls to registerSession, removeSession, or getSession could cause:

  1. ConcurrentModificationException
  2. Reading stale or corrupted data
  3. The session being removed between the read and the flag assignment

Additionally, cancelledByUser is not @Volatile, so the write may not be visible to other threads.

Consider one of these fixes:

Option 1: Use @Volatile for the flag and synchronize map access:

-    fun markSessionCancelledByUser(sessionId: String) {
-        sessions[sessionId]?.cancelledByUser = true
-    }
+    fun markSessionCancelledByUser(sessionId: String) {
+        synchronized(sessions) {
+            sessions[sessionId]?.cancelledByUser = true
+        }
+    }

And in ManagedSession:

-    var cancelledByUser: Boolean = false
+    @Volatile var cancelledByUser: Boolean = false

Option 2: Use a concurrent map:

-private val sessions = mutableMapOf<String, ManagedSession>()
+private val sessions = java.util.concurrent.ConcurrentHashMap<String, ManagedSession>()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Mark a session as cancelled by user.
* This is a non-suspend function for use in UI callbacks where coroutine context may not be available.
* Note: This accesses the sessions map without locking, which is safe for this specific use case
* because we're only setting a boolean flag on an existing session.
*/
fun markSessionCancelledByUser(sessionId: String) {
sessions[sessionId]?.cancelledByUser = true
}
/**
* Mark a session as cancelled by user.
* This is a non-suspend function for use in UI callbacks where coroutine context may not be available.
* Note: This accesses the sessions map without locking, which is safe for this specific use case
* because we're only setting a boolean flag on an existing session.
*/
fun markSessionCancelledByUser(sessionId: String) {
synchronized(sessions) {
sessions[sessionId]?.cancelledByUser = true
}
}

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds user cancellation tracking for terminal commands and implements ANSI escape sequence stripping to provide clean output to AI models. The changes span multiple components including session management, shell execution, rendering layers, and output processing.

Key Changes:

  • Added cancelledByUser flag to ManagedSession for distinguishing user cancellations from command failures
  • Implemented AnsiStripper utility to remove terminal control sequences (colors, cursor movements) from output
  • Updated all renderer implementations to handle cancellation status and display appropriate user feedback

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
AnsiStripper.kt New utility for stripping ANSI escape sequences from terminal output
ShellSessionManager.kt Added cancellation tracking with cancelledByUser flag and markSessionCancelledByUser() method
PtyShellExecutor.kt Added output synchronization to ManagedSession during command execution
ToolOrchestrator.kt Integrated ANSI stripping and cancellation detection in session monitoring
CodingAgentRenderer.kt Updated interface to include cancelledByUser parameter in updateLiveTerminalStatus()
CodingCli.kt Implemented cancellation handling with special status symbols and detailed error messages
JewelRenderer.kt Added cancellation-aware result handling with captured output
ComposeRenderer.kt Updated to distinguish user cancellations from command failures
IdeaAgentViewModel.kt Marks sessions as cancelled before terminating processes

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 418 to 430
message = buildString {
appendLine("⚠️ Command cancelled by user")
appendLine()
appendLine("Command: ${session.command}")
appendLine("Exit code: $exitCode (SIGKILL)")
appendLine()
if (output.isNotEmpty()) {
appendLine("Output before cancellation:")
appendLine(output)
} else {
appendLine("(no output captured before cancellation)")
}
},
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Duplication: Similar to the first occurrence, this error message construction is duplicated. Both instances (lines 301-313 and 418-430) build essentially the same cancelled message structure.

See the previous comment for the suggested helper function to eliminate this duplication.

Copilot uses AI. Check for mistakes.
Comment on lines +483 to +494
val errorMessage = buildString {
appendLine("⚠️ Command cancelled by user")
appendLine()
appendLine("Exit code: $exitCode (SIGKILL)")
appendLine()
if (!output.isNullOrEmpty()) {
appendLine("Output before cancellation:")
appendLine(output)
} else {
appendLine("(no output captured before cancellation)")
}
}
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Duplication: The cancelled command error message building is also duplicated in JewelRenderer (lines 483-494). This is the third instance of this pattern across different renderers.

Solution: Consider extracting this to a shared utility function in the CodingAgentRenderer interface or a common utility class, so all renderers can use the same logic.

Copilot uses AI. Check for mistakes.
// Use new ExecutableTool architecture for most tools
// Only special-case tools that need custom handling (shell with PTY, etc.)
return when (val toolType = toolName.toToolType()) {
return when (toolName.toToolType()) {
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Unnecessary Variable: The toolType variable was removed but the code still works because the when expression now directly evaluates toolName.toToolType(). However, if this method is called multiple times in error paths or has side effects, it could be inefficient.

While this change works, if toToolType() is expensive or has side effects, consider keeping the variable. Otherwise, this is a minor improvement.

Suggested change
return when (toolName.toToolType()) {
val toolType = toolName.toToolType()
return when (toolType) {

Copilot uses AI. Check for mistakes.
Comment on lines 269 to 282
// Start output reading job to sync to ManagedSession
// This ensures output is available for ToolOrchestrator.startSessionMonitoring()
// Note: For IDEA, ProcessOutputCollector in IdeaLiveTerminalBubble also reads output,
// but that's OK because they read from different streams or the same stream is already consumed.
val outputJob = launch {
try {
ptyHandle.inputStream.bufferedReader().use { reader ->
var line = reader.readLine()
while (line != null && isActive) {
val lineWithNewline = line + "\n"
// Sync to LiveShellSession (for backward compatibility)
session.appendStdout(lineWithNewline)
// Sync to ManagedSession (for ToolOrchestrator)
managedSession?.appendOutput(lineWithNewline)
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential Data Race: Multiple coroutines may be reading from the same PTY input stream concurrently. The comment mentions "ProcessOutputCollector in IdeaLiveTerminalBubble also reads output", but reading from the same InputStream concurrently is unsafe. Only one reader should consume from ptyHandle.inputStream at a time, or the output will be split unpredictably between readers.

Solution: Either:

  • Ensure only one reader exists per process (coordinate between this code and ProcessOutputCollector)
  • Use a broadcast/multicast pattern where one reader distributes to multiple consumers
  • Document clearly which component is responsible for reading in each scenario

Copilot uses AI. Check for mistakes.
Comment on lines 301 to 313
message = buildString {
appendLine("⚠️ Command cancelled by user")
appendLine()
appendLine("Command: ${session.command}")
appendLine("Exit code: $exitCode (SIGKILL)")
appendLine()
if (output.isNotEmpty()) {
appendLine("Output before cancellation:")
appendLine(output)
} else {
appendLine("(no output captured before cancellation)")
}
},
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Duplication: The error message building logic for cancelled commands is duplicated in two places (lines 301-313 and 418-430). This same pattern appears with nearly identical structure and content.

Solution: Extract this into a helper function:

private fun buildCancelledMessage(command: String, exitCode: Int, output: String): String {
    return buildString {
        appendLine("⚠️ Command cancelled by user")
        appendLine()
        appendLine("Command: $command")
        appendLine("Exit code: $exitCode (SIGKILL)")
        appendLine()
        if (output.isNotEmpty()) {
            appendLine("Output before cancellation:")
            appendLine(output)
        } else {
            appendLine("(no output captured before cancellation)")
        }
    }
}

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 117
package cc.unitmesh.agent.tool.shell

/**
* Utility object for stripping ANSI escape sequences from terminal output.
* This converts raw terminal output with color codes, cursor movements, etc.
* into clean, readable ASCII text.
*/
object AnsiStripper {
private const val ESC = '\u001B'

/**
* Strip all ANSI escape sequences from the given text.
* Handles:
* - CSI sequences (ESC[...X) - colors, cursor movement, erase
* - OSC sequences (ESC]...BEL/ST) - window title, etc.
* - Simple escape sequences (ESC X)
*
* @param text The text containing ANSI escape sequences
* @return Clean text with all escape sequences removed
*/
fun strip(text: String): String {
if (!text.contains(ESC)) {
return text
}

val result = StringBuilder()
var i = 0

while (i < text.length) {
val ch = text[i]

when {
ch == ESC && i + 1 < text.length -> {
val next = text[i + 1]
when (next) {
'[' -> {
// CSI sequence: ESC[...X (ends with a letter)
i = skipCsiSequence(text, i + 2)
}
']' -> {
// OSC sequence: ESC]...BEL or ESC]...ST
i = skipOscSequence(text, i + 2)
}
'(' , ')' -> {
// Character set selection: ESC(X or ESC)X
i = if (i + 2 < text.length) i + 3 else text.length
}
else -> {
// Simple escape sequence: ESC X
i += 2
}
}
}
ch == '\r' -> {
// Carriage return - skip it (will be handled with newlines)
i++
}
else -> {
result.append(ch)
i++
}
}
}

return result.toString()
}

/**
* Skip a CSI sequence starting at the given position.
* CSI sequences end with a letter (0x40-0x7E).
*/
private fun skipCsiSequence(text: String, start: Int): Int {
var i = start
while (i < text.length) {
val ch = text[i]
if (ch in '@'..'~') {
// Found the terminating character
return i + 1
}
i++
}
return text.length
}

/**
* Skip an OSC sequence starting at the given position.
* OSC sequences end with BEL (0x07) or ST (ESC\).
*/
private fun skipOscSequence(text: String, start: Int): Int {
var i = start
while (i < text.length) {
val ch = text[i]
when {
ch == '\u0007' -> {
// BEL character terminates OSC
return i + 1
}
ch == ESC && i + 1 < text.length && text[i + 1] == '\\' -> {
// ST (String Terminator) terminates OSC
return i + 2
}
}
i++
}
return text.length
}

/**
* Strip ANSI sequences and also normalize line endings.
* Converts \r\n to \n and removes standalone \r.
*/
fun stripAndNormalize(text: String): String {
return strip(text)
.replace("\r\n", "\n")
.replace("\r", "")
}
}
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing Test Coverage: The new AnsiStripper utility is not covered by tests. Given the complexity of ANSI escape sequence handling (CSI, OSC, character sets, etc.) and the presence of a critical bug (infinite loop in skipOscSequence), this component requires comprehensive test coverage.

Recommendation: Add tests covering:

  • CSI sequences (colors, cursor movement)
  • OSC sequences (with BEL and ST terminators)
  • Character set selection sequences
  • Edge cases (malformed sequences, unterminated sequences)
  • The stripAndNormalize() method

Copilot uses AI. Check for mistakes.
Comment on lines 57 to 65
/**
* Mark a session as cancelled by user.
* This is a non-suspend function for use in UI callbacks where coroutine context may not be available.
* Note: This accesses the sessions map without locking, which is safe for this specific use case
* because we're only setting a boolean flag on an existing session.
*/
fun markSessionCancelledByUser(sessionId: String) {
sessions[sessionId]?.cancelledByUser = true
}
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing Test Coverage: The cancelledByUser flag and markSessionCancelledByUser() method are not covered by tests. Given the thread-safety concerns with this implementation and its critical role in distinguishing user cancellations from other failures, this functionality needs test coverage.

Recommendation: Add tests verifying:

  • Setting and retrieving the cancelledByUser flag
  • Thread-safe access to the flag
  • Integration with the cancellation flow

Copilot uses AI. Check for mistakes.
Comment on lines 59 to 64
* This is a non-suspend function for use in UI callbacks where coroutine context may not be available.
* Note: This accesses the sessions map without locking, which is safe for this specific use case
* because we're only setting a boolean flag on an existing session.
*/
fun markSessionCancelledByUser(sessionId: String) {
sessions[sessionId]?.cancelledByUser = true
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thread Safety Issue: The markSessionCancelledByUser() method accesses the mutable sessions map without acquiring the mutex lock. While the comment claims this is "safe" because it only sets a boolean flag, this is actually not thread-safe. The map itself could be modified (e.g., by removeSession() or registerSession()) concurrently, which could cause:

  1. A ConcurrentModificationException on the JVM
  2. Undefined behavior on other platforms
  3. The session might be removed between the map access and the flag set

Solution: Either:

  • Make cancelledByUser an AtomicBoolean or use another thread-safe mechanism
  • Keep the method as suspend and use mutex.withLock { sessions[sessionId]?.cancelledByUser = true }
Suggested change
* This is a non-suspend function for use in UI callbacks where coroutine context may not be available.
* Note: This accesses the sessions map without locking, which is safe for this specific use case
* because we're only setting a boolean flag on an existing session.
*/
fun markSessionCancelledByUser(sessionId: String) {
sessions[sessionId]?.cancelledByUser = true
* This is a suspend function and must be called from a coroutine context.
* The access to the sessions map is now protected by the mutex for thread safety.
*/
suspend fun markSessionCancelledByUser(sessionId: String) {
mutex.withLock {
sessions[sessionId]?.cancelledByUser = true
}

Copilot uses AI. Check for mistakes.
// OSC sequence: ESC]...BEL or ESC]...ST
i = skipOscSequence(text, i + 2)
}
'(' , ')' -> {
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spacing Issue: Extra space in the when branch between '(' and ','. This should be '(', ')' without the space for consistency with Kotlin formatting conventions.

'(', ')' -> {
Suggested change
'(' , ')' -> {
'(', ')' -> {

Copilot uses AI. Check for mistakes.
Comment on lines 229 to 232
val statusMessage = when {
cancelledByUser -> "Cancelled by user"
exitCode == 0 -> "Exit code: $exitCode"
else -> "Exit code: $exitCode"
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant status message logic: Lines 229-233 have identical logic for both the success case (exitCode == 0) and the else case. Both branches produce "Exit code: $exitCode", making the distinction unnecessary.

Solution: Simplify to:

val statusMessage = if (cancelledByUser) {
    "Cancelled by user"  
} else {
    "Exit code: $exitCode"
}
Suggested change
val statusMessage = when {
cancelledByUser -> "Cancelled by user"
exitCode == 0 -> "Exit code: $exitCode"
else -> "Exit code: $exitCode"
val statusMessage = if (cancelledByUser) {
"Cancelled by user"
} else {
"Exit code: $exitCode"

Copilot uses AI. Check for mistakes.
Copy link

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. No suggestions at this time.

Comment augment review to trigger a new review at any time.

…commands

- Skip AnalysisAgent for cancelled commands in CodingAgentExecutor
- Skip error rendering for user-cancelled scenarios
- Preserve metadata in ToolExecutionResult.failure() for cancelled flag propagation
- Apply same fix to DocumentAgentExecutor

When user cancels a command (e.g., bootRun), the flow now:
1. Skips triggering AnalysisAgent for the cancelled output
2. Skips displaying extra 'Tool execution failed' error message
3. Only shows the concise cancellation confirmation

This eliminates unnecessary double-messaging (Summary + Error) for intentional cancellations.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/executor/CodingAgentExecutor.kt (1)

74-74: Replace emoji with plain text alternatives in commonMain code targeted at WASM.

This file is in commonMain, which compiles to all platforms including WASM. The coding guidelines explicitly state: "For WASM platform, avoid using emoji and UTF-8 in code."

Replace emojis at lines 74 and 345:

  • Line 74: Replace 🚀 CodingAgent started with Rocket CodingAgent started or CodingAgent started
  • Line 345: Replace ⏳ Process is still running with Process is still running
🧹 Nitpick comments (3)
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/executor/DocumentAgentExecutor.kt (1)

322-327: Cancellation guard is correct; consider centralizing metadata flag parsing

The early return on executionResult.metadata["cancelled"] == "true" is consistent with the existing isLiveSession check and correctly prevents unnecessary analysis for user‑cancelled commands while still preserving raw output.

To reduce duplication and make future changes to metadata representation safer, consider a small helper like:

private fun Map<String, String>.flagTrue(key: String): Boolean =
    this[key]?.toBooleanStrictOrNull() ?: false

and then use:

val isLiveSession = executionResult.metadata.flagTrue("isLiveSession")
val wasCancelledByUser = executionResult.metadata.flagTrue("cancelled")

This keeps the "true" convention in one place and hardens parsing if the producer ever emits true/false instead of "true".

mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/executor/CodingAgentExecutor.kt (2)

282-284: Consider using a constant for the metadata key and value.

The string comparison metadata["cancelled"] == "true" is repeated in the codebase (also at line 474). This pattern is fragile—a typo or case mismatch could silently break the logic.

Consider defining constants at the class or companion object level:

+    companion object {
+        private const val METADATA_KEY_CANCELLED = "cancelled"
+        private const val METADATA_VALUE_TRUE = "true"
+    }
+
     // 跳过用户取消的场景 - 用户取消是明确的意图,不需要显示额外的错误消息
-    val wasCancelledByUser = executionResult.metadata["cancelled"] == "true"
+    val wasCancelledByUser = executionResult.metadata[METADATA_KEY_CANCELLED] == METADATA_VALUE_TRUE
     if (!executionResult.isSuccess && !executionResult.isPending && !wasCancelledByUser) {

Alternatively, consider storing boolean values directly in metadata rather than string representations if the metadata map supports mixed types.


472-477: Logic is consistent and correct.

The cancellation check correctly bypasses content analysis for user-cancelled operations, which aligns with the intent expressed at line 283. The early return pattern is appropriate.

Optional: The Chinese comments (lines 282, 472-473) may hinder international collaboration. Consider translating to English for consistency with the rest of the codebase.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 039c09b and 23f2bc4.

📒 Files selected for processing (3)
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/executor/CodingAgentExecutor.kt (2 hunks)
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/executor/DocumentAgentExecutor.kt (2 hunks)
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/orchestrator/ToolExecutionResult.kt (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.kt

📄 CodeRabbit inference engine (AGENTS.md)

**/*.kt: Use expect/actual for platform-specific code (e.g., file I/O on JVM/JS/Wasm) in Kotlin Multiplatform projects
Check export first if some functions are not working well with CLI (TypeScript)
In Kotlin/JS @JsExport: Avoid Flow, use Promise instead
In Kotlin/JS @JsExport: Use concrete classes as return types and parameter types; avoid interface types
For WASM platform, avoid using emoji and UTF-8 in code

Files:

  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/executor/DocumentAgentExecutor.kt
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/executor/CodingAgentExecutor.kt
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/orchestrator/ToolExecutionResult.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Test mpp-core (KMP)
  • GitHub Check: Build (241)
  • GitHub Check: Build (223)
🔇 Additional comments (2)
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/executor/DocumentAgentExecutor.kt (1)

303-304: Doc note for cancelled commands matches implementation

The added KDoc note about skipping summarization for user‑cancelled commands accurately reflects the control flow below and clarifies behavior for future readers. No changes needed.

mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/orchestrator/ToolExecutionResult.kt (1)

89-108: Error-path metadata propagation is consistent and aligned with goals

Passing metadata into ToolResult.Error(error, metadata = metadata) while also storing it on ToolExecutionResult.metadata cleanly aligns the failure path with the success path and enables downstream checks (e.g., cancellation flags) without changing public APIs. This looks correct and self-explanatory with the added comment.

@phodal
Copy link
Member Author

phodal commented Dec 2, 2025

augment review

Copy link

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 1 suggestion posted.

Comment augment review to trigger a new review at any time.

executionTimeMs: Long,
output: String?
output: String?,
cancelledByUser: Boolean
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ComposeRenderer builds ToolResult.Error without setting an errorType, whereas JewelRenderer and the CLI set one (e.g., CANCELLED_BY_USER or COMMAND_FAILED). Consider including an errorType here to keep downstream handling consistent across renderers.

🤖 Was this useful? React with 👍 or 👎

- Fix thread-safety in ShellSessionManager.markSessionCancelledByUser() with synchronized block
- Fix condition ordering in JewelRenderer: check cancelledByUser before exitCode == 0
- Add execution_time_ms to awaitManagedSession for metadata consistency
- Extract buildCancelledMessage helper to reduce code duplication
- Simplify statusMessage logic in CodingCli (remove redundant when branches)
- Add errorType to ComposeRenderer for cancelled results
- Fix spacing in AnsiStripper ('(' , ')' -> '(', ')')
- Remove duplicate inputStream reader in PtyShellExecutor to fix data race
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt (1)

471-518: Condition ordering corrected; consider extracting shared message builder.

The when expression now correctly checks cancelledByUser first, addressing the previous review concern. However, the cancelled message building logic (lines 475-486) is duplicated across JewelRenderer, ComposeRenderer, and CodingCli. Consider extracting to a shared utility in RendererUtils or the CodingAgentRenderer interface.

mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/AnsiStripper.kt (1)

1-117: Add focused unit tests for ANSI/OSC stripping and normalization

This utility is central to making PTY output AI‑friendly and involves several subtle cases (CSI, OSC with BEL/ST, charset selects, simple ESC, malformed/unterminated sequences, CR handling, and stripAndNormalize). It deserves dedicated tests covering:

  • Typical CSI color/movement sequences.
  • OSC sequences terminated by both BEL and ESC\.
  • Charset selection escapes and simple ESC X sequences.
  • Malformed/unterminated CSI/OSC sequences.
  • Line‑ending behavior for strip vs stripAndNormalize.

This echoes the earlier review feedback and would help guard against regressions in these parsing routines.

🧹 Nitpick comments (5)
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/ShellSessionManager.kt (1)

119-123: Consider adding @Volatile to cancelledByUser for cross-thread visibility.

The synchronized block in markSessionCancelledByUser protects map access, but without @Volatile, the boolean write may not be visible to other threads reading this property (e.g., in awaitManagedSession or awaitProcess).

-    var cancelledByUser: Boolean = false
+    @Volatile var cancelledByUser: Boolean = false
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/ComposeRenderer.kt (1)

488-492: Inconsistent error type usage: string literal vs enum.

Line 489 uses a string literal "CANCELLED_BY_USER" while line 491 uses ToolErrorType.COMMAND_FAILED.code. For consistency and maintainability, consider defining CANCELLED_BY_USER as a constant or adding it to ToolErrorType enum.

-                errorType = "CANCELLED_BY_USER"
+                errorType = cc.unitmesh.agent.tool.ToolErrorType.CANCELLED_BY_USER.code

This requires adding CANCELLED_BY_USER to the ToolErrorType enum if not already present.

mpp-ui/src/jvmMain/kotlin/cc/unitmesh/server/cli/CodingCli.kt (1)

413-418: Minor duplication: status symbol logic repeated.

The status symbol computation (lines 224-228 and 413-417) is duplicated. Consider extracting to a small helper function.

private fun statusSymbol(cancelledByUser: Boolean, exitCode: Int): String = when {
    cancelledByUser -> ""
    exitCode == 0 -> ""
    else -> ""
}
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/AnsiStripper.kt (2)

21-66: ANSI stripping loop looks good; consider handling trailing lone ESC more defensively

The main loop correctly recognizes CSI/OSC/simple ESC sequences and skips them, but a lone ESC at the very end of the string currently falls through and is emitted as a raw control byte. If you prefer to treat that as a malformed escape and drop it, you can make ch == ESC unconditional and special‑case the “no next char” case:

-            when {
-                ch == ESC && i + 1 < text.length -> {
-                    val next = text[i + 1]
-                    when (next) {
-                        '[' -> {
-                            // CSI sequence: ESC[...X (ends with a letter)
-                            i = skipCsiSequence(text, i + 2)
-                        }
-                        ']' -> {
-                            // OSC sequence: ESC]...BEL or ESC]...ST
-                            i = skipOscSequence(text, i + 2)
-                        }
-                        '(', ')' -> {
-                            // Character set selection: ESC(X or ESC)X
-                            i = if (i + 2 < text.length) i + 3 else text.length
-                        }
-                        else -> {
-                            // Simple escape sequence: ESC X
-                            i += 2
-                        }
-                    }
-                }
+            when {
+                ch == ESC -> {
+                    if (i + 1 >= text.length) {
+                        // Trailing lone ESC – drop it
+                        i++
+                    } else {
+                        val next = text[i + 1]
+                        when (next) {
+                            '[' -> {
+                                // CSI sequence: ESC[...X (ends with a letter)
+                                i = skipCsiSequence(text, i + 2)
+                            }
+                            ']' -> {
+                                // OSC sequence: ESC]...BEL or ESC]...ST
+                                i = skipOscSequence(text, i + 2)
+                            }
+                            '(', ')' -> {
+                                // Character set selection: ESC(X or ESC)X
+                                i = if (i + 2 < text.length) i + 3 else text.length
+                            }
+                            else -> {
+                                // Simple escape sequence: ESC X
+                                i += 2
+                            }
+                        }
+                    }
+                }

The rest of the stripping logic looks solid for the CSI/OSC/simple ESC cases described in the KDoc.


54-57: Clarify where line-ending normalization happens (strip vs stripAndNormalize)

Right now strip already drops '\r', so by the time stripAndNormalize runs, the replace("\r\n", "\n").replace("\r", "") chain is effectively a no‑op. Either:

  • Keep CR stripping in strip and adjust the KDocs to say strip also normalizes \r, or
  • Make strip purely about ANSI removal and leave all line‑ending normalization to stripAndNormalize, e.g.:
-                ch == '\r' -> {
-                    // Carriage return - skip it (will be handled with newlines)
-                    i++
-                }
+                ch == '\r' -> {
+                    result.append(ch)
+                    i++
+                }

so that stripAndNormalize is the single place that converts \r\n\n and removes standalone \r. Either way, having a single, well‑documented layer responsible for EOL handling will make behavior easier to reason about.

Also applies to: 112-116

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 23f2bc4 and 3adf74c.

📒 Files selected for processing (6)
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/AnsiStripper.kt (1 hunks)
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/ShellSessionManager.kt (2 hunks)
  • mpp-core/src/jvmMain/kotlin/cc/unitmesh/agent/tool/shell/PtyShellExecutor.kt (2 hunks)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt (2 hunks)
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/ComposeRenderer.kt (2 hunks)
  • mpp-ui/src/jvmMain/kotlin/cc/unitmesh/server/cli/CodingCli.kt (3 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.kt

📄 CodeRabbit inference engine (AGENTS.md)

**/*.kt: Use expect/actual for platform-specific code (e.g., file I/O on JVM/JS/Wasm) in Kotlin Multiplatform projects
Check export first if some functions are not working well with CLI (TypeScript)
In Kotlin/JS @JsExport: Avoid Flow, use Promise instead
In Kotlin/JS @JsExport: Use concrete classes as return types and parameter types; avoid interface types
For WASM platform, avoid using emoji and UTF-8 in code

Files:

  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/ComposeRenderer.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/ShellSessionManager.kt
  • mpp-ui/src/jvmMain/kotlin/cc/unitmesh/server/cli/CodingCli.kt
  • mpp-core/src/jvmMain/kotlin/cc/unitmesh/agent/tool/shell/PtyShellExecutor.kt
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/AnsiStripper.kt
**/compose/**/*.{kt,kts}

📄 CodeRabbit inference engine (AGENTS.md)

In Compose (Desktop/Android), use AutoDevColors from cc.unitmesh.devins.ui.compose.theme or MaterialTheme.colorScheme

Files:

  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/ComposeRenderer.kt
{**/compose/**/*.kt,mpp-ui/src/jsMain/typescript/**/*.{ts,tsx}}

📄 CodeRabbit inference engine (AGENTS.md)

DO NOT hardcode colors (e.g., Color(0xFF...) or #hex). Always use design tokens for consistency across platforms

Files:

  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/ComposeRenderer.kt
**/idea/**/*.kt

📄 CodeRabbit inference engine (AGENTS.md)

**/idea/**/*.kt: For SwingPanel z-index issues with Compose popups, enable Jewel's custom popup renderer: JewelFlags.useCustomPopupRenderer = true in IdeaAgentToolWindowFactory
For popup/dropdown menus in IntelliJ plugins, use Jewel's PopupMenu instead of androidx.compose.ui.window.Popup
For dialogs in IntelliJ plugins, use IntelliJ's DialogWrapper with org.jetbrains.jewel.bridge.compose instead of androidx.compose.ui.window.Dialog

Files:

  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test mpp-core (KMP)
🔇 Additional comments (3)
mpp-core/src/jvmMain/kotlin/cc/unitmesh/agent/tool/shell/PtyShellExecutor.kt (1)

266-289: LGTM! Data race concern addressed.

The code correctly avoids creating a duplicate reader for the PTY input stream, relying on the UI's ProcessOutputCollector to handle output collection. The managed session sync and exit code propagation look correct.

mpp-ui/src/jvmMain/kotlin/cc/unitmesh/server/cli/CodingCli.kt (1)

336-348: Good refactor: buildCancelledMessage helper addresses duplication.

The helper function consolidates the cancelled message building logic, addressing the past review concern about code duplication. The metadata now consistently includes execution_time_ms in both awaitManagedSession and awaitProcess.

mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/AnsiStripper.kt (1)

72-83: CSI/OSC skipping helpers are bounded and handle unterminated sequences safely

skipCsiSequence terminates on @..~ with a fallback to text.length, and skipOscSequence terminates on BEL or ESC\ with a similar fallback while incrementing i on every iteration. That addresses the prior infinite‑loop risk and safely copes with truncated sequences in captured PTY output.

Also applies to: 89-105

Comment on lines 62 to 66
fun markSessionCancelledByUser(sessionId: String) {
synchronized(sessions) {
sessions[sessionId]?.cancelledByUser = true
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and examine the specific lines
find . -name "ShellSessionManager.kt" -type f

Repository: phodal/auto-dev-sketch

Length of output: 150


🏁 Script executed:

# Search for expect/actual synchronization patterns
rg -n "expect.*Mutex|actual.*Mutex|expect.*synchronized|actual.*synchronized|expect.*Lock|actual.*Lock" --type=kt

Repository: phodal/auto-dev-sketch

Length of output: 92


🏁 Script executed:

# Check for synchronized usage in commonMain
rg -n "synchronized\(" mpp-core/src/commonMain --type=kt -B 2 -A 2

Repository: phodal/auto-dev-sketch

Length of output: 92


🏁 Script executed:

# Check if there's any Mutex or synchronization utilities in the codebase
rg -n "Mutex|RwMutex|Semaphore" --type=kt | head -20

Repository: phodal/auto-dev-sketch

Length of output: 92


🏁 Script executed:

# Read the actual file to confirm the code
cat -n ./mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/ShellSessionManager.kt | sed -n '55,75p'

Repository: phodal/auto-dev-sketch

Length of output: 838


🏁 Script executed:

# Search for synchronized usage in commonMain without file type filter
rg -n "synchronized\(" mpp-core/src/commonMain -B 2 -A 2

Repository: phodal/auto-dev-sketch

Length of output: 658


🏁 Script executed:

# Search for expect/actual patterns across the entire codebase
rg -n "expect fun.*synchronized|actual fun.*synchronized|expect.*Mutex|actual.*Mutex" -B 1 -A 1

Repository: phodal/auto-dev-sketch

Length of output: 48


🏁 Script executed:

# Check for any Mutex usage in commonMain
rg -n "Mutex" mpp-core/src/commonMain -B 1 -A 1

Repository: phodal/auto-dev-sketch

Length of output: 1148


🏁 Script executed:

# Get an overview of all synchronized usage in the entire mpp-core
rg -n "synchronized\(" mpp-core

Repository: phodal/auto-dev-sketch

Length of output: 184


Use Mutex instead of synchronized() for thread-safe access in multiplatform code.

This function uses synchronized(sessions) on line 63, which is JVM-specific and will not provide synchronization guarantees on Kotlin/JS or Kotlin/Native platforms. The class already declares private val mutex = Mutex() (line 18) and uses it correctly elsewhere (e.g., line 72 in getActiveSessions()). Replace this function with a suspend variant using mutex.withLock(), or if a non-suspend function is required for UI callbacks, use expect/actual declarations to provide platform-specific implementations per the coding guidelines.

🤖 Prompt for AI Agents
In
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/shell/ShellSessionManager.kt
around lines 62-66, replace the JVM-only synchronized(sessions) block with a
multiplatform-safe approach: make markSessionCancelledByUser a suspend function
and perform the sessions modification inside mutex.withLock {
sessions[sessionId]?.cancelledByUser = true }; if you cannot make it suspend
(e.g., used by UI callbacks), instead introduce an expect declaration for
markSessionCancelledByUser in commonMain and provide platform-specific actual
implementations that use appropriate synchronization on each target (JVM uses
synchronized or Mutex, JS/Native use Mutex.withLock or platform primitives), and
update call sites accordingly.

synchronized is JVM-only. Direct access is safe because:
- JS/WASM are single-threaded
- On JVM, boolean assignment is atomic and cancelledByUser is only written once
@phodal phodal merged commit 3a8534f into master Dec 2, 2025
6 of 7 checks passed
phodal added a commit that referenced this pull request Dec 2, 2025
- Fix thread-safety in ShellSessionManager.markSessionCancelledByUser() with synchronized block
- Fix condition ordering in JewelRenderer: check cancelledByUser before exitCode == 0
- Add execution_time_ms to awaitManagedSession for metadata consistency
- Extract buildCancelledMessage helper to reduce code duplication
- Simplify statusMessage logic in CodingCli (remove redundant when branches)
- Add errorType to ComposeRenderer for cancelled results
- Fix spacing in AnsiStripper ('(' , ')' -> '(', ')')
- Remove duplicate inputStream reader in PtyShellExecutor to fix data race
@phodal phodal deleted the feat/terminal-cancel-with-output-capture branch December 2, 2025 10:30
phodal added a commit that referenced this pull request Dec 2, 2025
- Fix thread-safety in ShellSessionManager.markSessionCancelledByUser() with synchronized block
- Fix condition ordering in JewelRenderer: check cancelledByUser before exitCode == 0
- Add execution_time_ms to awaitManagedSession for metadata consistency
- Extract buildCancelledMessage helper to reduce code duplication
- Simplify statusMessage logic in CodingCli (remove redundant when branches)
- Add errorType to ComposeRenderer for cancelled results
- Fix spacing in AnsiStripper ('(' , ')' -> '(', ')')
- Remove duplicate inputStream reader in PtyShellExecutor to fix data race
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants