Local-first chat + image generation app built around:
- Next.js (App Router UI + API routes)
- LM Studio native REST chat (
/api/v1/chat) - ComfyUI (generation backend)
- MCP tool integrations
- SQLite persistence for threads, tasks, and generations
This project is optimized for:
- persistent threaded chat with
previous_response_id - queued task-group orchestration
- streaming chat + streaming image progress
- prompt modes and mood overlays
- chained utility-task workflows for image generation
- Architecture Overview
- Project Structure
- Task System
- Chat Pipeline
- Critique Pipeline
- Utility Task Chaining
- Image Generation Pipeline
- Integrations and MCP
- Data Model
- Environment Variables
- Setup and Run
- API Surface
- Troubleshooting
High-level layers:
- UI + API layer (
app/,components/) - Domain/application orchestration (
lib/tasks/,lib/tasks/chat/) - Infrastructure adapters (
lib/tasks/chat/adapters/,lib/lmstudio/,lib/comfy/) - Persistence (
lib/db.ts, task/thread stores)
Execution model:
- API route enqueues a task group (
chatorcomfy). - Task processor pulls next runnable group/task based on scheduler state.
- Runner executes each task kind.
- SSE streams events back to UI (
/api/chat/task/:taskId/events,/api/comfy/task/:taskId/events). - Results persist into SQLite-backed stores.
Top-level:
app/- Next.js pages and API routes (
/api/chat,/api/chat/critique,/api/comfy/*,/api/threads/*,/api/system/*)
- Next.js pages and API routes (
components/- Chat UI, image blocks, stream-bound components
lib/- All runtime logic
mcp/- Local MCP servers/wrappers
Core lib/ domains:
lib/lmstudio/- model loading/routing, prompt modes, moods, summaries, title generation, util-task settings
lib/comfy/- workflow builders, LoRA listing/selection helpers, generation persistence, Comfy client/runner glue
lib/tasks/- scheduler, queue processor, GPU mode manager, event bus, task runners
lib/tasks/chat/- refactored chat runtime with layered handlers, policies, util command chain logic, adapters
lib/chat/- message content normalization and chat markers (context compaction / critique markers)
execute.ts- chat runner entrypoint used by
lib/tasks/chat-runner.ts
- chat runner entrypoint used by
adapters/- thin boundaries around task store/thread store/lmstudio request paths
handlers/- task-kind and pipeline handlers (conversation, critique, intent, stream stages)
input/- LM Studio input builders (conversation + critique)
prompt/- prompt composition and mode-specific system prompt building
policies/- stop conditions, context budget checks, sampling/request shaping rules
util/- util command parsing, normalization, chain continuation and policy checks
stream/- stream event helpers/reducers
contracts/- lightweight contract tests for key pure behaviors
Task groups (lib/tasks/types.ts):
chatcomfy
Task kinds:
- chat:
chat.generatechat.streamchat.intentchat.unbiased_critiquechat.biased_critiquechat.compactchat.title
- image:
image.generateimage.stream
Chat payload kinds:
conversationcritiqueupdate_intentgenerate_titlecollapse_context
Default group task composition is injected by scheduler:
conversation->chat.generate+chat.streamcritique->chat.unbiased_critique+chat.biased_critique+chat.streamupdate_intent->chat.intentgenerate_title->chat.titlecollapse_context->chat.compact
Primary request route: POST /api/chat
Flow:
- Validate request and ensure thread exists.
- Persist latest user message (server-owned).
- Enqueue
conversationchat task group. - Optionally enqueue
update_intent(non-regenerate path). - Task processor runs:
chat.generateprepares LM request and opens streamchat.streamconsumes stream deltas and forwards SSE events
- Final assistant text/reasoning persisted.
lmstudioResponseId/model instance metadata updated on thread.
Overflow/near-limit handling:
- projection and stop-policy detect near-context-limit
- compaction tasks can be enqueued
- continuation groups are delegated with carry-over text/reasoning markers
Route: POST /api/chat/critique
Flow:
- Enqueue
critiquechat group. - Execute
chat.unbiased_critique. - Execute
chat.biased_critique. - Execute
chat.streamas stream bridge/output channel. - If biased output emits util command block, same task group continues with util chain tasks.
Important:
- critique tasks run through chat stream channel for live UI updates
- critique task MCP exposure is controlled per-task/mode integration policy
Command protocol supported by parser:
- bracket block form:
[[util_task]] ... [[/util_task]]- tags supported:
@persistent,@stateless
- inline form:
[[util_task:stage|context_text=...]]
- legacy JSON signal form is still parseable where present
Chain behavior:
chat.streamparser extracts command.- policy checks enforce depth/nonce/enqueue constraints.
- util-task setting is loaded from DB-configured util tasks.
- system prompt extension and (optionally) compact history snapshot are composed.
- group tasks are transitioned and continued in the same chat task group.
Flags:
@persistent- util execution behaves as persistent conversation result
@stateless- util execution omits snapshot/user-message seed injection for that stage
Comfy task flow:
image.generatecreates/queues Comfy job with selected workflow params.image.streamwatches progress and emits updates.- Completed images persisted to
.data/comfy-results/. - generation metadata and image references stored in SQLite tables.
Workflow support:
baseillustrationedit
LoRA listing:
- exposed through tooling and filtered by workflow mapping
- uses configured Comfy LoRA root directory
Per-mode integration policy (lib/tasks/chat/integrations.ts):
Fast: noneRegular: web search (if enabled)Writer: web search (if enabled)Artist: Comfy + optional Civitai
Util tasks can override MCP availability per util stage (utilMcpServers).
Comfy MCP:
- local server in repo (
mcp/comfy/) - supports generation/listing helpers used by artist pipeline
External wrappers:
mcp/external/web-search.tsmcp/external/civitai.tsmcp/memory/server.ts
SQLite schema is initialized in lib/db.ts.
Main tables:
threadsmessagestaskscomfy_generationscomfy_generation_images
Thread state stores:
lmstudio_response_idlmstudio_model_instance_idlast_prompt_mode- conversation summary counters
- context usage counters
user_intent
Minimum:
LM_STUDIO_BASE_URL=http://127.0.0.1:1234
LM_STUDIO_MODEL=your-loaded-model-id
COMFY_BASE_URL=http://127.0.0.1:8188
COMFY_MCP_PORT=4000
COMFY_LORA_DIR=E:\path\to\ComfyUI\models\lorasMode context lengths:
LM_STUDIO_CONTEXT_LENGTH_FAST=4096
LM_STUDIO_CONTEXT_LENGTH_REGULAR=16384
LM_STUDIO_CONTEXT_LENGTH_ARTIST=16384
LM_STUDIO_CONTEXT_LENGTH_WRITER=65536Optional:
LM_STUDIO_TOKENLM_STUDIO_AUTO_SUMMARYLM_STUDIO_ESTIMATED_IMAGE_TOKENS_PER_ATTACHMENTLM_STUDIO_REDUNDANT_MODELSLM_STUDIO_KEEP_MODELSLM_STUDIO_DEBUG_MODEL_ROUTING- MCP wrapper envs for web search / Civitai
- MCP wrapper envs for memory server:
MEMORY_MCP_ENABLED=trueMEMORY_MCP_URL=http://127.0.0.1:9558/mcpMEMORY_MCP_PORT=9558MEMORY_FILE_PATH=.data/memory/memory.jsonl
Install:
pnpm installRun app:
pnpm devRun app + MCP wrappers:
pnpm run dev:allIndividual MCP:
pnpm run mcp:comfy
pnpm run mcp:web-search
pnpm run mcp:civitai
pnpm run mcp:memoryChat:
POST /api/chatGET /api/chat/task/:taskId/eventsPOST /api/chat/critique
Threads:
GET /api/threadsGET /api/threads/:threadIdPATCH /api/threads/:threadIdPOST /api/threads/:threadId/collapse-contextPOST /api/threads/:threadId/title
Comfy:
GET /api/comfy/task/:taskIdGET /api/comfy/task/:taskId/eventsGET /api/comfy/result/:jobId
System:
GET /api/system/statePOST /api/system/unpause
If you see repeated chat.intent:
- ensure dispatch registry maps
chat.intentto intent handler only - clear stale queued tasks created before recent fixes
If util chains stop unexpectedly:
- check command parser logs (
util-command:*) - check depth/enqueue policy limits
- confirm util task name exists/enabled in DB settings
If chat stream ends too early:
- verify stream task remains pending/running during util chain continuation
- check
chat.streamevent output in task logs
If image progress is missing:
- verify Comfy websocket reachability
- verify
/api/comfy/task/:taskId/eventsstream
{
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint",
"mcp:comfy": "tsx mcp/comfy/index.ts",
"mcp:web-search": "tsx mcp/external/web-search.ts",
"mcp:civitai": "tsx mcp/external/civitai.ts",
"mcp:memory": "tsx mcp/memory/index.ts",
"dev:all": "concurrently \"pnpm run mcp:comfy\" \"pnpm run mcp:web-search\" \"pnpm run mcp:civitai\" \"pnpm run mcp:memory\" \"pnpm run dev\""
}