feat: add automatic greeting upon connection #42

Merged
naomi merged 3 commits from feat/greeting into main 2026-01-16 15:10:28 -08:00
5 changed files with 123 additions and 2 deletions
+32 -1
View File
@@ -21,7 +21,7 @@ pub struct ClaudeStartOptions {
pub allowed_tools: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HikariConfig {
#[serde(default)]
pub model: Option<String>,
@@ -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<String>,
}
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]
+40
View File
@@ -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 @@
</div>
</section>
<!-- Greeting Section -->
<section class="mb-6">
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
Greeting
</h3>
<!-- Enable/Disable Toggle -->
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
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>
</label>
<p class="text-xs text-gray-500 mt-1 ml-7">
Automatically greet you when a session starts with time-based messages
</p>
</div>
<!-- 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>
<textarea
id="greeting-prompt"
bind:value={config.greeting_custom_prompt}
rows="3"
placeholder="Leave empty for time-based greetings, or customize how you'd like to be greeted..."
class="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent-primary)] resize-none"
></textarea>
</div>
{/if}
</section>
<!-- MCP Servers Section -->
<section class="mb-6">
<h3 class="text-sm font-medium text-[var(--accent-primary)] uppercase tracking-wider mb-3">
+2
View File
@@ -23,6 +23,8 @@
mcp_servers_json: null,
auto_granted_tools: [],
theme: "dark",
greeting_enabled: true,
greeting_custom_prompt: null,
});
onMount(async () => {
+4
View File
@@ -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() {
+45 -1
View File
@@ -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<string>("claude:connection", (event) => {
await listen<string>("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");