feat(local-llm): Phase B quick actions + command palette (B1-B6)

- add fuse.js dependency and command palette modal (Cmd+K)
- add built-in quick actions library (30 templates across categories)
- add quick action CRUD + seeding + import/export helpers in db layer
- seed quick actions on workspace load and list top actions in sidebar
- implement quick action launcher -> creates preconfigured conversation
- add custom quick action editor modal for creating/editing actions
- wire command palette system actions and conversation navigation
- support passing QA template into conversation input via query param
This commit is contained in:
saravanakumardb1 2026-02-20 00:19:17 -08:00
parent 1335d47869
commit 7ae92da16e
10 changed files with 934 additions and 4 deletions

View File

@ -10,6 +10,7 @@
},
"dependencies": {
"@types/react-syntax-highlighter": "^15.5.13",
"fuse.js": "^7.1.0",
"idb": "^8.0.3",
"lucide-react": "^0.575.0",
"next": "16.1.6",

View File

@ -2,11 +2,13 @@
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { getConversation, getMessages } from '../../../lib/db';
import type { Conversation, Message } from '../../../lib/types';
import { ConversationView } from '../../components/ConversationView';
export default function ConversationPage({ params }: { params: { id: string } }) {
const searchParams = useSearchParams();
const [conversation, setConversation] = useState<Conversation | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [loading, setLoading] = useState(true);
@ -47,5 +49,11 @@ export default function ConversationPage({ params }: { params: { id: string } })
);
}
return <ConversationView conversation={conversation} initialMessages={messages} />;
return (
<ConversationView
conversation={conversation}
initialMessages={messages}
initialTemplate={searchParams.get('template') || ''}
/>
);
}

View File

@ -0,0 +1,169 @@
'use client';
import { useMemo, useState } from 'react';
import Fuse from 'fuse.js';
import type { Conversation, QuickAction } from '../../lib/types';
type CommandItem =
| {
type: 'qa';
id: string;
name: string;
description: string;
icon: string;
payload: QuickAction;
}
| {
type: 'conversation';
id: string;
name: string;
description: string;
icon: string;
payload: Conversation;
}
| {
type: 'system';
id: string;
name: string;
description: string;
icon: string;
action: 'mission-control' | 'settings' | 'export';
};
interface CommandPaletteProps {
open: boolean;
quickActions: QuickAction[];
conversations: Conversation[];
onClose: () => void;
onSelectQuickAction: (qa: QuickAction) => void;
onSelectConversation: (conversation: Conversation) => void;
onSelectSystem: (action: 'mission-control' | 'settings' | 'export') => void;
}
export function CommandPalette({
open,
quickActions,
conversations,
onClose,
onSelectQuickAction,
onSelectConversation,
onSelectSystem,
}: CommandPaletteProps) {
const [query, setQuery] = useState('');
const items = useMemo<CommandItem[]>(() => {
const qaItems: CommandItem[] = quickActions.map(qa => ({
type: 'qa',
id: qa.id,
name: qa.name,
description: qa.description,
icon: qa.icon,
payload: qa,
}));
const convItems: CommandItem[] = conversations.slice(0, 20).map(conv => ({
type: 'conversation',
id: conv.id,
name: conv.title,
description: conv.model || 'Conversation',
icon: '💬',
payload: conv,
}));
const systemItems: CommandItem[] = [
{
type: 'system',
id: 'sys-mission-control',
name: 'Mission Control',
description: 'Open model and system dashboard',
icon: '🎛️',
action: 'mission-control',
},
{
type: 'system',
id: 'sys-settings',
name: 'Settings',
description: 'Open settings panel',
icon: '⚙️',
action: 'settings',
},
{
type: 'system',
id: 'sys-export',
name: 'Export Data',
description: 'Export workspace data',
icon: '📦',
action: 'export',
},
];
return [...qaItems, ...convItems, ...systemItems];
}, [quickActions, conversations]);
const results = useMemo(() => {
if (!query.trim()) return items.slice(0, 15);
const fuse = new Fuse(items, {
keys: ['name', 'description'],
threshold: 0.35,
ignoreLocation: true,
includeScore: true,
});
return fuse
.search(query)
.map(r => r.item)
.slice(0, 15);
}, [items, query]);
if (!open) return null;
return (
<div
className="fixed inset-0 z-50 flex items-start justify-center bg-black/50 p-4"
onClick={onClose}
>
<div
className="mt-16 w-full max-w-2xl rounded-lg border border-white/10 bg-[var(--surface-card)] p-3 shadow-2xl"
onClick={e => e.stopPropagation()}
>
<input
autoFocus
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search actions, conversations, commands..."
className="mb-3 w-full rounded-md border border-white/10 bg-[var(--surface-muted)] px-3 py-2 text-sm text-[var(--text-primary)] outline-none placeholder:text-[var(--text-tertiary)]"
onKeyDown={e => {
if (e.key === 'Escape') onClose();
}}
/>
<div className="max-h-[60vh] space-y-1 overflow-auto">
{results.map(item => (
<button
key={`${item.type}-${item.id}`}
type="button"
onClick={() => {
if (item.type === 'qa') onSelectQuickAction(item.payload);
if (item.type === 'conversation') onSelectConversation(item.payload);
if (item.type === 'system') onSelectSystem(item.action);
onClose();
}}
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left hover:bg-white/5"
>
<span className="text-base">{item.icon}</span>
<div className="min-w-0 flex-1">
<p className="truncate text-sm text-[var(--text-primary)]">{item.name}</p>
<p className="truncate text-xs text-[var(--text-tertiary)]">{item.description}</p>
</div>
<span className="rounded border border-white/10 px-2 py-0.5 text-[10px] uppercase text-[var(--text-tertiary)]">
{item.type}
</span>
</button>
))}
{results.length === 0 && (
<p className="px-2 py-6 text-center text-sm text-[var(--text-tertiary)]">No results</p>
)}
</div>
</div>
</div>
);
}

