Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions docs/specs/toolcall-permission-flow/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Tool Call Rendering + Permission Flow - Implementation Plan

## Architecture Decisions

### Monaco-Based Code Blocks
- Replace `JsonObject` component and xterm terminal with `useMonaco` from `stream-monaco`
- Follow pattern from `CodeArtifact.vue` and `TraceDialog.vue`
- Lazy initialization: create editor only when `isExpanded === true`, dispose when collapsed
- Separate editors for params and response to allow independent updates

### Language Detection Strategy
- **Params**: Always use `json` language (raw string display)
- **Response**:
1. Try `JSON.parse(response)` → valid? use `json`
2. Fallback to `plaintext`
- **Terminal tools**:
- Detection: name/server_name contains `terminal|command|exec` (excluding `run_shell_command` from `powerpack`)
- Language: `powershell` if Windows, otherwise `shell`/`bash`

### Permission Block Lifecycle
- **Main layer** (`ToolCallHandler`, `PermissionHandler`):
- Permission block created with `action_type: 'tool_call_permission'` when permission required
- On `permission-granted`/`permission-denied` events:
- Remove permission block from `state.message.content`
- Find and update corresponding `tool_call` block by `tool_call.id`
- Persist updated content via `messageManager.editMessage()`
- **Renderer layer**:
- Remove `MessageBlockPermissionRequest` rendering for resolved permissions
- Only render tool_call blocks in final message content

### Device Platform Caching
- Store platform detection in `upgrade.ts` store to avoid repeated `devicePresenter.getDeviceInfo()` calls
- Add `isWindows` computed property based on `platform === 'win32'`
- Call `devicePresenter.getDeviceInfo()` once on store initialization

### Think-Content Logging
- Add `console.log` statements at key points:
- Block creation/updates in `chat.ts` store
- Type/content changes in `MessageBlockThink.vue` and `ThinkContent.vue`
- Focus on transient state during streaming
- Keep logs minimal and debug-focused

## Event Flow

### Permission Resolution Flow
```
User grants/denies permission
→ MessageBlockPermissionRequest.vue calls agentPresenter.handlePermissionResponse()
→ PermissionHandler.handlePermissionResponse()
→ Update permission block status (granted/denied)
→ Remove permission block from content
→ Update tool_call block by tool_call_id
→ messageManager.editMessage() to persist
→ Trigger resume of agent loop
```

### Tool Call Rendering Flow
```
Stream event: tool_call=start
→ Chat store creates tool_call block (status: loading)
→ MessageBlockToolCall.vue renders collapsed view

User expands block
→ isExpanded becomes true
→ Monaco editors created for params/response
→ Code displayed with syntax highlighting

User collapses block
→ isExpanded becomes false
→ Monaco editors disposed
→ No code rendering (performance optimization)
```

## Data Model Changes

### AssistantMessageBlock
- No type changes
- `action_type: 'tool_call_permission'` blocks removed from final content

### Upgrade Store
- Add `isWindows` computed property
- Cache device platform info once

## IPC Surface
- No new IPC channels needed
- Reuse existing `agentPresenter.handlePermissionResponse`

## Test Strategy

### Unit Tests
- Test permission block removal logic in `PermissionHandler`
- Test tool_call block update by ID
- Test language detection logic (json/plaintext/powershell/shell)
- Test device platform caching

### Integration Tests
- Test full permission flow (request → grant → block removal → tool_call update)
- Test Monaco editor lifecycle (create on expand, dispose on collapse)
- Test think-content state logging captures transient changes

### Manual Testing
- Verify Monaco code blocks render correctly for complex params/responses
- Verify copy buttons work for params and response
- Verify permission blocks disappear after resolution
- Verify Windows vs Linux terminal language detection
- Verify no performance degradation with many collapsed tool calls
- Verify console logs help debug think-content state toggling

## Risks and Mitigations

### Performance Risk
- **Risk**: Creating too many Monaco editors with many expanded blocks
- **Mitigation**: Lazy initialization and disposal when collapsed; limit max height with `max-h-64`

