fix: critical permission modal and config issues #127

Merged
naomi merged 19 commits from feat/many into main 2026-02-07 01:55:50 -08:00
9 changed files with 392 additions and 154 deletions
Showing only changes of commit 82061f125b - Show all commits
+17
View File
@@ -3,16 +3,33 @@
## Repository Information ## Repository Information
This project is hosted on both GitHub and Gitea: This project is hosted on both GitHub and Gitea:
- **GitHub**: `naomi-lgbt/hikari-desktop` (public mirror) - **GitHub**: `naomi-lgbt/hikari-desktop` (public mirror)
- **Gitea**: `nhcarrigan/hikari-desktop` (primary development) - **Gitea**: `nhcarrigan/hikari-desktop` (primary development)
## MCP Server Usage ## MCP Server Usage
When working with issues, pull requests, or other repository operations for this project: When working with issues, pull requests, or other repository operations for this project:
- **Use `gitea-hikari` MCP server** - This allows Hikari to act as herself - **Use `gitea-hikari` MCP server** - This allows Hikari to act as herself
- **Target repository**: `nhcarrigan/hikari-desktop` - **Target repository**: `nhcarrigan/hikari-desktop`
- **Gitea instance**: `git.nhcarrigan.com` - **Gitea instance**: `git.nhcarrigan.com`
## Git Commits
When asked to commit changes for this project:
- **Always commit as Hikari** using: `--author="Hikari <hikari@nhcarrigan.com>"`
- **Always use `--no-gpg-sign`** since Hikari doesn't have GPG signing set up
- **Never add `Co-Authored-By` lines** for Gitea commits
- **Always ask for confirmation** before committing
Example commit command:
```bash
git commit --author="Hikari <hikari@nhcarrigan.com>" --no-gpg-sign -m "your commit message"
```
## Project Context ## Project Context
Hikari Desktop is a Tauri-based desktop application that wraps Claude Code with a visual anime character (Hikari) who appears on screen. This is a personal project where Hikari can sign her work and act as herself! Hikari Desktop is a Tauri-based desktop application that wraps Claude Code with a visual anime character (Hikari) who appears on screen. This is a personal project where Hikari can sign her work and act as herself!
+4
View File
@@ -1,5 +1,9 @@
#!/bin/bash #!/bin/bash
# Source nvm to get access to pnpm
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
# Colors for output # Colors for output
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
+6 -1
View File
@@ -202,11 +202,16 @@ pub struct OutputEvent {
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionPromptEvent { pub struct PermissionPromptEventItem {
pub id: String, pub id: String,
pub tool_name: String, pub tool_name: String,
pub tool_input: serde_json::Value, pub tool_input: serde_json::Value,
pub description: String, pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionPromptEvent {
pub permissions: Vec<PermissionPromptEventItem>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub conversation_id: Option<String>, pub conversation_id: Option<String>,
} }
+91 -11
View File
@@ -16,9 +16,24 @@ use crate::stats::{calculate_cost, StatsUpdateEvent, UsageStats};
use crate::types::{ use crate::types::{
AgentEndEvent, AgentStartEvent, CharacterState, ClaudeMessage, ConnectionEvent, AgentEndEvent, AgentStartEvent, CharacterState, ClaudeMessage, ConnectionEvent,
ConnectionStatus, ContentBlock, MessageCost, OutputEvent, PermissionPromptEvent, ConnectionStatus, ContentBlock, MessageCost, OutputEvent, PermissionPromptEvent,
QuestionOption, SessionEvent, StateChangeEvent, UserQuestionEvent, WorkingDirectoryEvent, PermissionPromptEventItem, QuestionOption, SessionEvent, StateChangeEvent,
UserQuestionEvent, WorkingDirectoryEvent,
}; };
use parking_lot::RwLock; use parking_lot::RwLock;
use std::cell::RefCell;
thread_local! {
/// Stores pending tool uses from the most recent Assistant message
/// to enable batching permission requests for sibling cancelled tools
static PENDING_TOOL_USES: RefCell<Vec<PendingToolUse>> = const { RefCell::new(Vec::new()) };
}
#[derive(Debug, Clone)]
struct PendingToolUse {
tool_use_id: String,
tool_name: String,
tool_input: serde_json::Value,
}
const SEARCH_TOOLS: [&str; 5] = ["Read", "Glob", "Grep", "WebSearch", "WebFetch"]; const SEARCH_TOOLS: [&str; 5] = ["Read", "Glob", "Grep", "WebSearch", "WebFetch"];
const CODING_TOOLS: [&str; 3] = ["Edit", "Write", "NotebookEdit"]; const CODING_TOOLS: [&str; 3] = ["Edit", "Write", "NotebookEdit"];
@@ -770,6 +785,26 @@ fn process_json_line(
}) })
.collect(); .collect();
// Store pending tool uses for permission batching (only for top-level, not subagents)
if parent_tool_use_id.is_none() {
PENDING_TOOL_USES.with(|pending| {
let tool_uses: Vec<PendingToolUse> = message
.content
.iter()
.filter_map(|block| match block {
ContentBlock::ToolUse { id, name, input } => Some(PendingToolUse {
tool_use_id: id.clone(),
tool_name: name.clone(),
tool_input: input.clone(),
}),
_ => None,
})
.collect();
// Append to existing pending tools instead of replacing
pending.borrow_mut().extend(tool_uses);
});
}
// Track message cost for display // Track message cost for display
let mut message_cost: Option<MessageCost> = None; let mut message_cost: Option<MessageCost> = None;
@@ -1034,6 +1069,15 @@ fn process_json_line(
CharacterState::Error CharacterState::Error
}; };
// Capture pending tool uses before clearing them
// We'll use these for permission batching if there are denials
let captured_pending_tools = PENDING_TOOL_USES.with(|pending| {
let tools = pending.borrow().clone();
// Clear immediately so they don't accumulate across requests
pending.borrow_mut().clear();
tools
});
// Log turn metrics if available // Log turn metrics if available
if let Some(duration) = duration_ms { if let Some(duration) = duration_ms {
println!("Turn completed in {}ms", duration); println!("Turn completed in {}ms", duration);
@@ -1172,7 +1216,14 @@ fn process_json_line(
// Check for permission denials and emit prompts for each // Check for permission denials and emit prompts for each
if let Some(denials) = permission_denials { if let Some(denials) = permission_denials {
let mut has_regular_denials = false; // Only process if there are actually denials
if !denials.is_empty() {
let mut regular_permission_requests = Vec::new();
// Get denied tool IDs for later comparison
let denied_tool_ids: Vec<String> = denials.iter()
.map(|d| d.tool_use_id.clone())
.collect();
for denial in denials { for denial in denials {
// Special handling for AskUserQuestion tool // Special handling for AskUserQuestion tool
@@ -1235,24 +1286,41 @@ fn process_json_line(
} }
} }
} else { } else {
has_regular_denials = true;
let description = let description =
format_tool_description(&denial.tool_name, &denial.tool_input); format_tool_description(&denial.tool_name, &denial.tool_input);
let _ = app.emit( regular_permission_requests.push(PermissionPromptEventItem {
"claude:permission",
PermissionPromptEvent {
id: denial.tool_use_id.clone(), id: denial.tool_use_id.clone(),
tool_name: denial.tool_name.clone(), tool_name: denial.tool_name.clone(),
tool_input: denial.tool_input.clone(), tool_input: denial.tool_input.clone(),
description, description,
conversation_id: conversation_id.clone(), });
},
);
} }
} }
// Show permission state if there were any denials (questions or regular) // Check for sibling tools that may have been cancelled
if has_regular_denials || !denials.is_empty() { // Add them to the permission batch so they can be approved together
for tool_use in captured_pending_tools.iter() {
// Only add tools that weren't explicitly denied (these are likely cancelled siblings)
if !denied_tool_ids.contains(&tool_use.tool_use_id) {
let description = format_tool_description(&tool_use.tool_name, &tool_use.tool_input);
regular_permission_requests.push(PermissionPromptEventItem {
id: tool_use.tool_use_id.clone(),
tool_name: tool_use.tool_name.clone(),
tool_input: tool_use.tool_input.clone(),
description,
});
}
}
// Emit all regular permission requests as a single batched event
if !regular_permission_requests.is_empty() {
let _ = app.emit(
"claude:permission",
PermissionPromptEvent {
permissions: regular_permission_requests,
conversation_id: conversation_id.clone(),
},
);
emit_state_change( emit_state_change(
app, app,
CharacterState::Permission, CharacterState::Permission,
@@ -1261,7 +1329,19 @@ fn process_json_line(
); );
return Ok(()); return Ok(());
} }
// Show permission state if there were any question denials
if !denials.is_empty() {
emit_state_change(
app,
CharacterState::Permission,
None,
conversation_id.clone(),
);
return Ok(());
} }
} // end of else block for non-empty denials
} // end of if let Some(denials)
emit_state_change(app, state, None, conversation_id.clone()); emit_state_change(app, state, None, conversation_id.clone());
} }
+141 -48
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { SvelteSet } from "svelte/reactivity";
import { claudeStore } from "$lib/stores/claude"; import { claudeStore } from "$lib/stores/claude";
import { characterState } from "$lib/stores/character"; import { characterState } from "$lib/stores/character";
import type { PermissionRequest } from "$lib/types/messages"; import type { PermissionRequest } from "$lib/types/messages";
@@ -8,13 +9,16 @@
import { conversationsStore } from "$lib/stores/conversations"; import { conversationsStore } from "$lib/stores/conversations";
import { configStore } from "$lib/stores/config"; import { configStore } from "$lib/stores/config";
let permission: PermissionRequest | null = $state(null); let permissions: PermissionRequest[] = $state([]);
let selectedPermissions = new SvelteSet<string>();
let grantedToolsList: string[] = $state([]); let grantedToolsList: string[] = $state([]);
let workingDirectory = $state(""); let workingDirectory = $state("");
claudeStore.pendingPermission.subscribe((perm) => { conversationsStore.pendingPermissions.subscribe((perms) => {
permission = perm; permissions = perms;
if (perm) { // When new permissions arrive, select all by default
if (perms.length > 0) {
selectedPermissions = new SvelteSet(perms.map((p) => p.id));
characterState.setState("permission"); characterState.setState("permission");
} }
}); });
@@ -28,17 +32,33 @@
}); });
async function handleApproveAndReconnect() { async function handleApproveAndReconnect() {
if (permission) { const selectedPerms = permissions.filter((p) => selectedPermissions.has(p.id));
if (selectedPerms.length === 0) {
claudeStore.addLine("system", "No permissions selected to approve");
claudeStore.clearPermission();
characterState.setTemporaryState("idle", 1000);
return;
}
// Capture conversation history before clearing/reconnecting // Capture conversation history before clearing/reconnecting
const conversationHistory = claudeStore.getConversationHistory(); const conversationHistory = claudeStore.getConversationHistory();
const approvedTool = permission.tool;
const toolInput = permission.input;
claudeStore.grantTool(approvedTool); // Grant all selected tools
const newGrantedTools = [...grantedToolsList, approvedTool]; const newlyGrantedTools: string[] = [];
for (const perm of selectedPerms) {
if (!grantedToolsList.includes(perm.tool)) {
claudeStore.grantTool(perm.tool);
newlyGrantedTools.push(perm.tool);
}
}
const newGrantedTools = [...grantedToolsList, ...newlyGrantedTools];
const toolNames = selectedPerms.map((p) => p.tool).join(", ");
claudeStore.addLine( claudeStore.addLine(
"system", "system",
`Permission granted for: ${approvedTool}. Reconnecting with context...` `Permission granted for ${selectedPerms.length} tool(s): ${toolNames}. Reconnecting with context...`
); );
claudeStore.clearPermission(); claudeStore.clearPermission();
@@ -85,28 +105,31 @@
// Send conversation context to restore state // Send conversation context to restore state
if (conversationHistory) { if (conversationHistory) {
const blockedActions = selectedPerms
.map((p) => `- ${p.tool} with input:\n${JSON.stringify(p.input, null, 2)}`)
.join("\n\n");
const contextMessage = `[CONTEXT RESTORATION] const contextMessage = `[CONTEXT RESTORATION]
I just granted you permission to use the ${approvedTool} tool. Here's our conversation so far: I just granted you permission to use ${selectedPerms.length} tool(s): ${toolNames}. Here's our conversation so far:
${conversationHistory} ${conversationHistory}
The last action that was blocked was: ${approvedTool} with input: The actions that were blocked:
${JSON.stringify(toolInput, null, 2)} ${blockedActions}
Please continue where we left off and retry that action now that you have permission.`; Please continue where we left off and retry those actions now that you have permission.`;
await invoke("send_prompt", { await invoke("send_prompt", {
conversationId, conversationId,
message: contextMessage, message: contextMessage,
}); });
} }
characterState.setTemporaryState("success", 2000);
} catch (error) { } catch (error) {
console.error("Failed to reconnect:", error); console.error("Failed to reconnect:", error);
claudeStore.addLine("error", `Reconnect failed: ${error}`); claudeStore.addLine("error", `Reconnect failed: ${error}`);
} }
} }
characterState.setTemporaryState("success", 2000);
}
function handleDismiss() { function handleDismiss() {
claudeStore.clearPermission(); claudeStore.clearPermission();
@@ -126,8 +149,24 @@ Please continue where we left off and retry that action now that you have permis
return grantedToolsList.includes(toolName); return grantedToolsList.includes(toolName);
} }
function togglePermission(toolRequestId: string) {
if (selectedPermissions.has(toolRequestId)) {
selectedPermissions.delete(toolRequestId);
} else {
selectedPermissions.add(toolRequestId);
}
}
function selectAll() {
selectedPermissions = new SvelteSet(permissions.map((p) => p.id));
}
function selectNone() {
selectedPermissions = new SvelteSet();
}
function handleKeydown(event: KeyboardEvent) { function handleKeydown(event: KeyboardEvent) {
if (!permission) return; if (permissions.length === 0) return;
if (event.key === "Enter") { if (event.key === "Enter") {
event.preventDefault(); event.preventDefault();
@@ -141,72 +180,126 @@ Please continue where we left off and retry that action now that you have permis
<svelte:window onkeydown={handleKeydown} /> <svelte:window onkeydown={handleKeydown} />
{#if permission} {#if permissions.length > 0}
<div <div
class="permission-overlay fixed inset-0 bg-black/70 flex items-center justify-center z-50 backdrop-blur-sm" class="permission-overlay fixed inset-0 bg-black/70 flex items-center justify-center z-50 backdrop-blur-sm"
> >
<div <div
class="permission-modal bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-xl p-6 max-w-md w-full mx-4 shadow-2xl" class="permission-modal bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-xl p-6 max-w-2xl w-full mx-4 shadow-2xl max-h-[90vh] overflow-y-auto"
> >
<div class="flex items-center gap-3 mb-4"> <div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-full bg-yellow-500/20 flex items-center justify-center"> <div class="w-10 h-10 rounded-full bg-yellow-500/20 flex items-center justify-center">
<span class="text-xl">🔐</span> <span class="text-xl">🔐</span>
</div> </div>
<div> <div class="flex-1">
<h2 class="text-lg font-semibold text-[var(--text-primary)]">Permission Blocked</h2> <h2 class="text-lg font-semibold text-[var(--text-primary)]">
<p class="text-sm text-[var(--text-secondary)]">Hikari tried to use a restricted tool</p> {permissions.length === 1
? "Permission Required"
: `${permissions.length} Permissions Required`}
</h2>
<p class="text-sm text-[var(--text-secondary)]">
Hikari tried to use {permissions.length === 1
? "a restricted tool"
: "restricted tools"}
</p>
</div>
<div class="flex gap-2 text-xs">
<button
onclick={selectAll}
class="px-2 py-1 bg-blue-500/20 hover:bg-blue-500/30 text-blue-400 rounded transition-colors"
>
Select All
</button>
<button
onclick={selectNone}
class="px-2 py-1 bg-gray-500/20 hover:bg-gray-500/30 text-[var(--text-secondary)] rounded transition-colors"
>
Select None
</button>
</div> </div>
</div> </div>
<div class="mb-4 px-3 py-2 bg-amber-500/10 border border-amber-500/30 rounded-md"> <div class="mb-4 px-3 py-2 bg-amber-500/10 border border-amber-500/30 rounded-md">
<p class="text-sm text-amber-300"> <p class="text-sm text-amber-300">
This action was automatically blocked. Approve to allow this tool for future requests. {permissions.length === 1
? "This action was automatically blocked. Select which permissions to grant."
: "These actions were automatically blocked. Select which permissions to grant."}
</p> </p>
</div> </div>
<div class="mb-4"> <div class="space-y-3 mb-6">
<div class="text-sm text-[var(--text-secondary)] mb-1">Tool</div> {#each permissions as perm (perm.id)}
<div <div
class="px-3 py-2 bg-[var(--bg-secondary)] rounded-md text-[var(--accent-primary)] font-mono flex items-center justify-between" class="border border-[var(--border-color)] rounded-lg p-4 cursor-pointer transition-colors {selectedPermissions.has(
> perm.id
<span>{permission.tool}</span> )
{#if isToolAlreadyGranted(permission.tool)} ? 'bg-green-500/10 border-green-500/30'
<span class="text-xs text-green-400 bg-green-500/20 px-2 py-0.5 rounded" : 'bg-[var(--bg-secondary)] hover:bg-[var(--bg-secondary)]/80'}"
>Already Granted</span role="button"
tabindex="0"
onclick={() => togglePermission(perm.id)}
onkeydown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
togglePermission(perm.id);
}
}}
> >
<div class="flex items-start gap-3">
<div class="mt-1">
<input
type="checkbox"
checked={selectedPermissions.has(perm.id)}
onchange={() => togglePermission(perm.id)}
class="w-4 h-4 accent-green-500"
/>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-2">
<span class="text-[var(--accent-primary)] font-mono text-sm font-medium">
{perm.tool}
</span>
{#if isToolAlreadyGranted(perm.tool)}
<span class="text-xs text-green-400 bg-green-500/20 px-2 py-0.5 rounded">
Already Granted
</span>
{/if} {/if}
</div> </div>
<div class="text-sm text-[var(--text-secondary)] mb-2">
{perm.description}
</div> </div>
{#if Object.keys(perm.input).length > 0}
<div class="mb-4"> <details class="text-xs">
<div class="text-sm text-[var(--text-secondary)] mb-1">Description</div> <summary
<div class="px-3 py-2 bg-[var(--bg-secondary)] rounded-md text-[var(--text-primary)]"> class="cursor-pointer text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
{permission.description} >
</div> View details
</div> </summary>
{#if Object.keys(permission.input).length > 0}
<div class="mb-6">
<div class="text-sm text-[var(--text-secondary)] mb-1">Details</div>
<pre <pre
class="px-3 py-2 bg-[var(--bg-terminal)] rounded-md text-[var(--text-primary)] text-xs overflow-x-auto max-h-32">{formatInput( class="mt-2 px-3 py-2 bg-[var(--bg-terminal)] rounded-md text-[var(--text-primary)] overflow-x-auto max-h-32">{formatInput(
permission.input perm.input
)}</pre> )}</pre>
</div> </details>
{/if} {/if}
</div>
</div>
</div>
{/each}
</div>
<div class="flex gap-3"> <div class="flex gap-3">
<button <button
onclick={handleDismiss} onclick={handleDismiss}
class="flex-1 px-4 py-2 bg-gray-500/20 hover:bg-gray-500/30 text-[var(--text-secondary)] rounded-lg transition-colors font-medium" class="flex-1 px-4 py-2 bg-gray-500/20 hover:bg-gray-500/30 text-[var(--text-secondary)] rounded-lg transition-colors font-medium"
> >
Dismiss Dismiss All
</button> </button>
<button <button
onclick={handleApproveAndReconnect} onclick={handleApproveAndReconnect}
class="flex-1 px-4 py-2 bg-green-500/20 hover:bg-green-500/30 text-green-400 rounded-lg transition-colors font-medium" disabled={selectedPermissions.size === 0}
class="flex-1 px-4 py-2 bg-green-500/20 hover:bg-green-500/30 text-green-400 rounded-lg transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
> >
Allow & Reconnect Approve Selected ({selectedPermissions.size})
</button> </button>
</div> </div>
</div> </div>
+4 -1
View File
@@ -101,7 +101,10 @@ export const claudeStore = {
export const hasPermissionPending = derived( export const hasPermissionPending = derived(
claudeStore.activeConversation, claudeStore.activeConversation,
($conversation) => $conversation?.pendingPermission !== null ($conversation) =>
$conversation?.pendingPermissions !== null &&
$conversation?.pendingPermissions !== undefined &&
$conversation.pendingPermissions.length > 0
); );
export const hasQuestionPending = derived( export const hasQuestionPending = derived(
+35 -7
View File
@@ -28,7 +28,7 @@ export interface Conversation {
characterState: CharacterState; characterState: CharacterState;
isProcessing: boolean; isProcessing: boolean;
grantedTools: Set<string>; grantedTools: Set<string>;
pendingPermission: PermissionRequest | null; pendingPermissions: PermissionRequest[];
pendingQuestion: UserQuestionEvent | null; pendingQuestion: UserQuestionEvent | null;
scrollPosition: number; scrollPosition: number;
createdAt: Date; createdAt: Date;
@@ -66,7 +66,7 @@ function createConversationsStore() {
characterState: "idle", characterState: "idle",
isProcessing: false, isProcessing: false,
grantedTools: new Set(), grantedTools: new Set(),
pendingPermission: null, pendingPermissions: [],
pendingQuestion: null, pendingQuestion: null,
scrollPosition: -1, // -1 means "scroll to bottom" (auto-scroll) scrollPosition: -1, // -1 means "scroll to bottom" (auto-scroll)
createdAt: new Date(), createdAt: new Date(),
@@ -120,7 +120,11 @@ function createConversationsStore() {
); );
const pendingPermission = derived( const pendingPermission = derived(
activeConversation, activeConversation,
($conv) => $conv?.pendingPermission || null ($conv) => $conv?.pendingPermissions[0] || null
);
const pendingPermissions = derived(
activeConversation,
($conv) => $conv?.pendingPermissions || []
); );
const pendingQuestion = derived(activeConversation, ($conv) => $conv?.pendingQuestion || null); const pendingQuestion = derived(activeConversation, ($conv) => $conv?.pendingQuestion || null);
const scrollPosition = derived(activeConversation, ($conv) => $conv?.scrollPosition ?? -1); const scrollPosition = derived(activeConversation, ($conv) => $conv?.scrollPosition ?? -1);
@@ -133,6 +137,7 @@ function createConversationsStore() {
currentWorkingDirectory: { subscribe: currentWorkingDirectory.subscribe }, currentWorkingDirectory: { subscribe: currentWorkingDirectory.subscribe },
terminalLines: { subscribe: terminalLines.subscribe }, terminalLines: { subscribe: terminalLines.subscribe },
pendingPermission: { subscribe: pendingPermission.subscribe }, pendingPermission: { subscribe: pendingPermission.subscribe },
pendingPermissions: { subscribe: pendingPermissions.subscribe },
pendingQuestion: { subscribe: pendingQuestion.subscribe }, pendingQuestion: { subscribe: pendingQuestion.subscribe },
isProcessing: { subscribe: isProcessing.subscribe }, isProcessing: { subscribe: isProcessing.subscribe },
grantedTools: { subscribe: grantedTools.subscribe }, grantedTools: { subscribe: grantedTools.subscribe },
@@ -190,7 +195,7 @@ function createConversationsStore() {
conversations.update((convs) => { conversations.update((convs) => {
const conv = convs.get(activeId); const conv = convs.get(activeId);
if (conv) { if (conv) {
conv.pendingPermission = request; conv.pendingPermissions.push(request);
conv.lastActivityAt = new Date(); conv.lastActivityAt = new Date();
} }
return convs; return convs;
@@ -203,7 +208,7 @@ function createConversationsStore() {
conversations.update((convs) => { conversations.update((convs) => {
const conv = convs.get(activeId); const conv = convs.get(activeId);
if (conv) { if (conv) {
conv.pendingPermission = null; conv.pendingPermissions = [];
conv.lastActivityAt = new Date(); conv.lastActivityAt = new Date();
} }
return convs; return convs;
@@ -213,7 +218,7 @@ function createConversationsStore() {
conversations.update((convs) => { conversations.update((convs) => {
const conv = convs.get(conversationId); const conv = convs.get(conversationId);
if (conv) { if (conv) {
conv.pendingPermission = request; conv.pendingPermissions.push(request);
conv.lastActivityAt = new Date(); conv.lastActivityAt = new Date();
} }
return convs; return convs;
@@ -223,7 +228,30 @@ function createConversationsStore() {
conversations.update((convs) => { conversations.update((convs) => {
const conv = convs.get(conversationId); const conv = convs.get(conversationId);
if (conv) { if (conv) {
conv.pendingPermission = null; conv.pendingPermissions = [];
conv.lastActivityAt = new Date();
}
return convs;
});
},
removePermission: (id: string) => {
const activeId = get(activeConversationId);
if (!activeId) return;
conversations.update((convs) => {
const conv = convs.get(activeId);
if (conv) {
conv.pendingPermissions = conv.pendingPermissions.filter((p) => p.id !== id);
conv.lastActivityAt = new Date();
}
return convs;
});
},
removePermissionForConversation: (conversationId: string, id: string) => {
conversations.update((convs) => {
const conv = convs.get(conversationId);
if (conv) {
conv.pendingPermissions = conv.pendingPermissions.filter((p) => p.id !== id);
conv.lastActivityAt = new Date(); conv.lastActivityAt = new Date();
} }
return convs; return convs;
+6 -2
View File
@@ -329,9 +329,12 @@ export async function initializeTauriListeners() {
unlisteners.push(cwdUnlisten); unlisteners.push(cwdUnlisten);
const permissionUnlisten = await listen<PermissionPromptEvent>("claude:permission", (event) => { const permissionUnlisten = await listen<PermissionPromptEvent>("claude:permission", (event) => {
const { id, tool_name, tool_input, description, conversation_id } = event.payload; const { permissions, conversation_id } = event.payload;
// Store each permission request for the specific conversation
for (const permission of permissions) {
const { id, tool_name, tool_input, description } = permission;
// Store permission request for the specific conversation
if (conversation_id) { if (conversation_id) {
claudeStore.requestPermissionForConversation(conversation_id, { claudeStore.requestPermissionForConversation(conversation_id, {
id, id,
@@ -354,6 +357,7 @@ export async function initializeTauriListeners() {
}); });
claudeStore.addLine("system", `Permission requested for: ${tool_name}`); claudeStore.addLine("system", `Permission requested for: ${tool_name}`);
} }
}
}); });
unlisteners.push(permissionUnlisten); unlisteners.push(permissionUnlisten);
+5 -1
View File
@@ -126,11 +126,15 @@ export interface PermissionRequest {
input: Record<string, unknown>; input: Record<string, unknown>;
} }
export interface PermissionPromptEvent { export interface PermissionPromptEventItem {
id: string; id: string;
tool_name: string; tool_name: string;
tool_input: Record<string, unknown>; tool_input: Record<string, unknown>;
description: string; description: string;
}
export interface PermissionPromptEvent {
permissions: PermissionPromptEventItem[];
conversation_id?: string; conversation_id?: string;
} }