generated from nhcarrigan/template
fix: critical permission modal and config issues (#127)
## Summary This PR resolves several critical bugs that were blocking the permission modal and causing config loss: - **Permission modal not appearing** - Fixed z-index issues and runtime errors - **Config store race condition** - Resolved critical race condition causing settings to be lost - **Excessive logging** - Removed redundant fmt layer that was writing to hidden stdout - **System tool prompts** - Prevented unnecessary permission prompts for built-in tools - **Permission batching** - Added support for parallel permission requests - **ExitPlanMode tool** - Fixed ExitPlanMode tool not functioning correctly ## Changes Made ### Permission Modal Fixes - Updated z-index to proper value (9999) to ensure modal appears above all other UI elements - Fixed runtime errors that were preventing modal from rendering - Resolved issues with permission grants not being properly applied ### Config Store Race Condition - Fixed critical race condition where multiple rapid config updates would result in lost settings - Ensured config writes are properly sequenced to prevent data loss - Added proper synchronisation for config store operations ### Logging Cleanup - Removed redundant fmt formatting layer that was outputting to hidden stdout - Cleaned up excessive debug logging added during troubleshooting - Removed temporary debugging documentation files ### UX Improvements - Added close confirmation modal with minimise to tray option - Implemented batching for parallel permission requests - Added debug console for viewing frontend and backend logs ### ExitPlanMode Fix - Fixed ExitPlanMode tool not functioning correctly, ensuring proper transitions out of plan mode ## Issues Resolved Closes #112 - Permission flow now properly handles multiple tool requests Closes #113 - ExitPlanMode tool now functions correctly Closes #126 - Debug console feature added (partial - basic implementation complete) ## Test Plan - [x] Permission modal appears and functions correctly - [x] Config settings persist across app restarts - [x] No excessive logging in production builds - [x] System tools don't trigger permission prompts - [x] Parallel permission requests are properly batched - [x] Debug console displays frontend and backend logs - [x] ExitPlanMode properly exits plan mode --- ✨ This PR was created with help from Hikari~ 🌸 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Reviewed-on: #127 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #127.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { get } from "svelte/store";
|
||||
import { SvelteSet } from "svelte/reactivity";
|
||||
import { claudeStore } from "$lib/stores/claude";
|
||||
import { characterState } from "$lib/stores/character";
|
||||
import type { PermissionRequest } from "$lib/types/messages";
|
||||
@@ -8,13 +9,16 @@
|
||||
import { conversationsStore } from "$lib/stores/conversations";
|
||||
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 workingDirectory = $state("");
|
||||
|
||||
claudeStore.pendingPermission.subscribe((perm) => {
|
||||
permission = perm;
|
||||
if (perm) {
|
||||
conversationsStore.pendingPermissions.subscribe((perms) => {
|
||||
permissions = perms;
|
||||
// When new permissions arrive, select all by default
|
||||
if (perms.length > 0) {
|
||||
selectedPermissions = new SvelteSet(perms.map((p) => p.id));
|
||||
characterState.setState("permission");
|
||||
}
|
||||
});
|
||||
@@ -28,84 +32,103 @@
|
||||
});
|
||||
|
||||
async function handleApproveAndReconnect() {
|
||||
if (permission) {
|
||||
// Capture conversation history before clearing/reconnecting
|
||||
const conversationHistory = claudeStore.getConversationHistory();
|
||||
const approvedTool = permission.tool;
|
||||
const toolInput = permission.input;
|
||||
const selectedPerms = permissions.filter((p) => selectedPermissions.has(p.id));
|
||||
|
||||
claudeStore.grantTool(approvedTool);
|
||||
const newGrantedTools = [...grantedToolsList, approvedTool];
|
||||
claudeStore.addLine(
|
||||
"system",
|
||||
`Permission granted for: ${approvedTool}. Reconnecting with context...`
|
||||
);
|
||||
if (selectedPerms.length === 0) {
|
||||
claudeStore.addLine("system", "No permissions selected to approve");
|
||||
claudeStore.clearPermission();
|
||||
characterState.setTemporaryState("idle", 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop current session and reconnect with new permissions
|
||||
try {
|
||||
const conversationId = get(claudeStore.activeConversationId);
|
||||
if (!conversationId) {
|
||||
throw new Error("No active conversation");
|
||||
}
|
||||
// Capture conversation history before clearing/reconnecting
|
||||
const conversationHistory = claudeStore.getConversationHistory();
|
||||
|
||||
// Prevent stats reset on reconnection
|
||||
setSkipNextGreeting(true);
|
||||
// Grant all selected tools
|
||||
const newlyGrantedTools: string[] = [];
|
||||
for (const perm of selectedPerms) {
|
||||
if (!grantedToolsList.includes(perm.tool)) {
|
||||
claudeStore.grantTool(perm.tool);
|
||||
newlyGrantedTools.push(perm.tool);
|
||||
}
|
||||
}
|
||||
|
||||
await invoke("stop_claude", { conversationId });
|
||||
const newGrantedTools = [...grantedToolsList, ...newlyGrantedTools];
|
||||
const toolNames = selectedPerms.map((p) => p.tool).join(", ");
|
||||
|
||||
// Small delay to ensure clean shutdown
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
claudeStore.addLine(
|
||||
"system",
|
||||
`Permission granted for ${selectedPerms.length} tool(s): ${toolNames}. Reconnecting with context...`
|
||||
);
|
||||
claudeStore.clearPermission();
|
||||
|
||||
const config = configStore.getConfig();
|
||||
await invoke("start_claude", {
|
||||
conversationId,
|
||||
options: {
|
||||
working_dir: workingDirectory || "/home/naomi",
|
||||
model: config.model || null,
|
||||
api_key: config.api_key || null,
|
||||
custom_instructions: config.custom_instructions || null,
|
||||
mcp_servers_json: config.mcp_servers_json || null,
|
||||
allowed_tools: newGrantedTools,
|
||||
},
|
||||
});
|
||||
// Stop current session and reconnect with new permissions
|
||||
try {
|
||||
const conversationId = get(claudeStore.activeConversationId);
|
||||
if (!conversationId) {
|
||||
throw new Error("No active conversation");
|
||||
}
|
||||
|
||||
// Update Discord RPC when reconnecting after permission grant
|
||||
const activeConversation = get(conversationsStore.activeConversation);
|
||||
if (activeConversation) {
|
||||
await updateDiscordRpc(
|
||||
activeConversation.name,
|
||||
config.model || "claude",
|
||||
activeConversation.startedAt
|
||||
);
|
||||
}
|
||||
// Prevent stats reset on reconnection
|
||||
setSkipNextGreeting(true);
|
||||
|
||||
// Wait for connection to establish
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await invoke("stop_claude", { conversationId });
|
||||
|
||||
// Send conversation context to restore state
|
||||
if (conversationHistory) {
|
||||
const contextMessage = `[CONTEXT RESTORATION]
|
||||
I just granted you permission to use the ${approvedTool} tool. Here's our conversation so far:
|
||||
// Small delay to ensure clean shutdown
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
const config = configStore.getConfig();
|
||||
await invoke("start_claude", {
|
||||
conversationId,
|
||||
options: {
|
||||
working_dir: workingDirectory || "/home/naomi",
|
||||
model: config.model || null,
|
||||
api_key: config.api_key || null,
|
||||
custom_instructions: config.custom_instructions || null,
|
||||
mcp_servers_json: config.mcp_servers_json || null,
|
||||
allowed_tools: newGrantedTools,
|
||||
},
|
||||
});
|
||||
|
||||
// Update Discord RPC when reconnecting after permission grant
|
||||
const activeConversation = get(conversationsStore.activeConversation);
|
||||
if (activeConversation) {
|
||||
await updateDiscordRpc(
|
||||
activeConversation.name,
|
||||
config.model || "claude",
|
||||
activeConversation.startedAt
|
||||
);
|
||||
}
|
||||
|
||||
// Wait for connection to establish
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
// Send conversation context to restore state
|
||||
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]
|
||||
I just granted you permission to use ${selectedPerms.length} tool(s): ${toolNames}. Here's our conversation so far:
|
||||
|
||||
${conversationHistory}
|
||||
|
||||
The last action that was blocked was: ${approvedTool} with input:
|
||||
${JSON.stringify(toolInput, null, 2)}
|
||||
The actions that were blocked:
|
||||
${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", {
|
||||
conversationId,
|
||||
message: contextMessage,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to reconnect:", error);
|
||||
claudeStore.addLine("error", `Reconnect failed: ${error}`);
|
||||
await invoke("send_prompt", {
|
||||
conversationId,
|
||||
message: contextMessage,
|
||||
});
|
||||
}
|
||||
characterState.setTemporaryState("success", 2000);
|
||||
} catch (error) {
|
||||
console.error("Failed to reconnect:", error);
|
||||
claudeStore.addLine("error", `Reconnect failed: ${error}`);
|
||||
}
|
||||
characterState.setTemporaryState("success", 2000);
|
||||
}
|
||||
|
||||
function handleDismiss() {
|
||||
@@ -126,8 +149,24 @@ Please continue where we left off and retry that action now that you have permis
|
||||
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) {
|
||||
if (!permission) return;
|
||||
if (permissions.length === 0) return;
|
||||
|
||||
if (event.key === "Enter") {
|
||||
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} />
|
||||
|
||||
{#if permission}
|
||||
{#if permissions.length > 0}
|
||||
<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-[60] backdrop-blur-sm"
|
||||
>
|
||||
<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="w-10 h-10 rounded-full bg-yellow-500/20 flex items-center justify-center">
|
||||
<span class="text-xl">🔐</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-[var(--text-primary)]">Permission Blocked</h2>
|
||||
<p class="text-sm text-[var(--text-secondary)]">Hikari tried to use a restricted tool</p>
|
||||
<div class="flex-1">
|
||||
<h2 class="text-lg font-semibold text-[var(--text-primary)]">
|
||||
{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 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">
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="text-sm text-[var(--text-secondary)] mb-1">Tool</div>
|
||||
<div
|
||||
class="px-3 py-2 bg-[var(--bg-secondary)] rounded-md text-[var(--accent-primary)] font-mono flex items-center justify-between"
|
||||
>
|
||||
<span>{permission.tool}</span>
|
||||
{#if isToolAlreadyGranted(permission.tool)}
|
||||
<span class="text-xs text-green-400 bg-green-500/20 px-2 py-0.5 rounded"
|
||||
>Already Granted</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="space-y-3 mb-6">
|
||||
{#each permissions as perm (perm.id)}
|
||||
<div
|
||||
class="border border-[var(--border-color)] rounded-lg p-4 cursor-pointer transition-colors {selectedPermissions.has(
|
||||
perm.id
|
||||
)
|
||||
? 'bg-green-500/10 border-green-500/30'
|
||||
: 'bg-[var(--bg-secondary)] hover:bg-[var(--bg-secondary)]/80'}"
|
||||
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}
|
||||
</div>
|
||||
<div class="text-sm text-[var(--text-secondary)] mb-2">
|
||||
{perm.description}
|
||||
</div>
|
||||
{#if Object.keys(perm.input).length > 0}
|
||||
<details class="text-xs">
|
||||
<summary
|
||||
class="cursor-pointer text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
||||
>
|
||||
View details
|
||||
</summary>
|
||||
<pre
|
||||
class="mt-2 px-3 py-2 bg-[var(--bg-terminal)] rounded-md text-[var(--text-primary)] overflow-x-auto max-h-32">{formatInput(
|
||||
perm.input
|
||||
)}</pre>
|
||||
</details>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="text-sm text-[var(--text-secondary)] mb-1">Description</div>
|
||||
<div class="px-3 py-2 bg-[var(--bg-secondary)] rounded-md text-[var(--text-primary)]">
|
||||
{permission.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if Object.keys(permission.input).length > 0}
|
||||
<div class="mb-6">
|
||||
<div class="text-sm text-[var(--text-secondary)] mb-1">Details</div>
|
||||
<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(
|
||||
permission.input
|
||||
)}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Dismiss
|
||||
Dismiss All
|
||||
</button>
|
||||
<button
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user