Building an AI Chat App with DenchClaw
Build a fully functional AI chat app inside DenchClaw using the App Builder, window.dench bridge API, and your local DuckDB data in minutes.
Building an AI chat app with DenchClaw takes less time than you think. Using the App Builder and the window.dench bridge API, you can wire up a fully functional, context-aware chat interface that queries your local DuckDB, persists conversation history, and responds using the AI model powering DenchClaw — all in plain HTML, CSS, and JavaScript. No build tools. No backend. No API keys.
Here's exactly how to do it.
What You're Building#
By the end of this guide, you'll have a .dench.app that:
- Renders a chat interface in a DenchClaw panel
- Streams AI responses using
dench.chat - Queries your CRM data from DuckDB to give the AI real context
- Saves conversation history to
dench.store - Shows toast notifications on errors
Step 1: Create the .dench.app Folder#
Every DenchClaw app lives in a .dench.app folder inside your workspace. Start by creating the structure:
mkdir -p ~/denchclaw-workspace/apps/ai-chat.dench.app
cd ~/denchclaw-workspace/apps/ai-chat.dench.app
touch .dench.yaml
touch index.htmlStep 2: Write the Manifest#
The .dench.yaml file tells DenchClaw how to display and run your app. For a chat app, you want a full panel — not a widget:
name: "AI Chat"
description: "Context-aware AI chat with your CRM data"
display: "panel"
icon: "💬"
version: "1.0.0"If you wanted this as a floating widget instead, you'd set display: "widget" — but for a full chat interface, panel mode is the right call.
Step 3: Build the Chat UI#
Open index.html and build the interface. Here's a complete, production-quality implementation:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI Chat</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #0f0f0f;
color: #e5e5e5;
height: 100vh;
display: flex;
flex-direction: column;
}
#header {
padding: 16px 20px;
border-bottom: 1px solid #222;
font-weight: 600;
font-size: 15px;
display: flex;
align-items: center;
gap: 8px;
}
#messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.message {
max-width: 80%;
padding: 12px 16px;
border-radius: 12px;
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
}
.message.user {
align-self: flex-end;
background: #2563eb;
color: #fff;
border-bottom-right-radius: 4px;
}
.message.assistant {
align-self: flex-start;
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-bottom-left-radius: 4px;
}
.message.thinking {
opacity: 0.5;
font-style: italic;
}
#input-area {
padding: 16px 20px;
border-top: 1px solid #222;
display: flex;
gap: 10px;
}
#input {
flex: 1;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
padding: 10px 14px;
color: #e5e5e5;
font-size: 14px;
resize: none;
outline: none;
font-family: inherit;
}
#input:focus { border-color: #2563eb; }
#send-btn {
background: #2563eb;
color: white;
border: none;
border-radius: 8px;
padding: 10px 18px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
}
#send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
#context-bar {
padding: 8px 20px;
font-size: 12px;
color: #666;
background: #0a0a0a;
border-bottom: 1px solid #1a1a1a;
}
</style>
</head>
<body>
<div id="header">💬 AI Chat <span style="color:#666;font-weight:400;font-size:13px;">— powered by DenchClaw</span></div>
<div id="context-bar">Loading CRM context...</div>
<div id="messages"></div>
<div id="input-area">
<textarea id="input" rows="1" placeholder="Ask anything about your contacts, deals, or tasks..."></textarea>
<button id="send-btn">Send</button>
</div>
<script>
const messagesEl = document.getElementById('messages');
const inputEl = document.getElementById('input');
const sendBtn = document.getElementById('send-btn');
const contextBar = document.getElementById('context-bar');
let crmContext = '';
let isStreaming = false;
// Load conversation history from persistent store
const history = dench.store.get('chat-history') || [];
// Restore previous messages
history.forEach(msg => appendMessage(msg.role, msg.content));
// Build CRM context from DuckDB
async function loadCRMContext() {
try {
const contacts = await dench.db.query(`
SELECT o.name, ef.value as status
FROM objects o
LEFT JOIN entry_fields ef ON ef.entry_id = (
SELECT e.id FROM entries e WHERE e.object_id = o.id LIMIT 1
)
LEFT JOIN fields f ON f.id = ef.field_id AND f.name = 'Status'
LIMIT 20
`);
const count = contacts.length;
crmContext = `You have access to a CRM with ${count} contacts. Recent contacts: ${contacts.slice(0, 5).map(c => c.name).join(', ')}.`;
contextBar.textContent = `CRM loaded: ${count} contacts available as context`;
} catch (err) {
contextBar.textContent = 'CRM context unavailable — chatting without data';
}
}
function appendMessage(role, content, isThinking = false) {
const div = document.createElement('div');
div.className = `message ${role}${isThinking ? ' thinking' : ''}`;
div.textContent = content;
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
return div;
}
async function sendMessage() {
const text = inputEl.value.trim();
if (!text || isStreaming) return;
isStreaming = true;
sendBtn.disabled = true;
inputEl.value = '';
// Add user message
appendMessage('user', text);
history.push({ role: 'user', content: text });
// Show thinking indicator
const thinkingEl = appendMessage('assistant', 'Thinking…', true);
try {
// Build system prompt with CRM context
const systemPrompt = `You are a helpful AI assistant embedded inside DenchClaw, a local-first CRM. ${crmContext} Help the user understand their contacts, deals, and relationships. Be concise and actionable.`;
// Stream the response
let fullResponse = '';
thinkingEl.textContent = '';
thinkingEl.classList.remove('thinking');
await dench.chat.stream(
[
{ role: 'system', content: systemPrompt },
...history.slice(-10), // Keep last 10 for context window
],
(chunk) => {
fullResponse += chunk;
thinkingEl.textContent = fullResponse;
messagesEl.scrollTop = messagesEl.scrollHeight;
}
);
// Save to history
history.push({ role: 'assistant', content: fullResponse });
dench.store.set('chat-history', history.slice(-50)); // Keep last 50 messages
} catch (err) {
thinkingEl.textContent = 'Something went wrong. Try again.';
dench.ui.toast('Chat error: ' + err.message, { type: 'error' });
}
isStreaming = false;
sendBtn.disabled = false;
inputEl.focus();
}
// Send on Enter (Shift+Enter for newline)
inputEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
sendBtn.addEventListener('click', sendMessage);
// Initialize
loadCRMContext();
</script>
</body>
</html>Step 4: Understand the Bridge API Calls#
Let's break down the window.dench methods used above:
dench.db.query(sql)#
Runs a SQL query against your local DuckDB. Returns a promise resolving to an array of row objects. This is how you give the AI real context — your actual CRM data.
dench.chat.stream(messages, onChunk)#
Streams an AI response token-by-token. messages follows the OpenAI format ([{role, content}]). onChunk fires for each streamed token. Use this instead of dench.chat.complete() for a responsive feel.
dench.store.get(key) / dench.store.set(key, value)#
Persistent key-value store scoped to your app. Survives page refreshes. Use it for conversation history, user preferences, or any state you want to keep around.
dench.ui.toast(message, options)#
Shows a toast notification in the DenchClaw UI. Pass { type: 'error' }, { type: 'success' }, or { type: 'info' }.
Step 5: Add Events for Real-Time Updates#
Want the chat to react when someone adds a new contact? Use dench.events:
dench.events.on('crm:contact:created', async (contact) => {
crmContext = await buildCRMContext(); // Re-fetch context
dench.ui.toast(`New contact added: ${contact.name}`);
});This is what makes DenchClaw apps feel alive — they're not isolated tools, they're integrated into your workspace.
Step 6: Register the App#
With DenchClaw running (npx denchclaw), drop your .dench.app folder into the workspace apps directory. The app will appear in the sidebar automatically. No restart required.
Taking It Further#
Once you have the basic chat working, here's what to build next:
- Slash commands — Parse
/contact John Doeto look up and inject a contact's full profile into the conversation - Cron-based summaries — Use
dench.cronto send daily briefings into the chat - Multi-model routing — Use
dench.http.fetch()to call external model APIs for specialized tasks - Shared memory — Store AI-generated insights back into DuckDB with
dench.db.query('INSERT ...')
The App Builder guide covers all of these patterns. The bridge API reference has the full method signatures.
Why This Matters#
Most AI chat tools are disconnected from your actual work. They don't know who you're meeting with, what deals are in flight, or which hires you're tracking. DenchClaw's App Builder changes that. Your chat app lives inside your CRM, has read/write access to your data, and can act on your behalf — not just answer questions.
That's the difference between a chatbot and an AI co-pilot.
To understand the full platform, start with what is a dench.app.
FAQ#
Do I need an API key to use dench.chat?
No. dench.chat routes through DenchClaw's built-in AI layer. You don't manage API keys in your app code.
Can I use external AI models?
Yes. Use dench.http.fetch() to call any external API — OpenAI, Anthropic, Groq, whatever you prefer. You handle auth headers in your fetch call.
Is conversation history secure?
Your history is stored locally in dench.store, which persists in your local workspace. Nothing leaves your machine unless you explicitly call an external API.
Can multiple .dench.app instances share the same store?
No — dench.store is scoped per app. Use DuckDB (via dench.db.query) as the shared data layer across apps.
What's the difference between dench.chat.stream and dench.chat.complete?
stream delivers tokens progressively for a responsive feel. complete waits for the full response. For chat UIs, always use stream.
Ready to try DenchClaw? Install in one command: npx denchclaw. Full setup guide →
