feat: naomi did too much at once (#53)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 53s
CI / Lint & Test (push) Successful in 14m10s
CI / Build Linux (push) Successful in 16m47s
CI / Build Windows (cross-compile) (push) Successful in 26m36s

- feat: add slash commands
- feat: toggle window always on top
- fix: save settings button closes settings panel
- feat: input history (both text and commands)
- feat: add keyboard shortcuts
- feat: add confirmation modal when closing connected tabs
- fix: better text colours in light mode
- fix: handle multiple tabs requesting permission

Closes #6
Closes #13
Closes #21
Closes #28

Reviewed-on: #53
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #53.
This commit is contained in:
2026-01-21 17:38:36 -08:00
committed by Naomi Carrigan
parent 9fe4e8a48a
commit 947e56ef41
17 changed files with 1040 additions and 137 deletions
+55 -22
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { configStore, type HikariConfig, type Theme } from "$lib/stores/config";
import { claudeStore } from "$lib/stores/claude";
import { getCurrentWindow } from "@tauri-apps/api/window";
let config: HikariConfig = $state({
model: null,
@@ -13,6 +14,7 @@
greeting_custom_prompt: null,
notifications_enabled: true,
notification_volume: 0.7,
always_on_top: false,
});
let isOpen = $state(false);
@@ -61,6 +63,7 @@
saveError = null;
try {
await configStore.saveConfig(config);
configStore.closeSidebar();
} catch {
// Error is handled by the store
} finally {
@@ -95,6 +98,13 @@
function importFromSession() {
config.auto_granted_tools = [...new Set([...config.auto_granted_tools, ...grantedTools])];
}
async function handleAlwaysOnTopChange(enabled: boolean) {
config.always_on_top = enabled;
const window = getCurrentWindow();
await window.setAlwaysOnTop(enabled);
await configStore.updateConfig({ always_on_top: enabled });
}
</script>
<!-- Backdrop -->
@@ -121,7 +131,7 @@
<h2 class="text-lg font-semibold text-[var(--text-primary)]">Settings</h2>
<button
onclick={configStore.closeSidebar}
class="p-1 text-gray-400 hover:text-white transition-colors"
class="p-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors"
aria-label="Close settings"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -149,7 +159,7 @@
<!-- Model Selection -->
<div class="mb-4">
<label for="model" class="block text-sm text-gray-400 mb-1">Model</label>
<label for="model" class="block text-sm text-[var(--text-secondary)] mb-1">Model</label>
<select
id="model"
bind:value={config.model}
@@ -163,8 +173,8 @@
<!-- API Key -->
<div class="mb-4">
<label for="api-key" class="block text-sm text-gray-400 mb-1">
API Key <span class="text-gray-600">(optional override)</span>
<label for="api-key" class="block text-sm text-[var(--text-secondary)] mb-1">
API Key <span class="text-[var(--text-tertiary)]">(optional override)</span>
</label>
<div class="relative">
<input
@@ -177,7 +187,7 @@
<button
type="button"
onclick={() => (showApiKey = !showApiKey)}
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300"
class="absolute right-2 top-1/2 -translate-y-1/2 text-[var(--text-tertiary)] hover:text-[var(--text-primary)]"
aria-label={showApiKey ? "Hide API key" : "Show API key"}
>
{#if showApiKey}
@@ -211,7 +221,7 @@
<!-- Custom Instructions -->
<div class="mb-4">
<label for="instructions" class="block text-sm text-gray-400 mb-1"
<label for="instructions" class="block text-sm text-[var(--text-secondary)] mb-1"
>Custom Instructions</label
>
<textarea
@@ -238,9 +248,9 @@
bind:checked={config.greeting_enabled}
class="w-4 h-4 rounded border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--accent-primary)] focus:ring-[var(--accent-primary)]"
/>
<span class="text-sm text-gray-300">Send greeting on connect</span>
<span class="text-sm text-[var(--text-primary)]">Send greeting on connect</span>
</label>
<p class="text-xs text-gray-500 mt-1 ml-7">
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
Automatically greet you when a session starts with time-based messages
</p>
</div>
@@ -248,8 +258,8 @@
<!-- Custom Greeting Prompt -->
{#if config.greeting_enabled}
<div class="mb-4">
<label for="greeting-prompt" class="block text-sm text-gray-400 mb-1">
Custom Greeting Prompt <span class="text-gray-600">(optional)</span>
<label for="greeting-prompt" class="block text-sm text-[var(--text-secondary)] mb-1">
Custom Greeting Prompt <span class="text-[var(--text-tertiary)]">(optional)</span>
</label>
<textarea
id="greeting-prompt"
@@ -268,8 +278,8 @@
MCP Servers
</h3>
<div class="mb-2">
<label for="mcp-config" class="block text-sm text-gray-400 mb-1">
Server Configuration <span class="text-gray-600">(JSON)</span>
<label for="mcp-config" class="block text-sm text-[var(--text-secondary)] mb-1">
Server Configuration <span class="text-[var(--text-tertiary)]">(JSON)</span>
</label>
<textarea
id="mcp-config"
@@ -286,14 +296,14 @@
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
Auto-Granted Tools
</h3>
<p class="text-xs text-gray-500 mb-3">
<p class="text-xs text-[var(--text-tertiary)] mb-3">
These tools will be pre-approved for every session (no permission prompts).
</p>
<!-- Common tools checkboxes -->
<div class="grid grid-cols-2 gap-2 mb-3">
{#each commonTools as tool (tool)}
<label class="flex items-center gap-2 text-sm text-gray-300 cursor-pointer">
<label class="flex items-center gap-2 text-sm text-[var(--text-primary)] cursor-pointer">
<input
type="checkbox"
checked={config.auto_granted_tools.includes(tool)}
@@ -309,7 +319,7 @@
{#if grantedTools.length > 0}
<div class="mb-3">
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-gray-500">Session-granted tools:</span>
<span class="text-xs text-[var(--text-tertiary)]">Session-granted tools:</span>
<button
onclick={importFromSession}
class="text-xs text-[var(--accent-primary)] hover:text-[var(--accent-secondary)] transition-colors"
@@ -332,7 +342,7 @@
<!-- Custom tools list -->
{#if config.auto_granted_tools.filter((t) => !commonTools.includes(t)).length > 0}
<div class="mb-3">
<span class="text-xs text-gray-500 block mb-2">Custom tools:</span>
<span class="text-xs text-[var(--text-tertiary)] block mb-2">Custom tools:</span>
<div class="flex flex-wrap gap-1">
{#each config.auto_granted_tools.filter((t) => !commonTools.includes(t)) as tool (tool)}
<span
@@ -341,7 +351,7 @@
{tool}
<button
onclick={() => removeTool(tool)}
class="text-gray-500 hover:text-red-400"
class="text-[var(--text-tertiary)] hover:text-red-400"
aria-label="Remove {tool}"
>
×
@@ -381,7 +391,7 @@
onclick={() => handleThemeChange("dark")}
class="flex-1 px-4 py-2 rounded-lg border transition-colors {config.theme === 'dark'
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-gray-400 hover:border-[var(--accent-primary)]'}"
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
>
Dark
</button>
@@ -389,13 +399,36 @@
onclick={() => handleThemeChange("light")}
class="flex-1 px-4 py-2 rounded-lg border transition-colors {config.theme === 'light'
? 'bg-[var(--accent-primary)] border-[var(--accent-primary)] text-white'
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-gray-400 hover:border-[var(--accent-primary)]'}"
: 'bg-[var(--bg-primary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]'}"
>
Light
</button>
</div>
</section>
<!-- Window Section -->
<section class="mb-6">
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
Window
</h3>
<!-- Always on Top Toggle -->
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={config.always_on_top}
onchange={(e) => handleAlwaysOnTopChange(e.currentTarget.checked)}
class="w-4 h-4 text-[var(--accent-primary)] bg-[var(--bg-primary)] border-[var(--border-color)] rounded focus:ring-[var(--accent-primary)] focus:ring-2"
/>
<span class="text-sm text-[var(--text-primary)]">Always on top</span>
</label>
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
Keep the window above other windows
</p>
</div>
</section>
<!-- Notifications Section -->
<section class="mb-6">
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
@@ -410,13 +443,13 @@
bind:checked={config.notifications_enabled}
class="w-4 h-4 text-[var(--accent-primary)] bg-[var(--bg-primary)] border-[var(--border-color)] rounded focus:ring-[var(--accent-primary)] focus:ring-2"
/>
<span class="text-sm text-gray-300">Enable sound notifications</span>
<span class="text-sm text-[var(--text-primary)]">Enable sound notifications</span>
</label>
</div>
<!-- Volume Control -->
<div class="mb-4">
<label for="notification-volume" class="block text-sm text-gray-400 mb-2">
<label for="notification-volume" class="block text-sm text-[var(--text-secondary)] mb-2">
Notification Volume
</label>
<div class="flex items-center gap-3">
@@ -436,7 +469,7 @@
</div>
</div>
<div class="text-xs text-gray-500">
<div class="text-xs text-[var(--text-tertiary)]">
Sound notifications will play when I complete tasks, encounter errors, or need permissions.
</div>
</section>