generated from nhcarrigan/template
fix: critical permission modal and config issues #127
@@ -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!
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user