From ac84366716a0879858bfda407027b892eb02a5a6 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Fri, 16 Jan 2026 15:10:28 -0800 Subject: [PATCH] feat: add automatic greeting upon connection (#42) ### Explanation _No response_ ### Issue Closes #23 ### Attestations - [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/) - [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/). - [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/). ### Dependencies - [ ] I have pinned the dependencies to a specific patch version. ### Style - [ ] I have run the linter and resolved any errors. - [ ] My pull request uses an appropriate title, matching the conventional commit standards. - [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request. ### Tests - [ ] My contribution adds new code, and I have added tests to cover it. - [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes. - [ ] All new and existing tests pass locally with my changes. - [ ] Code coverage remains at or above the configured threshold. ### Documentation _No response_ ### Versioning _No response_ Reviewed-on: https://git.nhcarrigan.com/nhcarrigan/hikari-desktop/pulls/42 Co-authored-by: Naomi Carrigan Co-committed-by: Naomi Carrigan --- src-tauri/src/config.rs | 33 +++++++++++++++++- src/lib/components/ConfigSidebar.svelte | 40 +++++++++++++++++++++ src/lib/components/StatusBar.svelte | 2 ++ src/lib/stores/config.ts | 4 +++ src/lib/tauri.ts | 46 ++++++++++++++++++++++++- 5 files changed, 123 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 29fc620..e8923b6 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -21,7 +21,7 @@ pub struct ClaudeStartOptions { pub allowed_tools: Vec, } -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct HikariConfig { #[serde(default)] pub model: Option, @@ -40,6 +40,31 @@ pub struct HikariConfig { #[serde(default)] pub theme: Theme, + + #[serde(default = "default_greeting_enabled")] + pub greeting_enabled: bool, + + #[serde(default)] + pub greeting_custom_prompt: Option, +} + +impl Default for HikariConfig { + fn default() -> Self { + Self { + model: None, + api_key: None, + custom_instructions: None, + mcp_servers_json: None, + auto_granted_tools: Vec::new(), + theme: Theme::default(), + greeting_enabled: true, + greeting_custom_prompt: None, + } + } +} + +fn default_greeting_enabled() -> bool { + true } #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] @@ -63,6 +88,8 @@ mod tests { assert!(config.mcp_servers_json.is_none()); assert!(config.auto_granted_tools.is_empty()); assert_eq!(config.theme, Theme::Dark); + assert!(config.greeting_enabled); + assert!(config.greeting_custom_prompt.is_none()); } #[test] @@ -74,6 +101,8 @@ mod tests { mcp_servers_json: None, auto_granted_tools: vec!["Read".to_string(), "Glob".to_string()], theme: Theme::Light, + greeting_enabled: true, + greeting_custom_prompt: Some("Hello!".to_string()), }; let json = serde_json::to_string(&config).unwrap(); @@ -83,6 +112,8 @@ mod tests { assert_eq!(deserialized.custom_instructions, config.custom_instructions); assert_eq!(deserialized.auto_granted_tools, config.auto_granted_tools); assert_eq!(deserialized.theme, Theme::Light); + assert!(deserialized.greeting_enabled); + assert_eq!(deserialized.greeting_custom_prompt, Some("Hello!".to_string())); } #[test] diff --git a/src/lib/components/ConfigSidebar.svelte b/src/lib/components/ConfigSidebar.svelte index bb09d8c..fe53587 100644 --- a/src/lib/components/ConfigSidebar.svelte +++ b/src/lib/components/ConfigSidebar.svelte @@ -9,6 +9,8 @@ mcp_servers_json: null, auto_granted_tools: [], theme: "dark", + greeting_enabled: true, + greeting_custom_prompt: null, }); let isOpen = $state(false); @@ -220,6 +222,44 @@ + +
+

+ Greeting +

+ + +
+ +

+ Automatically greet you when a session starts with time-based messages +

+
+ + + {#if config.greeting_enabled} +
+ + +
+ {/if} +
+

diff --git a/src/lib/components/StatusBar.svelte b/src/lib/components/StatusBar.svelte index 5f34ede..4d6ca1f 100644 --- a/src/lib/components/StatusBar.svelte +++ b/src/lib/components/StatusBar.svelte @@ -23,6 +23,8 @@ mcp_servers_json: null, auto_granted_tools: [], theme: "dark", + greeting_enabled: true, + greeting_custom_prompt: null, }); onMount(async () => { diff --git a/src/lib/stores/config.ts b/src/lib/stores/config.ts index 2a2d63a..7d5aa45 100644 --- a/src/lib/stores/config.ts +++ b/src/lib/stores/config.ts @@ -10,6 +10,8 @@ export interface HikariConfig { mcp_servers_json: string | null; auto_granted_tools: string[]; theme: Theme; + greeting_enabled: boolean; + greeting_custom_prompt: string | null; } const defaultConfig: HikariConfig = { @@ -19,6 +21,8 @@ const defaultConfig: HikariConfig = { mcp_servers_json: null, auto_granted_tools: [], theme: "dark", + greeting_enabled: true, + greeting_custom_prompt: null, }; function createConfigStore() { diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index bc0cd4f..a747089 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -1,6 +1,8 @@ import { listen } from "@tauri-apps/api/event"; +import { invoke } from "@tauri-apps/api/core"; import { claudeStore } from "$lib/stores/claude"; import { characterState } from "$lib/stores/character"; +import { configStore } from "$lib/stores/config"; import type { ConnectionStatus, PermissionPromptEvent } from "$lib/types/messages"; import type { CharacterState } from "$lib/types/states"; @@ -9,6 +11,46 @@ interface StateChangePayload { tool_name: string | null; } +function getTimeOfDay(): string { + const hour = new Date().getHours(); + + if (hour >= 5 && hour < 12) { + return "morning"; + } else if (hour >= 12 && hour < 17) { + return "afternoon"; + } else if (hour >= 17 && hour < 21) { + return "evening"; + } else { + return "late night"; + } +} + +function generateGreetingPrompt(): string { + const timeOfDay = getTimeOfDay(); + return `[System: A new session has started. It's currently ${timeOfDay}. Please greet the user warmly and briefly. Keep it short - just 1-2 sentences.]`; +} + +async function sendGreeting() { + const config = configStore.getConfig(); + + if (!config.greeting_enabled) { + return; + } + + const greetingPrompt = config.greeting_custom_prompt?.trim() || generateGreetingPrompt(); + + // Don't show the system prompt in the UI - just trigger Claude to respond + characterState.setState("thinking"); + + try { + await invoke("send_prompt", { message: greetingPrompt }); + } catch (error) { + console.error("Failed to send greeting:", error); + claudeStore.addLine("error", `Failed to send greeting: ${error}`); + characterState.setTemporaryState("error", 3000); + } +} + interface OutputPayload { line_type: string; content: string; @@ -16,13 +58,15 @@ interface OutputPayload { } export async function initializeTauriListeners() { - await listen("claude:connection", (event) => { + await listen("claude:connection", async (event) => { const status = event.payload as ConnectionStatus; claudeStore.setConnectionStatus(status); if (status === "connected") { claudeStore.addLine("system", "Connected to Claude Code"); characterState.setState("idle"); + // Send greeting when connection is established + await sendGreeting(); } else if (status === "disconnected") { claudeStore.addLine("system", "Disconnected from Claude Code"); characterState.setState("idle");