diff --git a/README.md b/README.md index 72753e2..9e4c176 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Most AI tools require accounts, API keys, or subscriptions that bill you per tok - **Isolated sandbox:** optionally run models in a hardened Docker container with capability dropping, read-only volumes, and localhost-only networking - **Image input:** paste or drag images and screenshots directly into the chat - **Screen capture:** type `/screen` to instantly capture your entire screen and attach it to your question as context -- **Slash commands:** built-in prompt shortcuts for common tasks: `/translate`, `/rewrite`, `/tldr`, `/refine`, `/bullets`, `/todos`. Highlight text anywhere, summon Thuki, type a command, and hit Enter +- **Slash commands:** built-in commands for live search and prompt shortcuts: `/search`, `/translate`, `/rewrite`, `/tldr`, `/refine`, `/bullets`, `/todos`. Highlight text anywhere, summon Thuki, type a command, and hit Enter - **Extended reasoning:** type `/think` to have the model reason through a problem step by step before answering - **Privacy-first:** zero-trust architecture, all data stays on your device diff --git a/docs/commands.md b/docs/commands.md index 20fd5d4..5ea2bc4 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,6 +1,10 @@ + + # Commands -Commands are typed at the start of a message using the `/` prefix. Press `/` to open the command suggestion menu, then Tab to complete or Enter to select. +Commands are written as whole-word `/` triggers anywhere in your message. Press `/` to open the command suggestion menu, then Tab to complete or Enter to select. + +Commands can be combined when their behavior allows it. For example, `/screen /think` captures the screen and enables extended reasoning, while `/think /tldr` summarizes with thinking enabled. Commands that operate on text follow a consistent input priority: @@ -8,7 +12,21 @@ Commands that operate on text follow a consistent input priority: 2. **No highlighted text + typed text after command:** typed text is the input 3. **Both present:** highlighted text is the primary input; typed text is appended as an additional instruction -This means you can highlight a paragraph anywhere on screen, summon Thuki with double-tap Control, type a command, and hit Enter without typing anything else. +This means you can highlight text anywhere on screen, summon Thuki with double-tap Control, type a command, and hit Enter without retyping the selected content. + +## /search + +Runs agentic web search and answers from live sources with citations. + +**Usage:** `/search ` + +**Examples:** +- `/search who owns Figma now?`: searches live sources for a current answer +- `/search latest React 19 release notes`: retrieves recent release information from the web + +**Behavior:** Routes the message through Thuki's local search pipeline instead of plain chat. Answers are grounded in retrieved web sources and typically include inline citations plus a Sources footer. + +**Limit:** Requires the search sandbox to be running. Use it for current, changing, or cutoff-sensitive information. --- @@ -20,31 +38,31 @@ Captures your screen and attaches it as context for the current message. **Examples:** - `/screen`: sends a screenshot with no additional message -- `/screen what is this error?`: attaches a screenshot and asks the question +- `/screen what is this error?`: attaches a screenshot and asks a question about it -**Behavior:** The screenshot is taken the moment you press Enter. Thuki's own window is excluded from the capture: no flicker, no hide. The image appears in your message bubble exactly like a pasted screenshot. +**Behavior:** The screenshot is taken when you submit the message. Thuki's own window is excluded from the capture, and the image appears in your message bubble like a pasted screenshot. -**Composable:** `/screen` works with all other commands. `/screen /rewrite` captures the screen and rewrites whatever text the model sees. `/screen /think` enables extended reasoning on the captured content. +**Composable:** `/screen` can combine with `/think` and utility commands. For example, `/screen /rewrite` captures the screen and rewrites whatever text the model can see. -**Limit:** One `/screen` capture per message. You may also attach up to 3 images manually (paste, drag, or the camera button) for a total of 4 images per message. +**Limit:** One `/screen` capture per message. You may also attach up to 3 images manually for a total of 4 images per message. -**Permission:** Requires Screen Recording permission. On first use, macOS will prompt you to grant it. If denied, Thuki cannot capture the screen. Grant access in System Settings > Privacy & Security > Screen Recording. +**Permission:** Requires Screen Recording permission. If denied, Thuki cannot capture the screen until access is granted in System Settings. --- ## /think -Enables extended reasoning before the model responds. The model works through the problem step by step internally before writing its answer. +Enables extended reasoning before the model responds. **Usage:** `/think [optional message or highlighted text]` **Examples:** -- `/think` (with highlighted text): reasons through the selected content +- `/think` with highlighted text: reasons through the selected content - `/think what are the tradeoffs of a monorepo vs polyrepo?`: asks a question with deep reasoning enabled -**Behavior:** A collapsible "Thinking" block appears above the response showing the model's reasoning chain. The final answer appears below it as normal. +**Behavior:** A collapsible Thinking block appears above the response showing the model's reasoning chain. The final answer appears below it as normal. -**Composable:** `/think` works with all utility commands. `/think /tldr` summarizes with extended reasoning enabled. +**Composable:** `/think` works with `/screen` and all utility commands. For example, `/think /tldr` summarizes with extended reasoning enabled. --- @@ -52,16 +70,18 @@ Enables extended reasoning before the model responds. The model works through th Translates text to another language. -**Usage:** `/translate [language] [text]` or `/translate` with highlighted text +**Usage:** `/translate [language] [text] or /translate with highlighted text` **Examples:** -- `/translate` (with highlighted text): auto-detects language and translates. Non-English input translates to English; English input translates to Vietnamese -- `/translate ja` (with highlighted text): translates highlighted text to Japanese -- `/translate Spanish meeting notes here`: translates the typed text to Spanish +- `/translate` with highlighted text: auto-detects the source language and translates it +- `/translate ja` with highlighted text: translates highlighted text to Japanese +- `/translate Spanish meeting notes here`: translates typed text to Spanish + +**Behavior:** Outputs only the translation with no commentary or explanation. -**Language format:** You can specify the target language by full name (`French`), ISO code (`fr`, `fra`), or common shorthand. The model interprets it flexibly. +**Language format:** The target language can be a full name (`French`), ISO code (`fr`, `fra`), or common shorthand. -**Default behavior:** If no language is specified, non-English input is translated to English and English input is translated to Vietnamese. +**Default behavior:** If no language is specified, non-English input translates to English and English input translates to Vietnamese. --- @@ -69,13 +89,13 @@ Translates text to another language. Rewrites text to read more naturally and clearly. -**Usage:** `/rewrite [text]` or `/rewrite` with highlighted text +**Usage:** `/rewrite [text] or /rewrite with highlighted text` **Examples:** -- `/rewrite` (with highlighted text): rewrites the selected text -- `/rewrite so basically what happened was i was trying to fix the bug`: rewrites the typed text +- `/rewrite` with highlighted text: rewrites the selected text +- `/rewrite so basically what happened was i was trying to fix the bug`: rewrites typed text for clarity -**Behavior:** Preserves the original meaning while improving flow and readability. Output only: no commentary or explanation. +**Behavior:** Preserves the original meaning while improving flow and readability. Outputs only the rewritten text. --- @@ -83,11 +103,11 @@ Rewrites text to read more naturally and clearly. Summarizes text into 1-3 short, direct sentences. -**Usage:** `/tldr [text]` or `/tldr` with highlighted text +**Usage:** `/tldr [text] or /tldr with highlighted text` **Examples:** -- `/tldr` (with highlighted text): summarizes the selected content -- `/tldr [paste a long article]`: summarizes the typed or pasted text +- `/tldr` with highlighted text: summarizes the selected content +- `/tldr [paste a long article]`: summarizes typed or pasted text **Behavior:** Captures the core message, key decision, or critical takeaway. Skips background detail and qualifications. @@ -97,11 +117,11 @@ Summarizes text into 1-3 short, direct sentences. Fixes grammar, spelling, and punctuation while preserving your voice. -**Usage:** `/refine [text]` or `/refine` with highlighted text +**Usage:** `/refine [text] or /refine with highlighted text` **Examples:** -- `/refine` (with highlighted text): corrects the selected text -- `/refine hey just wanted to follow up on the thing we discussed`: cleans up the typed text +- `/refine` with highlighted text: corrects the selected text +- `/refine hey just wanted to follow up on the thing we discussed`: cleans up typed text **Behavior:** Corrects errors and smooths rough phrasing without restructuring or adding new ideas. Your original tone and meaning stay intact. @@ -111,13 +131,13 @@ Fixes grammar, spelling, and punctuation while preserving your voice. Extracts key points from text as a markdown bullet list. -**Usage:** `/bullets [text]` or `/bullets` with highlighted text +**Usage:** `/bullets [text] or /bullets with highlighted text` **Examples:** -- `/bullets` (with highlighted text): extracts key points from the selection -- `/bullets [paste meeting notes]`: extracts key points from the typed or pasted content +- `/bullets` with highlighted text: extracts key points from the selection +- `/bullets [paste meeting notes]`: extracts key points from typed or pasted content -**Behavior:** Each point is a concise, self-contained statement. Ordered by importance or logical sequence. Filler and repetition are removed. Output is a `- ` prefixed markdown list. +**Behavior:** Each point is a concise, self-contained statement. Ordered by importance or logical sequence. Filler and repetition are removed. Output uses `- ` prefixed markdown bullets. --- @@ -125,10 +145,10 @@ Extracts key points from text as a markdown bullet list. Summarizes what a piece of text is about, then extracts every task, action item, and commitment as a markdown checkbox list. -**Usage:** `/todos [text]` or `/todos` with highlighted text +**Usage:** `/todos [text] or /todos with highlighted text` **Examples:** -- `/todos` (with highlighted text): summarizes and extracts to-dos from the selected text -- `/todos [paste a conversation or notes]`: processes the typed or pasted content +- `/todos` with highlighted text: summarizes and extracts to-dos from the selected text +- `/todos [paste a conversation or notes]`: processes typed or pasted content -**Behavior:** Responds in two parts: a short paragraph explaining the context and what is at stake, followed by a `- [ ]` checkbox list of all tasks. Each to-do includes who is responsible (if mentioned) and any deadline or timeframe. Observations and background that imply no action are excluded. +**Behavior:** Responds in two parts: a short paragraph explaining the context and what is at stake, followed by a `- [ ]` checkbox list of all tasks. Each to-do includes who is responsible, plus any deadline or timeframe if mentioned. diff --git a/docs/configurations.md b/docs/configurations.md index 65dc66c..8bce5c6 100644 --- a/docs/configurations.md +++ b/docs/configurations.md @@ -30,7 +30,7 @@ Controls the system prompt prepended to every conversation sent to Ollama. | Variable | Description | Default | | :--- | :--- | :--- | -| `THUKI_SYSTEM_PROMPT` | Custom system prompt for all conversations. If unset or empty, the built-in default is used. | Built-in secretary persona prompt (see `src-tauri/src/commands.rs`) | +| `THUKI_SYSTEM_PROMPT` | Custom base system prompt for all conversations. If unset or empty, the built-in default is used. Thuki still appends its generated slash-command appendix so built-in command knowledge stays available. | Built-in secretary persona prompt plus generated slash-command appendix (see `src-tauri/src/commands.rs`) | ### Model Configuration diff --git a/package.json b/package.json index 060b226..20480de 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "scripts": { "dev": "tauri dev", "frontend:dev": "vite", + "generate:commands": "bun scripts/generate-commands.ts", "build:frontend": "tsc && vite build", "build:backend": "tauri build --bundles app", "build:all": "bun run build:frontend && bun run build:backend", diff --git a/scripts/generate-commands.ts b/scripts/generate-commands.ts new file mode 100644 index 0000000..2bb879d --- /dev/null +++ b/scripts/generate-commands.ts @@ -0,0 +1,30 @@ +import { mkdir, writeFile } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { + renderCommandsMarkdown, + renderSlashCommandPromptAppendix, +} from '../src/config/commandArtifacts'; + +const repoRoot = fileURLToPath(new URL('../', import.meta.url)); +const outputs = [ + { + path: resolve(repoRoot, 'docs/commands.md'), + content: renderCommandsMarkdown(), + }, + { + path: resolve( + repoRoot, + 'src-tauri/prompts/generated/slash_commands.txt', + ), + content: renderSlashCommandPromptAppendix(), + }, +]; + +await Promise.all( + outputs.map(async ({ path, content }) => { + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, content, 'utf8'); + }), +); diff --git a/src-tauri/prompts/generated/slash_commands.txt b/src-tauri/prompts/generated/slash_commands.txt new file mode 100644 index 0000000..05c494f --- /dev/null +++ b/src-tauri/prompts/generated/slash_commands.txt @@ -0,0 +1,23 @@ +# Supported slash commands + +These are Thuki's only built-in slash commands: /search, /screen, /think, /translate, /rewrite, /tldr, /refine, /bullets, /todos. + +If the user asks what slash commands are available, what built-in commands exist, or how to use them, answer with the slash-command list below. Do not answer about generic tools, tool availability, or function calling. + +/search: agentic web search for current or cutoff-sensitive questions. + +/screen: capture current screen and attach it as image context. + +/think: enable extended reasoning before answering. + +/translate: translate selected or typed text to requested language. + +/rewrite: rewrite text for clarity and flow. + +/tldr: summarize text in 1-3 short direct sentences. + +/refine: fix grammar, spelling, punctuation, and rough phrasing while preserving tone. + +/bullets: extract key points as markdown bullets. + +/todos: summarize context and extract tasks as markdown checkboxes. diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index f16218a..aa98918 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -10,6 +10,7 @@ use tokio_util::sync::CancellationToken; pub const DEFAULT_OLLAMA_URL: &str = "http://127.0.0.1:11434"; pub const DEFAULT_MODEL_NAME: &str = "gemma4:e2b"; const DEFAULT_SYSTEM_PROMPT: &str = include_str!("../prompts/system_prompt.txt"); +const SLASH_COMMAND_PROMPT_APPENDIX: &str = include_str!("../prompts/generated/slash_commands.txt"); /// Classifies the kind of error returned from the Ollama backend. /// Used by the frontend to pick accent bar color and display copy. @@ -191,11 +192,33 @@ pub struct SystemPrompt(pub String); /// Reads `THUKI_SYSTEM_PROMPT` from the environment, falling back to the /// built-in default when unset or empty. +pub fn compose_system_prompt_with_appendix(base_prompt: &str, appendix: &str) -> String { + let base = base_prompt.trim_end(); + let appendix = appendix.trim(); + + if appendix.is_empty() { + base.to_string() + } else { + format!("{base}\n\n{appendix}") + } +} + +/// Reads `THUKI_SYSTEM_PROMPT` from the environment, falling back to the +/// built-in default when unset or empty. +pub fn compose_system_prompt(base_prompt: &str) -> String { + compose_system_prompt_with_appendix(base_prompt, SLASH_COMMAND_PROMPT_APPENDIX) +} + +/// Reads `THUKI_SYSTEM_PROMPT` from the environment as the base prompt, then +/// appends the generated slash-command appendix so built-in command knowledge +/// stays in sync even when the persona prompt is overridden. pub fn load_system_prompt() -> String { - std::env::var("THUKI_SYSTEM_PROMPT") + let base_prompt = std::env::var("THUKI_SYSTEM_PROMPT") .ok() .filter(|s| !s.trim().is_empty()) - .unwrap_or_else(|| DEFAULT_SYSTEM_PROMPT.to_string()) + .unwrap_or_else(|| DEFAULT_SYSTEM_PROMPT.to_string()); + + compose_system_prompt(&base_prompt) } /// Model configuration loaded once at startup from the `THUKI_SUPPORTED_AI_MODELS` @@ -1253,7 +1276,7 @@ mod tests { std::env::remove_var("THUKI_SYSTEM_PROMPT"); let prompt = load_system_prompt(); - assert_eq!(prompt, DEFAULT_SYSTEM_PROMPT); + assert_eq!(prompt, compose_system_prompt(DEFAULT_SYSTEM_PROMPT)); } #[test] @@ -1262,7 +1285,7 @@ mod tests { std::env::set_var("THUKI_SYSTEM_PROMPT", "Custom prompt"); let prompt = load_system_prompt(); - assert_eq!(prompt, "Custom prompt"); + assert_eq!(prompt, compose_system_prompt("Custom prompt")); std::env::remove_var("THUKI_SYSTEM_PROMPT"); } @@ -1273,11 +1296,26 @@ mod tests { std::env::set_var("THUKI_SYSTEM_PROMPT", " "); let prompt = load_system_prompt(); - assert_eq!(prompt, DEFAULT_SYSTEM_PROMPT); + assert_eq!(prompt, compose_system_prompt(DEFAULT_SYSTEM_PROMPT)); std::env::remove_var("THUKI_SYSTEM_PROMPT"); } + #[test] + fn compose_system_prompt_appends_slash_command_appendix() { + let prompt = compose_system_prompt("Base prompt"); + + assert!(prompt.starts_with("Base prompt\n\n# Supported slash commands")); + assert!(prompt.contains("/search:")); + } + + #[test] + fn compose_system_prompt_returns_base_when_appendix_is_blank() { + let prompt = compose_system_prompt_with_appendix("Base prompt", " "); + + assert_eq!(prompt, "Base prompt"); + } + #[test] fn conversation_history_new_starts_at_epoch_zero() { let h = ConversationHistory::new(); diff --git a/src/components/__tests__/CommandSuggestion.test.tsx b/src/components/__tests__/CommandSuggestion.test.tsx index cb85693..69ef6ae 100644 --- a/src/components/__tests__/CommandSuggestion.test.tsx +++ b/src/components/__tests__/CommandSuggestion.test.tsx @@ -3,66 +3,69 @@ import { describe, it, expect, vi } from 'vitest'; import { CommandSuggestion } from '../CommandSuggestion'; import type { Command } from '../../config/commands'; -const SEARCH_CMD: Command = { - trigger: '/search', - label: '/search', - description: 'Agentic web search: iterative reasoning & cited synthesis', -}; +function makeCommand( + trigger: string, + description: string, + promptTemplate?: string, +): Command { + return { + trigger, + label: trigger, + description, + docs: { + summary: description, + usage: `${trigger} [text]`, + examples: [`\`${trigger} example\``], + behavior: description, + }, + promptHelp: { + summary: description, + }, + promptTemplate, + }; +} -const SCREEN_CMD: Command = { - trigger: '/screen', - label: '/screen', - description: 'Capture your screen and include it as context', -}; +const SEARCH_CMD = makeCommand( + '/search', + 'Agentic web search: iterative reasoning & cited synthesis', +); -const FOO_CMD: Command = { - trigger: '/foo', - label: '/foo', - description: 'A test command', -}; +const SCREEN_CMD = makeCommand( + '/screen', + 'Capture your screen and include it as context', +); -const THINK_CMD: Command = { - trigger: '/think', - label: '/think', - description: 'Think deeply before answering', -}; +const FOO_CMD = makeCommand('/foo', 'A test command'); -const TRANSLATE_CMD: Command = { - trigger: '/translate', - label: '/translate', - description: 'Translate text to another language', -}; +const THINK_CMD = makeCommand('/think', 'Think deeply before answering'); -const REWRITE_CMD: Command = { - trigger: '/rewrite', - label: '/rewrite', - description: 'Rewrite text for clarity and flow', -}; +const TRANSLATE_CMD = makeCommand( + '/translate', + 'Translate text to another language', +); -const TLDR_CMD: Command = { - trigger: '/tldr', - label: '/tldr', - description: 'Summarize text in 1-3 sentences', -}; +const REWRITE_CMD = makeCommand( + '/rewrite', + 'Rewrite text for clarity and flow', +); -const REFINE_CMD: Command = { - trigger: '/refine', - label: '/refine', - description: 'Fix grammar, spelling, and punctuation', -}; +const TLDR_CMD = makeCommand('/tldr', 'Summarize text in 1-3 sentences'); -const BULLETS_CMD: Command = { - trigger: '/bullets', - label: '/bullets', - description: 'Extract key points as a bullet list', -}; +const REFINE_CMD = makeCommand( + '/refine', + 'Fix grammar, spelling, and punctuation', +); -const ACTION_CMD: Command = { - trigger: '/todos', - label: '/todos', - description: 'Extract to-do items as a checkbox list', - promptTemplate: 'dummy $INPUT', -}; +const BULLETS_CMD = makeCommand( + '/bullets', + 'Extract key points as a bullet list', +); + +const ACTION_CMD = makeCommand( + '/todos', + 'Extract to-do items as a checkbox list', + 'dummy $INPUT', +); describe('CommandSuggestion', () => { it('shows "No commands found" when commands list is empty', () => { diff --git a/src/config/__tests__/commandArtifacts.test.ts b/src/config/__tests__/commandArtifacts.test.ts new file mode 100644 index 0000000..7786cbb --- /dev/null +++ b/src/config/__tests__/commandArtifacts.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; + +import { + renderCommandsMarkdown, + renderSlashCommandPromptAppendix, +} from '../commandArtifacts'; +import checkedInCommandsMarkdown from '../../../docs/commands.md?raw'; +import checkedInPromptAppendix from '../../../src-tauri/prompts/generated/slash_commands.txt?raw'; + +describe('generated command artifacts', () => { + it('renders docs markdown that matches the checked-in file', () => { + expect(renderCommandsMarkdown()).toBe(checkedInCommandsMarkdown); + }); + + it('renders prompt appendix that matches the checked-in file', () => { + expect(renderSlashCommandPromptAppendix()).toBe(checkedInPromptAppendix); + }); + + it('includes /search in both generated artifacts', () => { + expect(renderCommandsMarkdown()).toContain('## /search'); + expect(renderSlashCommandPromptAppendix()).toContain('/search:'); + }); + + it('explicitly teaches the model how to answer slash-command questions', () => { + const appendix = renderSlashCommandPromptAppendix(); + + expect(appendix).toContain( + "These are Thuki's only built-in slash commands:", + ); + expect(appendix).toContain( + 'If the user asks what slash commands are available, what built-in commands exist, or how to use them, answer with the slash-command list below.', + ); + expect(appendix).toContain( + 'Do not answer about generic tools, tool availability, or function calling.', + ); + }); +}); diff --git a/src/config/__tests__/commands.test.ts b/src/config/__tests__/commands.test.ts index f402001..ca8c825 100644 --- a/src/config/__tests__/commands.test.ts +++ b/src/config/__tests__/commands.test.ts @@ -17,6 +17,17 @@ describe('COMMANDS registry', () => { expect(typeof cmd.description).toBe('string'); expect(cmd.description.length).toBeGreaterThan(0); + + expect(typeof cmd.docs.summary).toBe('string'); + expect(cmd.docs.summary.length).toBeGreaterThan(0); + expect(typeof cmd.docs.usage).toBe('string'); + expect(cmd.docs.usage.length).toBeGreaterThan(0); + expect(cmd.docs.examples.length).toBeGreaterThan(0); + expect(typeof cmd.docs.behavior).toBe('string'); + expect(cmd.docs.behavior.length).toBeGreaterThan(0); + + expect(typeof cmd.promptHelp.summary).toBe('string'); + expect(cmd.promptHelp.summary.length).toBeGreaterThan(0); } }); @@ -39,6 +50,13 @@ describe('COMMANDS registry', () => { expect(screen?.description.length).toBeGreaterThan(0); }); + it('includes the /search command', () => { + const search = COMMANDS.find((c: Command) => c.trigger === '/search'); + expect(search).toBeDefined(); + expect(search?.label).toBe('/search'); + expect(search?.description.length).toBeGreaterThan(0); + }); + it('includes the /think command', () => { const think = COMMANDS.find((c: Command) => c.trigger === '/think'); expect(think).toBeDefined(); diff --git a/src/config/commandArtifacts.ts b/src/config/commandArtifacts.ts new file mode 100644 index 0000000..2b12601 --- /dev/null +++ b/src/config/commandArtifacts.ts @@ -0,0 +1,92 @@ +import { COMMANDS, type Command } from './commands'; + +const GENERATED_DOCS_NOTICE = + ''; +const SLASH_COMMAND_LIST = COMMANDS.map((command) => command.trigger).join( + ', ', +); + +function renderDocsSection(command: Command): string { + const sections = [ + `## ${command.trigger}`, + '', + command.docs.summary, + '', + `**Usage:** \`${command.docs.usage}\``, + '', + '**Examples:**', + ...command.docs.examples.map((example) => `- ${example}`), + '', + `**Behavior:** ${command.docs.behavior}`, + ]; + + if (command.docs.composability) { + sections.push('', `**Composable:** ${command.docs.composability}`); + } + + if (command.docs.limit) { + sections.push('', `**Limit:** ${command.docs.limit}`); + } + + if (command.docs.permission) { + sections.push('', `**Permission:** ${command.docs.permission}`); + } + + if (command.docs.languageFormat) { + sections.push('', `**Language format:** ${command.docs.languageFormat}`); + } + + if (command.docs.defaultBehavior) { + sections.push('', `**Default behavior:** ${command.docs.defaultBehavior}`); + } + + return sections.join('\n'); +} + +function renderPromptSection(command: Command): string { + return `${command.trigger}: ${command.promptHelp.summary}`; +} + +export function renderCommandsMarkdown(): string { + return [ + GENERATED_DOCS_NOTICE, + '', + '# Commands', + '', + 'Commands are written as whole-word `/` triggers anywhere in your message. Press `/` to open the command suggestion menu, then Tab to complete or Enter to select.', + '', + 'Commands can be combined when their behavior allows it. For example, `/screen /think` captures the screen and enables extended reasoning, while `/think /tldr` summarizes with thinking enabled.', + '', + 'Commands that operate on text follow a consistent input priority:', + '', + '1. **Highlighted text + no typed text:** highlighted text is the input', + '2. **No highlighted text + typed text after command:** typed text is the input', + '3. **Both present:** highlighted text is the primary input; typed text is appended as an additional instruction', + '', + 'This means you can highlight text anywhere on screen, summon Thuki with double-tap Control, type a command, and hit Enter without retyping the selected content.', + '', + ...COMMANDS.flatMap((command, index) => { + const section = renderDocsSection(command); + return index === COMMANDS.length - 1 + ? [section] + : [section, '', '---', '']; + }), + '', + ].join('\n'); +} + +export function renderSlashCommandPromptAppendix(): string { + return [ + '# Supported slash commands', + '', + `These are Thuki's only built-in slash commands: ${SLASH_COMMAND_LIST}.`, + '', + 'If the user asks what slash commands are available, what built-in commands exist, or how to use them, answer with the slash-command list below. Do not answer about generic tools, tool availability, or function calling.', + '', + ...COMMANDS.flatMap((command, index) => { + const section = renderPromptSection(command); + return index === COMMANDS.length - 1 ? [section] : [section, '']; + }), + '', + ].join('\n'); +} diff --git a/src/config/commands.ts b/src/config/commands.ts index f92f0f7..19df38f 100644 --- a/src/config/commands.ts +++ b/src/config/commands.ts @@ -6,6 +6,38 @@ * no other registration is needed. */ +export interface CommandDocs { + /** Short paragraph used as the section opener in generated docs. */ + readonly summary: string; + /** Usage string shown in generated docs. */ + readonly usage: string; + /** Human-facing examples rendered as markdown bullets. */ + readonly examples: readonly string[]; + /** Main behavior description for generated docs. */ + readonly behavior: string; + /** Optional composition note for generated docs. */ + readonly composability?: string; + /** Optional limit note for generated docs. */ + readonly limit?: string; + /** Optional permission note for generated docs. */ + readonly permission?: string; + /** Optional language-format note for generated docs. */ + readonly languageFormat?: string; + /** Optional default-behavior note for generated docs. */ + readonly defaultBehavior?: string; +} + +export interface CommandPromptHelp { + /** Short model-facing summary for the generated prompt appendix. */ + readonly summary: string; + /** Conservative guidance on when this command should be mentioned. */ + readonly whenToSuggest?: string; + /** Optional composition guidance for the prompt appendix. */ + readonly composition?: string; + /** Optional limits or caveats for the prompt appendix. */ + readonly limit?: string; +} + export interface Command { /** The slash trigger, e.g. "/screen". Must start with "/". */ readonly trigger: string; @@ -13,6 +45,10 @@ export interface Command { readonly label: string; /** One-line description shown as muted subtext in the suggestion row. */ readonly description: string; + /** Human-facing docs metadata used to generate docs/commands.md. */ + readonly docs: CommandDocs; + /** Model-facing metadata used to generate the slash-command prompt appendix. */ + readonly promptHelp: CommandPromptHelp; /** Prompt template with $INPUT / $LANG placeholders. Absent for non-template commands. */ readonly promptTemplate?: string; } @@ -22,21 +58,101 @@ export const COMMANDS: readonly Command[] = [ trigger: '/search', label: '/search', description: 'Agentic web search: iterative reasoning & cited synthesis', + docs: { + summary: + 'Runs agentic web search and answers from live sources with citations.', + usage: '/search ', + examples: [ + '`/search who owns Figma now?`: searches live sources for a current answer', + '`/search latest React 19 release notes`: retrieves recent release information from the web', + ], + behavior: + "Routes the message through Thuki's local search pipeline instead of plain chat. Answers are grounded in retrieved web sources and typically include inline citations plus a Sources footer.", + limit: + 'Requires the search sandbox to be running. Use it for current, changing, or cutoff-sensitive information.', + }, + promptHelp: { + summary: 'agentic web search for current or cutoff-sensitive questions.', + whenToSuggest: + 'Mention this when the user asks for current web information, live prices, recent releases, current ownership, or facts likely newer than the model cutoff.', + limit: + 'Do not claim to have searched the web without `/search`. `/search` requires the local search sandbox.', + }, }, { trigger: '/screen', label: '/screen', description: 'Capture your screen and include it as context', + docs: { + summary: + 'Captures your screen and attaches it as context for the current message.', + usage: '/screen [optional message]', + examples: [ + '`/screen`: sends a screenshot with no additional message', + '`/screen what is this error?`: attaches a screenshot and asks a question about it', + ], + behavior: + "The screenshot is taken when you submit the message. Thuki's own window is excluded from the capture, and the image appears in your message bubble like a pasted screenshot.", + composability: + '`/screen` can combine with `/think` and utility commands. For example, `/screen /rewrite` captures the screen and rewrites whatever text the model can see.', + limit: + 'One `/screen` capture per message. You may also attach up to 3 images manually for a total of 4 images per message.', + permission: + 'Requires Screen Recording permission. If denied, Thuki cannot capture the screen until access is granted in System Settings.', + }, + promptHelp: { + summary: 'capture current screen and attach it as image context.', + composition: + 'Can combine with `/think` and utility commands in the same message.', + limit: + 'One `/screen` capture per message and it requires Screen Recording permission.', + }, }, { trigger: '/think', label: '/think', description: 'Think deeply before answering', + docs: { + summary: 'Enables extended reasoning before the model responds.', + usage: '/think [optional message or highlighted text]', + examples: [ + '`/think` with highlighted text: reasons through the selected content', + '`/think what are the tradeoffs of a monorepo vs polyrepo?`: asks a question with deep reasoning enabled', + ], + behavior: + "A collapsible Thinking block appears above the response showing the model's reasoning chain. The final answer appears below it as normal.", + composability: + '`/think` works with `/screen` and all utility commands. For example, `/think /tldr` summarizes with extended reasoning enabled.', + }, + promptHelp: { + summary: 'enable extended reasoning before answering.', + composition: 'Can combine with `/screen` and utility commands.', + }, }, { trigger: '/translate', label: '/translate', description: 'Translate text to another language', + docs: { + summary: 'Translates text to another language.', + usage: '/translate [language] [text] or /translate with highlighted text', + examples: [ + '`/translate` with highlighted text: auto-detects the source language and translates it', + '`/translate ja` with highlighted text: translates highlighted text to Japanese', + '`/translate Spanish meeting notes here`: translates typed text to Spanish', + ], + behavior: + 'Outputs only the translation with no commentary or explanation.', + languageFormat: + 'The target language can be a full name (`French`), ISO code (`fr`, `fra`), or common shorthand.', + defaultBehavior: + 'If no language is specified, non-English input translates to English and English input translates to Vietnamese.', + }, + promptHelp: { + summary: 'translate selected or typed text to requested language.', + limit: + 'If no language is given, non-English input goes to English and English input goes to Vietnamese.', + }, promptTemplate: 'You are a translation assistant. Translate the following text to the specified target language. The user may specify the target language by its full name (e.g., "Vietnamese"), ISO code (e.g., "vi", "vie"), abbreviation, or informal shorthand. Interpret the language identifier flexibly and use your best judgment. If no target language is specified: translate to English if the text is non-English, or to Vietnamese if it is already in English. Output only the translation with no commentary or explanation.\n\nTarget language: $LANG\n\nText: $INPUT', }, @@ -44,6 +160,19 @@ export const COMMANDS: readonly Command[] = [ trigger: '/rewrite', label: '/rewrite', description: 'Rewrite text for clarity and flow', + docs: { + summary: 'Rewrites text to read more naturally and clearly.', + usage: '/rewrite [text] or /rewrite with highlighted text', + examples: [ + '`/rewrite` with highlighted text: rewrites the selected text', + '`/rewrite so basically what happened was i was trying to fix the bug`: rewrites typed text for clarity', + ], + behavior: + 'Preserves the original meaning while improving flow and readability. Outputs only the rewritten text.', + }, + promptHelp: { + summary: 'rewrite text for clarity and flow.', + }, promptTemplate: 'Please help rewrite the text below so it reads naturally and smoothly. Make it clear, easy to understand, and easy to follow. No icons, no em dashes. Please output only the rewritten text.\n\nText: $INPUT', }, @@ -51,6 +180,19 @@ export const COMMANDS: readonly Command[] = [ trigger: '/tldr', label: '/tldr', description: 'Summarize text in 1-3 sentences', + docs: { + summary: 'Summarizes text into 1-3 short, direct sentences.', + usage: '/tldr [text] or /tldr with highlighted text', + examples: [ + '`/tldr` with highlighted text: summarizes the selected content', + '`/tldr [paste a long article]`: summarizes typed or pasted text', + ], + behavior: + 'Captures the core message, key decision, or critical takeaway. Skips background detail and qualifications.', + }, + promptHelp: { + summary: 'summarize text in 1-3 short direct sentences.', + }, promptTemplate: "Summarize the following text into a TL;DR. Capture the core message in 1-3 short, direct sentences. Focus on what matters most: the main point, the key decision, or the critical takeaway. Skip background details, qualifications, and anything that isn't essential to understanding the gist. Output only the summary.\n\nText: $INPUT", }, @@ -58,6 +200,21 @@ export const COMMANDS: readonly Command[] = [ trigger: '/refine', label: '/refine', description: 'Fix grammar, spelling, and punctuation', + docs: { + summary: + 'Fixes grammar, spelling, and punctuation while preserving your voice.', + usage: '/refine [text] or /refine with highlighted text', + examples: [ + '`/refine` with highlighted text: corrects the selected text', + '`/refine hey just wanted to follow up on the thing we discussed`: cleans up typed text', + ], + behavior: + 'Corrects errors and smooths rough phrasing without restructuring or adding new ideas. Your original tone and meaning stay intact.', + }, + promptHelp: { + summary: + 'fix grammar, spelling, punctuation, and rough phrasing while preserving tone.', + }, promptTemplate: 'Refine the following text by correcting grammar, spelling, punctuation, and awkward phrasing. Keep the original tone, voice, and meaning intact. Do not restructure paragraphs, add new ideas, or remove content. If a sentence is grammatically correct but stylistically rough, smooth it lightly without changing the intent. Output only the refined text.\n\nText: $INPUT', }, @@ -65,6 +222,19 @@ export const COMMANDS: readonly Command[] = [ trigger: '/bullets', label: '/bullets', description: 'Extract key points as a bullet list', + docs: { + summary: 'Extracts key points from text as a markdown bullet list.', + usage: '/bullets [text] or /bullets with highlighted text', + examples: [ + '`/bullets` with highlighted text: extracts key points from the selection', + '`/bullets [paste meeting notes]`: extracts key points from typed or pasted content', + ], + behavior: + 'Each point is a concise, self-contained statement. Ordered by importance or logical sequence. Filler and repetition are removed. Output uses `- ` prefixed markdown bullets.', + }, + promptHelp: { + summary: 'extract key points as markdown bullets.', + }, promptTemplate: 'Extract the key points from the following text as a bulleted list. Each item must begin with "- " (a hyphen followed by a space). Do not use numbered lists, plain paragraphs, headers, or any other formatting. Output only the bulleted list, nothing else.\n\nExample output format:\n- First key point\n- Second key point\n- Third key point\n\nEach bullet should be a concise, self-contained statement. Order by importance or logical sequence. Leave out filler and repetition.\n\nText: $INPUT', }, @@ -72,6 +242,20 @@ export const COMMANDS: readonly Command[] = [ trigger: '/todos', label: '/todos', description: 'Extract to-do items as a checkbox list', + docs: { + summary: + 'Summarizes what a piece of text is about, then extracts every task, action item, and commitment as a markdown checkbox list.', + usage: '/todos [text] or /todos with highlighted text', + examples: [ + '`/todos` with highlighted text: summarizes and extracts to-dos from the selected text', + '`/todos [paste a conversation or notes]`: processes typed or pasted content', + ], + behavior: + 'Responds in two parts: a short paragraph explaining the context and what is at stake, followed by a `- [ ]` checkbox list of all tasks. Each to-do includes who is responsible, plus any deadline or timeframe if mentioned.', + }, + promptHelp: { + summary: 'summarize context and extract tasks as markdown checkboxes.', + }, promptTemplate: 'Read the following text and respond in two parts:\n\n**Part 1: Summary.** Write a short paragraph (3-5 sentences) explaining what this text is about. Cover: what the situation or topic is, who is involved, what the current state is, and why it matters or what is at stake. This should give someone who has not read the original text a clear picture of the context.\n\n**Part 2: To-dos.** List every task, action item, commitment, and follow-up from the text as a markdown checkbox list. Every single item MUST begin with "- [ ] " (hyphen, space, open bracket, space, close bracket, space). Do not use numbered lists, plain bullets, headers, or any other format for the list items.\n\nSeparate the two parts with a blank line. Do not add any headings or labels like "Summary:" or "To-dos:"; just write the paragraph, then the list.\n\nExample output format:\nThis is a paragraph explaining what the text is about, who is involved, and what the situation is. It gives enough context to understand why the tasks matter. It is clear and direct.\n\n- [ ] First task to complete\n- [ ] Second task to complete\n- [ ] Third task to complete\n\nFor each to-do item, include who is responsible (if mentioned), what needs to be done, and any deadline or timeframe (if mentioned). Order by urgency or sequence when possible.\n\nText: $INPUT', },