### Data Integrity Risk
- **Risk**: Removing permission blocks could break historical message rendering
- **Mitigation**: Only affects new/active messages; old messages with permission blocks remain readable (won't occur with new flow)

### Platform Detection Race Condition
- **Risk**: Device info not loaded when tool call renders
- **Mitigation**: Initialize device info in upgrade store early; provide fallback to `shell` if not ready
41 changes: 41 additions & 0 deletions docs/specs/toolcall-permission-flow/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Tool Call Rendering + Permission Flow

## User Stories
- As a user, I want tool call params and responses rendered as raw code blocks without complex JSON parsing, making increasingly complex payloads easier to read and consistent with other code displays.
- As a user, I want permission request blocks to appear only while unresolved and disappear once resolved, leaving a single tool call block as the final record.
- As a user, I want tool call code blocks to be lazy-rendered so expanding a block doesn't cause unnecessary performance costs.
- As a developer, I want clearer logging around think-content state changes to debug transient UI toggling.

## Business Value
- Reduces UI complexity and parsing errors for increasingly complex tool call payloads.
- Improves UX clarity by preventing stale permission request blocks from persisting in message history.
- Avoids unnecessary render work for collapsed tool call blocks, improving performance.
- Better debugging capability for think-content state issues.

## Scope
- Tool call message block rendering in renderer
- Permission-request block lifecycle and its persistence in message content
- Basic logging for think-content state transitions
- Device platform detection for PowerShell vs shell language selection

## Non-Goals
- No new permission UX design changes
- No new backend policy logic
- No refactor of unrelated message block types
- No permission request migration from old data (only affects new messages)

## Acceptance Criteria
- Tool call params and responses render via Monaco-based code blocks, without JsonObject parsing or field extraction
- Code block rendering is lazy: no Monaco editor is created until tool call block is expanded; editors are disposed when collapsed
- Language selection rules:
- Params always use JSON (raw string shown)
- Responses prefer JSON, then fallback to plaintext
- Terminal-like tools render as shell/bash; Windows uses PowerShell based on device info
- Copy buttons are preserved for both params and responses
- Permission request block is persisted only while unresolved; once resolved, it is removed from stored content and tool call block is updated by ID
- Think-content logs include enough context to trace type/content changes during streaming
- Device platform is cached once in upgrade store and reused for language detection
- Final stored messages contain only tool_call blocks (no action type permission blocks)

## Open Questions
- None
93 changes: 93 additions & 0 deletions docs/specs/toolcall-permission-flow/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Tool Call Rendering + Permission Flow - Tasks

## Task 1: Add Device Platform Caching to Upgrade Store
- **File**: `src/renderer/src/stores/upgrade.ts`
- **Changes**:
- Import `usePresenter` for `devicePresenter`
- Add `isWindows` computed property
- Fetch device info once on store initialization
- Cache platform value
- **Acceptance**: `upgradeStore.isWindows` returns correct boolean based on `platform === 'win32'`

## Task 2: Refactor MessageBlockToolCall.vue to Use Monaco
- **File**: `src/renderer/src/components/message/MessageBlockToolCall.vue`
- **Changes**:
- Remove `JsonObject` and `Terminal` imports
- Add `useMonaco` import from `stream-monaco`
- Replace params/response rendering with Monaco code blocks
- Implement lazy initialization (create only when `isExpanded === true`)
- Dispose editors when collapsed
- Add copy buttons for params and response
- Implement language detection (json, plaintext, shell/bash, powershell)
- Use `upgradeStore.isWindows` for PowerShell detection
- **Acceptance**:
- Params always render as JSON code block
- Responses render as JSON if valid JSON, else plaintext
- Terminal tools use shell/bash (Linux/Mac) or powershell (Windows)
- Editors created only on expand, disposed on collapse
- Copy buttons work correctly

## Task 3: Remove Permission Block Rendering in MessageItemAssistant.vue
- **File**: `src/renderer/src/components/message/MessageItemAssistant.vue`
- **Changes**:
- Remove `MessageBlockPermissionRequest` component usage
- Remove permission block condition from template
- **Acceptance**: Permission blocks no longer render in assistant messages

## Task 4: Add Permission Block Removal in PermissionHandler
- **File**: `src/main/presenter/agentPresenter/permission/permissionHandler.ts`
- **Changes**:
- In `handlePermissionResponse()`, after updating permission block status:
- Remove permission block from `content` array
- Find corresponding tool_call block by `tool_call.id`
- Update tool_call block with result
- Persist via `messageManager.editMessage()`
- Apply same logic to `generatingState.message.content` if present
- **Acceptance**: Permission blocks removed from message content after resolution; tool_call blocks updated

## Task 5: Add Logging for Think-Content State Changes
- **File**: `src/renderer/src/components/message/MessageBlockThink.vue`
- **Changes**:
- Add `console.log` in watch for `block.status` and `block.reasoning_time`
- Log block type, content length, and status changes
- **File**: `src/renderer/src/components/think-content/ThinkContent.vue`
- **Changes**:
- Add `console.log` for props changes (label, expanded, thinking, content)
- Log state transitions
- **File**: `src/renderer/src/stores/chat.ts`
- **Changes**:
- Add `console.log` when `reasoning_content` blocks are created or updated
- Log block type and content changes during streaming
- **Acceptance**: Console logs capture all state transitions for think-content blocks

## Task 6: Update Tests for Permission Block Removal
- **File**: `test/main/presenter/sessionPresenter/permissionHandler.test.ts` (if exists)
- **Changes**:
- Add tests for permission block removal logic
- Verify tool_call block is updated correctly
- **File**: `test/renderer/message/messageBlockSnapshot.test.ts`
- **Changes**:
- Update snapshots to reflect removed permission blocks
- Ensure final messages contain only tool_call blocks
- **Acceptance**: Tests pass with new permission block behavior

## Task 7: Format and Typecheck
- Run: `pnpm run format`
- Run: `pnpm run lint`
- Run: `pnpm run typecheck`
- **Acceptance**: All commands pass without errors

## Task 8: Manual Testing
- Test permission flow with grant and deny actions
- Verify permission blocks disappear after resolution
- Verify tool_call blocks show updated status
- Test Monaco code block rendering for various tool calls
- Test copy functionality for params and responses
- Test think-content with logs visible in console
- Test on Windows and Linux/Mac for terminal language detection
- **Acceptance**: All manual tests pass; UI behaves as expected

## Task 9: Update SDD Documentation (Optional)
- Update `docs/specs/toolcall-permission-flow/spec.md` if acceptance criteria change
- Update `docs/specs/toolcall-permission-flow/plan.md` if architecture changes
- **Acceptance**: Documentation reflects implementation reality
24 changes: 23 additions & 1 deletion src/main/presenter/agentPresenter/loop/toolCallHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,29 @@ export class ToolCallHandler {
state.pendingToolCall = undefined
}

async processToolCallError(
state: GeneratingMessageState,
event: LLMAgentEventData
): Promise<void> {
const toolCallBlock = state.message.content.find(
(block) =>
block.type === 'tool_call' &&
block.tool_call?.id === event.tool_call_id &&
block.status === 'loading'
)

if (toolCallBlock && toolCallBlock.type === 'tool_call') {
toolCallBlock.status = 'error'
if (toolCallBlock.tool_call) {
toolCallBlock.tool_call.response = event.tool_call_response || ''
}
}

this.searchingMessages.delete(event.eventId)
state.isSearching = false
state.pendingToolCall = undefined
}

async processToolCallPermission(
state: GeneratingMessageState,
event: LLMAgentEventData,
Expand Down Expand Up @@ -414,7 +437,6 @@ export class ToolCallHandler {

const lastBlock = state.message.content[state.message.content.length - 1]
if (lastBlock && lastBlock.type === 'tool_call' && lastBlock.tool_call) {
lastBlock.status = 'success'
}

this.finalizeLastBlock(state)
Expand Down
Loading