View File

@ -12,12 +12,14 @@ import { estimateTokens, getModelContextWindow } from '../../lib/format';
interface ConversationViewProps {
conversation: Conversation;
initialMessages: Message[];
initialTemplate?: string;
onConversationUpdated?: (next: Conversation) => void;
}
export function ConversationView({
conversation,
initialMessages,
initialTemplate,
onConversationUpdated,
}: ConversationViewProps) {
const [title, setTitle] = useState(conversation.title);
@ -269,6 +271,7 @@ export function ConversationView({
streaming={streaming}
onCancel={onCancel}
disabled={!selectedModel}
initialText={initialTemplate || ''}
/>
</div>
);

View File

@ -0,0 +1,205 @@
'use client';
import { useEffect, useMemo, useState } from 'react';
import type { QuickAction } from '../../lib/types';
interface QuickActionEditorProps {
open: boolean;
editing?: QuickAction | null;
onClose: () => void;
onSave: (action: QuickAction) => Promise<void>;
}
const CATEGORIES: QuickAction['category'][] = [
'code',
'writing',
'analysis',
'creative',
'devops',
'custom',
];
const MODEL_HINTS = ['fast', 'coding', 'reasoning', 'chat', 'vision'];
export function QuickActionEditor({ open, editing, onClose, onSave }: QuickActionEditorProps) {
const [name, setName] = useState('');
const [icon, setIcon] = useState('✨');
const [category, setCategory] = useState<QuickAction['category']>('custom');
const [description, setDescription] = useState('');
const [modelHint, setModelHint] = useState('chat');
const [systemPrompt, setSystemPrompt] = useState('');
const [userTemplate, setUserTemplate] = useState('');
const [hotkey, setHotkey] = useState('');
const [saving, setSaving] = useState(false);
useEffect(() => {
if (!open) return;
if (editing) {
setName(editing.name);
setIcon(editing.icon);
setCategory(editing.category);
setDescription(editing.description);
setModelHint(editing.modelHint);
setSystemPrompt(editing.systemPrompt);
setUserTemplate(editing.userTemplate);
setHotkey(editing.hotkey || '');
return;
}
setName('');
setIcon('✨');
setCategory('custom');
setDescription('');
setModelHint('chat');
setSystemPrompt('');
setUserTemplate('');
setHotkey('');
}, [open, editing]);
const canSave = useMemo(() => {
return name.trim().length > 0 && systemPrompt.trim().length > 0;
}, [name, systemPrompt]);
if (!open) return null;
return (
<div
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 p-4"
onClick={onClose}
>
<div
className="w-full max-w-2xl rounded-lg border border-white/10 bg-[var(--surface-card)] p-4"
onClick={e => e.stopPropagation()}
>
<h2 className="mb-3 text-lg font-semibold text-[var(--text-primary)]">
{editing ? 'Edit Quick Action' : 'Create Quick Action'}
</h2>
<div className="grid gap-3 md:grid-cols-2">
<label className="text-sm text-[var(--text-secondary)]">
Name
<input
value={name}
onChange={e => setName(e.target.value)}
className="mt-1 w-full rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-1 text-[var(--text-primary)] outline-none"
/>
</label>
<label className="text-sm text-[var(--text-secondary)]">
Icon
<input
value={icon}
onChange={e => setIcon(e.target.value)}
className="mt-1 w-full rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-1 text-[var(--text-primary)] outline-none"
/>
</label>
<label className="text-sm text-[var(--text-secondary)]">
Category
<select
value={category}
onChange={e => setCategory(e.target.value as QuickAction['category'])}
className="mt-1 w-full rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-1 text-[var(--text-primary)] outline-none"
>
{CATEGORIES.map(item => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</label>
<label className="text-sm text-[var(--text-secondary)]">
Model Hint
<select
value={modelHint}
onChange={e => setModelHint(e.target.value)}
className="mt-1 w-full rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-1 text-[var(--text-primary)] outline-none"
>
{MODEL_HINTS.map(item => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</label>
<label className="md:col-span-2 text-sm text-[var(--text-secondary)]">
Description
<input
value={description}
onChange={e => setDescription(e.target.value)}
className="mt-1 w-full rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-1 text-[var(--text-primary)] outline-none"
/>
</label>
<label className="md:col-span-2 text-sm text-[var(--text-secondary)]">
System Prompt
<textarea
rows={5}
value={systemPrompt}
onChange={e => setSystemPrompt(e.target.value)}
className="mt-1 w-full rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-1 text-[var(--text-primary)] outline-none"
/>
</label>
<label className="md:col-span-2 text-sm text-[var(--text-secondary)]">
User Template
<textarea
rows={4}
value={userTemplate}
onChange={e => setUserTemplate(e.target.value)}
className="mt-1 w-full rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-1 text-[var(--text-primary)] outline-none"
/>
</label>
<label className="text-sm text-[var(--text-secondary)]">
Hotkey (optional)
<input
value={hotkey}
onChange={e => setHotkey(e.target.value)}
className="mt-1 w-full rounded border border-white/10 bg-[var(--surface-muted)] px-2 py-1 text-[var(--text-primary)] outline-none"
/>
</label>
</div>
<div className="mt-4 flex justify-end gap-2">
<button
type="button"
onClick={onClose}
className="rounded border border-white/10 px-3 py-1 text-sm text-[var(--text-secondary)] hover:bg-white/5"
>
Cancel
</button>
<button
type="button"
disabled={!canSave || saving}
onClick={async () => {
setSaving(true);
const action: QuickAction = {
id: editing?.id || crypto.randomUUID(),
name: name.trim(),
icon: icon.trim() || '✨',
category,
description: description.trim(),
modelHint,
systemPrompt: systemPrompt.trim(),
userTemplate: userTemplate.trim(),
builtin: false,
hotkey: hotkey.trim() || undefined,
usageCount: editing?.usageCount ?? 0,
lastUsed: editing?.lastUsed,
};
await onSave(action);
setSaving(false);
onClose();
}}
className="rounded bg-[var(--accent-primary)] px-3 py-1 text-sm font-medium text-white disabled:cursor-not-allowed disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save'}
</button>
</div>
</div>
</div>
);
}

View File

@ -11,7 +11,7 @@ import {
Sparkles,
UserRound,
} from 'lucide-react';
import type { Conversation } from '../../lib/types';
import type { Conversation, QuickAction } from '../../lib/types';
import type { ReactNode } from 'react';
import { ConversationList } from './ConversationList';
@ -21,6 +21,8 @@ interface SidebarProps {
onNewConversation: () => void;
activeConversationId?: string;
onSelectConversation: (id: string) => void;
quickActions: QuickAction[];
onLaunchQuickAction: (qa: QuickAction) => void;
}
export function Sidebar({
@ -29,6 +31,8 @@ export function Sidebar({
onNewConversation,
activeConversationId,
onSelectConversation,
quickActions,
onLaunchQuickAction,
}: SidebarProps) {
const [query, setQuery] = useState('');
const filtered = useMemo(() => {
@ -36,6 +40,7 @@ export function Sidebar({
if (!q) return conversations;
return conversations.filter(c => c.title.toLowerCase().includes(q));
}, [conversations, query]);
const topQuickActions = useMemo(() => quickActions.slice(0, 5), [quickActions]);
return (
<aside
@ -67,6 +72,27 @@ export function Sidebar({
/>
</nav>
{!collapsed && topQuickActions.length > 0 && (
<section>
<p className="mb-1 text-[10px] uppercase tracking-wide text-[var(--text-tertiary)]">
Frequently-used Quick Actions
</p>
<div className="space-y-1">
{topQuickActions.map(qa => (
<button
key={qa.id}
type="button"
onClick={() => onLaunchQuickAction(qa)}
className="flex w-full items-center gap-2 rounded px-2 py-1 text-left text-xs text-[var(--text-secondary)] hover:bg-white/5 hover:text-[var(--text-primary)]"
>
<span>{qa.icon}</span>
<span className="truncate">{qa.name}</span>
</button>
))}
</div>
</section>
)}
<div className="my-1 border-t border-white/10" />
<div className="min-h-0 flex-1 overflow-auto">

View File

@ -3,15 +3,27 @@
import { useEffect, useState } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import { Sidebar } from './components/Sidebar';
import { addConversation, listConversations } from '../lib/db';
import type { Conversation } from '../lib/types';
import {
addQuickAction,
addConversation,
listConversations,
listQuickActions,
seedBuiltinQuickActions,
updateQuickAction,
} from '../lib/db';
import type { Conversation, QuickAction } from '../lib/types';
import { migrateV3ToV4 } from '../lib/migrate';
import { CommandPalette } from './components/CommandPalette';
import { QuickActionEditor } from './components/QuickActionEditor';
export default function WorkspaceLayout({ children }: { children: React.ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const [collapsed, setCollapsed] = useState(false);
const [conversations, setConversations] = useState<Conversation[]>([]);
const [quickActions, setQuickActions] = useState<QuickAction[]>([]);
const [paletteOpen, setPaletteOpen] = useState(false);
const [editorOpen, setEditorOpen] = useState(false);
useEffect(() => {
const saved = localStorage.getItem('llm-sidebar-state');
@ -28,6 +40,15 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod
return next;
});
}
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
e.preventDefault();
setPaletteOpen(true);
}
if (e.key === 'Escape') {
setPaletteOpen(false);
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
@ -36,12 +57,49 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod
useEffect(() => {
const load = async () => {
await migrateV3ToV4();
await seedBuiltinQuickActions();
const rows = await listConversations({ archived: false, limit: 100 });
const qas = await listQuickActions();
setConversations(rows);
setQuickActions(qas);
};
void load();
}, [pathname]);
const launchQuickAction = async (qa: QuickAction) => {
const now = Date.now();
const conv: Conversation = {
id: crypto.randomUUID(),
title: qa.name,
model: qa.modelHint,
systemPrompt: qa.systemPrompt,
messageCount: 0,
createdAt: now,
updatedAt: now,
pinned: false,
archived: false,
metadata: {
totalTokens: 0,
totalPrompts: 0,
avgTokPerSec: 0,
models: qa.modelHint ? [qa.modelHint] : [],
},
};
await addConversation(conv);
await updateQuickAction(qa.id, { usageCount: qa.usageCount + 1, lastUsed: now });
setConversations(prev => [conv, ...prev]);
setQuickActions(prev =>
prev
.map(item =>
item.id === qa.id ? { ...item, usageCount: item.usageCount + 1, lastUsed: now } : item
)
.sort((a, b) => b.usageCount - a.usageCount)
);
const template = encodeURIComponent(qa.userTemplate);
router.push(`/c/${conv.id}?template=${template}`);
};
const onNewConversation = async () => {
const now = Date.now();
const conv: Conversation = {
@ -75,8 +133,38 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod
onNewConversation={onNewConversation}
activeConversationId={activeConversationId}
onSelectConversation={id => router.push(`/c/${id}`)}
quickActions={quickActions}
onLaunchQuickAction={launchQuickAction}
/>
<main className="min-w-0 flex-1">{children}</main>
<CommandPalette
open={paletteOpen}
quickActions={quickActions}
conversations={conversations}
onClose={() => setPaletteOpen(false)}
onSelectQuickAction={launchQuickAction}
onSelectConversation={conversation => router.push(`/c/${conversation.id}`)}
onSelectSystem={action => {
if (action === 'mission-control') router.push('/mission-control');
if (action === 'settings') {
setEditorOpen(true);
}
if (action === 'export') {
alert('Data export will be wired in a follow-up phase.');
}
}}
/>
<QuickActionEditor
open={editorOpen}
onClose={() => setEditorOpen(false)}
onSave={async action => {
await addQuickAction(action);
const qas = await listQuickActions();
setQuickActions(qas);
}}
/>
</div>
);
}

View File

@ -8,6 +8,7 @@ import type {
QuickAction,
ScheduledTask,
} from './types';
import { BUILTIN_QUICK_ACTIONS } from './quick-actions';
interface TaskRunRecord {
id?: number;
@ -234,3 +235,83 @@ export async function countMessages(conversationId: string): Promise<number> {
const range = IDBKeyRange.bound([conversationId, 0], [conversationId, Number.MAX_SAFE_INTEGER]);
return db.countFromIndex('messages', 'by-conversation-ts', range);
}
export async function addQuickAction(action: QuickAction): Promise<void> {
const db = await getDb();
await db.put('quickActions', action);
}
export async function getQuickAction(id: string): Promise<QuickAction | undefined> {
const db = await getDb();
return db.get('quickActions', id);
}
export async function listQuickActions(category?: QuickAction['category']): Promise<QuickAction[]> {
const db = await getDb();
let rows = await db.getAll('quickActions');
if (category) {
rows = rows.filter(row => row.category === category);
}
rows.sort((a, b) => {
if (a.builtin !== b.builtin) return a.builtin ? -1 : 1;
return b.usageCount - a.usageCount;
});
return rows;
}
export async function updateQuickAction(
id: string,
partial: Partial<QuickAction>
): Promise<QuickAction | null> {
const db = await getDb();
const existing = await db.get('quickActions', id);
if (!existing) return null;
const updated: QuickAction = { ...existing, ...partial, id };
await db.put('quickActions', updated);
return updated;
}
export async function deleteQuickAction(id: string): Promise<void> {
const db = await getDb();
await db.delete('quickActions', id);
}
export async function seedBuiltinQuickActions(): Promise<number> {
const seeded = typeof window !== 'undefined' ? localStorage.getItem('llm-qa-seeded') : 'true';
if (seeded === 'true') return 0;
const db = await getDb();
const tx = db.transaction('quickActions', 'readwrite');
let inserted = 0;
for (const action of BUILTIN_QUICK_ACTIONS) {
const existing = await tx.store.get(action.id);
if (!existing) {
await tx.store.put(action);
inserted += 1;
}
}
await tx.done;
if (typeof window !== 'undefined') {
localStorage.setItem('llm-qa-seeded', 'true');
}
return inserted;
}
export async function exportQuickActions(): Promise<QuickAction[]> {
const rows = await listQuickActions();
return rows.filter(row => !row.builtin);
}
export async function importQuickActions(actions: QuickAction[]): Promise<number> {
const db = await getDb();
let imported = 0;
for (const action of actions) {
const existing = await db.get('quickActions', action.id);
if (existing) continue;
await db.put('quickActions', action);
imported += 1;
}
return imported;
}

View File

@ -0,0 +1,337 @@
import type { QuickAction } from './types';
function qa(
id: string,
name: string,
icon: string,
category: QuickAction['category'],
modelHint: string,
description: string,
systemPrompt: string,
userTemplate: string
): QuickAction {
return {
id,
name,
icon,
category,
modelHint,
description,
systemPrompt,
userTemplate,
builtin: true,
usageCount: 0,
};
}
export const BUILTIN_QUICK_ACTIONS: QuickAction[] = [
// Code (8)
qa(
'builtin-code-review',
'Code Review',
'🔍',
'code',
'reasoning',
'Review code for bugs, perf, and security',
'You are a senior code reviewer. Find bugs, security issues, performance issues, and maintainability improvements. Reference concrete code lines and provide fixes.',
'Review this code:\n\n{paste code}'
),
qa(
'builtin-explain-code',
'Explain Code',
'📖',
'code',
'fast',
'Explain what code does',
'Explain the provided code in clear simple language. Break down each section and call out important patterns.',
'Explain this code:\n\n{paste code}'
),
qa(
'builtin-write-tests',
'Write Tests',
'🧪',
'code',
'coding',
'Generate unit tests',
'You are a test engineer. Generate comprehensive unit tests with edge cases and clear test names.',
'Write tests for:\n\n{paste code}'
),
qa(
'builtin-debug-error',
'Debug Error',
'🐛',
'code',
'reasoning',
'Root-cause debugging assistant',
'You are a debugging specialist. Identify root cause, explain why, and provide corrected code.',
'Error:\n{paste error}\n\nCode:\n{paste code}'
),
qa(
'builtin-refactor',
'Refactor',
'♻️',
'code',
'coding',
'Refactor for readability and performance',
'Refactor the code for readability, maintainability, and performance without changing behavior.',
'Refactor this:\n\n{paste code}'
),
qa(
'builtin-generate-code',
'Generate Code',
'⚡',
'code',
'coding',
'Generate production-ready code',
'Generate robust production-ready code with clear comments only where needed.',
'Build this:\n\n{describe requirement}'
),
qa(
'builtin-regex-builder',
'Regex Builder',
'🔤',
'code',
'fast',
'Build and explain regex patterns',
'You are a regex expert. Build the requested regex and explain each part with test examples.',
'Create regex for:\n\n{describe pattern}'
),
qa(
'builtin-sql-query',
'SQL Query',
'🗄️',
'code',
'coding',
'Generate optimized SQL queries',
'You are a SQL expert. Generate efficient SQL and explain indexing/performance implications.',
'Write SQL for:\n\n{describe}\n\nSchema:\n{paste schema}'
),
// Writing (6)
qa(
'builtin-summarize',
'Summarize',
'📝',
'writing',
'fast',
'Summarize content into key points',
'Summarize the content clearly with concise bullet points and key takeaways.',
'Summarize:\n\n{paste text}'
),
qa(
'builtin-rewrite',
'Rewrite',
'✏️',
'writing',
'chat',
'Rewrite text for clarity/tone',
'Rewrite while preserving meaning and improving clarity, flow, and tone.',
'Rewrite this:\n\n{paste text}'
),
qa(
'builtin-translate',
'Translate',
'🌐',
'writing',
'chat',
'Translate while preserving nuance',
'Translate accurately preserving tone and intent. Provide alternate phrasing if ambiguous.',
'Translate to {language}:\n\n{paste text}'
),
qa(
'builtin-proofread',
'Proofread',
'✅',
'writing',
'fast',
'Proofread grammar and style',
'Proofread grammar, spelling, punctuation, and style. Return corrected version and notable issues.',
'Proofread:\n\n{paste text}'
),
qa(
'builtin-draft-email',
'Draft Email',
'📧',
'writing',
'chat',
'Draft professional email',
'Draft concise professional emails with clear subject, action items, and tone.',
'Draft email to {recipient} about {topic}.'
),
qa(
'builtin-tech-doc',
'Technical Docs',
'📄',
'writing',
'coding',
'Write technical documentation',
'Write clear technical documentation with examples, edge cases, and concise structure.',
'Document this:\n\n{paste code or feature}'
),
// Analysis (6)
qa(
'builtin-deep-think',
'Deep Think',
'🧠',
'analysis',
'reasoning',
'Step-by-step deep analysis',
'Think carefully and step-by-step. Compare alternatives and provide recommendation.',
'Think through:\n\n{problem}'
),
qa(
'builtin-compare-options',
'Compare Options',
'⚖️',
'analysis',
'reasoning',
'Compare alternatives with trade-offs',
'Compare options objectively with pros/cons matrix and recommendation.',
'Compare:\n\n{option A}\nvs\n{option B}'
),
qa(
'builtin-eli5',
'Explain Like Im 5',
'👶',
'analysis',
'chat',
'Explain complex ideas simply',
'Explain complex concepts to a beginner using analogies and plain language.',
'Explain simply:\n\n{topic}'
),
qa(
'builtin-fact-check',
'Fact Check',
'🔎',
'analysis',
'reasoning',
'Check claims and caveats',
'Evaluate factual claims. Separate confirmed facts, likely true, and uncertain points.',
'Fact-check:\n\n{claim}'
),
qa(
'builtin-risk-review',
'Risk Review',
'⚠️',
'analysis',
'reasoning',
'Analyze implementation risks',
'Identify technical, security, and operational risks. Provide mitigations and severity.',
'Review risks for:\n\n{proposal}'
),
qa(
'builtin-root-cause',
'Root Cause Analysis',
'🧩',
'analysis',
'reasoning',
'Find root causes systematically',
'Perform root cause analysis and provide 5 whys plus actionable fixes.',
'Analyze root cause:\n\n{incident description}'
),
// Creative (5)
qa(
'builtin-brainstorm',
'Brainstorm',
'💡',
'creative',
'chat',
'Generate diverse ideas',
'Generate diverse ideas including practical and unconventional options.',
'Brainstorm ideas for:\n\n{topic}'
),
qa(
'builtin-role-play',
'Role Play',
'🎭',
'creative',
'chat',
'Role-play scenario',
'Stay in character and respond consistently with persona and context.',
'Role-play:\n\n{scenario}'
),
qa(
'builtin-story-writer',
'Story Writer',
'📚',
'creative',
'chat',
'Write story content',
'Write engaging story content with strong pacing and vivid details.',
'Write a story about:\n\n{premise}'
),
qa(
'builtin-name-generator',
'Name Generator',
'🏷️',
'creative',
'fast',
'Generate names with rationale',
'Generate creative names and explain why each name works.',
'Generate names for:\n\n{product or concept}'
),
qa(
'builtin-pitch-maker',
'Pitch Maker',
'🎤',
'creative',
'chat',
'Create a concise pitch',
'Craft a concise compelling pitch with hook, value, and call to action.',
'Pitch this idea:\n\n{idea}'
),
// DevOps (5)
qa(
'builtin-shell-command',
'Shell Command',
'💻',
'devops',
'coding',
'Generate safe shell commands',
'Generate safe macOS zsh commands. Warn explicitly for destructive commands.',
'Write command to:\n\n{task}'
),
qa(
'builtin-docker-k8s',
'Docker/K8s',
'🐳',
'devops',
'coding',
'Generate Docker/K8s configs',
'Generate secure Docker/K8s config with sensible defaults and brief rationale.',
'Generate config for:\n\n{service requirement}'
),
qa(
'builtin-git-help',
'Git Help',
'🌿',
'devops',
'fast',
'Provide git commands for scenarios',
'Provide exact git commands and explain each command briefly.',
'Help with git scenario:\n\n{scenario}'
),
qa(
'builtin-json-yaml',
'JSON/YAML Tool',
'📋',
'devops',
'fast',
'Convert/validate data formats',
'Convert and validate JSON/YAML with clear errors and fixed output.',
'Convert/validate:\n\n{data}'
),
qa(
'builtin-release-notes',
'Release Notes',
'🚀',
'devops',
'chat',
'Generate release notes from changes',
'Generate concise release notes grouped by feature/fix/chore.',
'Create release notes from:\n\n{change log}'
),
];

12
pnpm-lock.yaml generated
View File

@ -71,6 +71,9 @@ importers:
'@types/react-syntax-highlighter':
specifier: ^15.5.13
version: 15.5.13
fuse.js:
specifier: ^7.1.0
version: 7.1.0
idb:
specifier: ^8.0.3
version: 8.0.3
@ -3983,6 +3986,13 @@ packages:
integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==,
}
fuse.js@7.1.0:
resolution:
{
integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==,
}
engines: { node: '>=10' }
generator-function@2.0.1:
resolution:
{
@ -9581,6 +9591,8 @@ snapshots:
functions-have-names@1.2.3: {}
fuse.js@7.1.0: {}
generator-function@2.0.1: {}
gensync@1.0.0-beta.2: {}