summaryrefslogtreecommitdiffhomepage
path: root/.rules
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-03-24 12:45:55 +0900
committerAdam Malczewski <[email protected]>2026-03-24 12:45:55 +0900
commite5583b836d4fe2f7f9806ed85a190254a6ea3990 (patch)
tree99f306f25b40046bddd212d358e4b1751dd44909 /.rules
parente2c88087f3926ec477ea099fd771d1bc9d11d7c5 (diff)
downloadai-pulse-obsidian-plugin-e5583b836d4fe2f7f9806ed85a190254a6ea3990.tar.gz
ai-pulse-obsidian-plugin-e5583b836d4fe2f7f9806ed85a190254a6ea3990.zip
create remaining documentation and create a plan
Diffstat (limited to '.rules')
-rw-r--r--.rules/changelog/2026-03/24/03.md19
-rw-r--r--.rules/changelog/2026-03/24/04.md4
-rw-r--r--.rules/default/ollama.md38
-rw-r--r--.rules/docs/ollama/embed.md16
-rw-r--r--.rules/docs/ollama/errors.md34
-rw-r--r--.rules/docs/ollama/streaming.md86
-rw-r--r--.rules/docs/ollama/structured-outputs.md68
-rw-r--r--.rules/docs/ollama/thinking.md64
-rw-r--r--.rules/docs/ollama/tool-calling.md144
-rw-r--r--.rules/docs/ollama/vision.md87
-rw-r--r--.rules/plan/plan.md255
11 files changed, 810 insertions, 5 deletions
diff --git a/.rules/changelog/2026-03/24/03.md b/.rules/changelog/2026-03/24/03.md
new file mode 100644
index 0000000..27bf614
--- /dev/null
+++ b/.rules/changelog/2026-03/24/03.md
@@ -0,0 +1,19 @@
+# Changelog — 2026-03-24 #03
+
+## Summary
+
+Added Ollama capability docs and expanded the index with cross-references.
+
+## New Files
+
+- `.rules/docs/ollama/streaming.md` — Streaming transport, chunk fields, accumulation, when to disable
+- `.rules/docs/ollama/thinking.md` — Thinking/reasoning traces, supported models, two-phase streaming
+- `.rules/docs/ollama/structured-outputs.md` — `format` parameter, schema-constrained JSON, parsing tips
+- `.rules/docs/ollama/vision.md` — Multimodal image input, base64 encoding, vision + structured output
+- `.rules/docs/ollama/tool-calling.md` — Tool calling flow, parallel calls, agent loop, streaming tools
+- `.rules/docs/ollama/errors.md` — HTTP status codes, error format, mid-stream error handling
+
+## Modified Files
+
+- `.rules/docs/ollama/embed.md` — Added overview context, recommended models, usage tips
+- `.rules/default/ollama.md` — Updated index entries to point to new docs; added "Related Files" section mapping coupled docs
diff --git a/.rules/changelog/2026-03/24/04.md b/.rules/changelog/2026-03/24/04.md
new file mode 100644
index 0000000..659124d
--- /dev/null
+++ b/.rules/changelog/2026-03/24/04.md
@@ -0,0 +1,4 @@
+# Changelog — 2026-03-24 #04
+
+## Added
+- `.rules/plan.md`: Stage 1 development plan for the AI Organizer chat sidebar feature. Covers file plan, step-by-step tasks, UI layout, Ollama client functions, settings interface, styles, and a verification checklist.
diff --git a/.rules/default/ollama.md b/.rules/default/ollama.md
index cf6f962..83562b8 100644
--- a/.rules/default/ollama.md
+++ b/.rules/default/ollama.md
@@ -8,15 +8,45 @@ Local LLM inference via Ollama (`http://localhost:11434`). Docs are in `.rules/d
|------------|------|
| One-shot text completion, prompt/suffix, system prompt | `.rules/docs/ollama/generate.md` |
| ModelOptions (temperature, top_k, top_p, seed, num_ctx, stop, etc.) | `.rules/docs/ollama/generate.md` |
-| Structured output via `format` (JSON / JSON schema) | `.rules/docs/ollama/generate.md` |
+| Structured output via `format` (JSON / JSON schema) | `.rules/docs/ollama/structured-outputs.md` |
+| Parsing and validating structured responses | `.rules/docs/ollama/structured-outputs.md` |
| Load / unload a model (`keep_alive`) | `.rules/docs/ollama/generate.md` |
-| Streaming vs non-streaming response handling | `.rules/docs/ollama/generate.md` |
-| Thinking / reasoning traces (`think` param) | `.rules/docs/ollama/generate.md` |
+| Streaming vs non-streaming response handling | `.rules/docs/ollama/streaming.md` |
+| Accumulating streamed chunks (content, thinking, tool calls) | `.rules/docs/ollama/streaming.md` |
+| When to disable streaming | `.rules/docs/ollama/streaming.md` |
+| Thinking / reasoning traces (`think` param) | `.rules/docs/ollama/thinking.md` |
+| Supported thinking models (Qwen 3, DeepSeek R1, GPT-OSS, etc.) | `.rules/docs/ollama/thinking.md` |
+| Streaming thinking chunks (two-phase output) | `.rules/docs/ollama/thinking.md` |
| Multi-turn conversation (chat history, roles) | `.rules/docs/ollama/chat.md` |
-| Tool / function calling (define tools, handle tool_calls) | `.rules/docs/ollama/chat.md` |
+| Tool / function calling (define tools, handle tool_calls) | `.rules/docs/ollama/tool-calling.md` |
+| Parallel tool calls, agent loop (multi-turn tools) | `.rules/docs/ollama/tool-calling.md` |
+| Tool calling with streaming | `.rules/docs/ollama/tool-calling.md` |
+| Tool result messages (`role: "tool"`) | `.rules/docs/ollama/tool-calling.md` |
| ChatMessage, ToolDefinition, ToolCall schemas | `.rules/docs/ollama/chat.md` |
| Generate vector embeddings from text | `.rules/docs/ollama/embed.md` |
| Batch embed multiple strings at once | `.rules/docs/ollama/embed.md` |
+| Recommended embedding models | `.rules/docs/ollama/embed.md` |
+| Cosine similarity, RAG tips for embeddings | `.rules/docs/ollama/embed.md` |
| List locally available models | `.rules/docs/ollama/list-models.md` |
| Get model details (parameters, template, license) | `.rules/docs/ollama/show-model.md` |
| Check Ollama server version / health | `.rules/docs/ollama/version.md` |
+| HTTP status codes, error response format | `.rules/docs/ollama/errors.md` |
+| Handling errors during streaming | `.rules/docs/ollama/errors.md` |
+| Send images to vision/multimodal models | `.rules/docs/ollama/vision.md` |
+| Base64-encode images for the REST API | `.rules/docs/ollama/vision.md` |
+| Combine vision with structured output | `.rules/docs/ollama/vision.md` |
+
+## Related Files
+
+Some docs are closely coupled. When reading one, also check its companions:
+
+| File | Also read |
+|------|----------|
+| `chat.md` | `tool-calling.md` (tool calling uses `/api/chat`), `streaming.md` (streaming chat responses) |
+| `tool-calling.md` | `chat.md` (ChatMessage/ToolDefinition/ToolCall schemas), `streaming.md` (accumulating streamed tool calls) |
+| `streaming.md` | `thinking.md` (two-phase thinking+content chunks), `tool-calling.md` (streaming tool calls), `errors.md` (mid-stream errors) |
+| `errors.md` | `streaming.md` (mid-stream error handling requires checking each chunk) |
+| `thinking.md` | `streaming.md` (accumulating thinking chunks in streamed responses) |
+| `generate.md` | Defines `ModelOptions` — referenced by `chat.md`, `embed.md`, and others |
+| `structured-outputs.md` | `vision.md` (combining vision with structured output) |
+| `vision.md` | `structured-outputs.md` (JSON schema for structured image descriptions) |
diff --git a/.rules/docs/ollama/embed.md b/.rules/docs/ollama/embed.md
index 9c81ebf..53d46df 100644
--- a/.rules/docs/ollama/embed.md
+++ b/.rules/docs/ollama/embed.md
@@ -1,9 +1,17 @@
-# Generate embeddings
+# Generate Embeddings
`POST /api/embed` — Creates vector embeddings representing the input text.
**Server:** `http://localhost:11434`
+Embeddings turn text into numeric vectors for semantic search, retrieval, and RAG pipelines. Vector length depends on the model (typically 384–1024 dimensions). Vectors are **L2-normalized** (unit-length).
+
+## Recommended Models
+
+- `embeddinggemma`
+- `qwen3-embedding`
+- `all-minilm`
+
## Request
| Field | Type | Required | Description |
@@ -54,3 +62,9 @@ curl http://localhost:11434/api/embed -d '{
"dimensions": 128
}'
```
+
+## Tips
+
+- Use **cosine similarity** for most semantic search use cases.
+- Use the **same embedding model** for both indexing and querying.
+- Batch multiple strings in one request for efficiency.
diff --git a/.rules/docs/ollama/errors.md b/.rules/docs/ollama/errors.md
new file mode 100644
index 0000000..0ce30f6
--- /dev/null
+++ b/.rules/docs/ollama/errors.md
@@ -0,0 +1,34 @@
+# Error Handling
+
+## HTTP Status Codes
+
+All endpoints return standard HTTP status codes:
+
+| Code | Meaning |
+|------|---------|
+| `200` | Success |
+| `400` | Bad Request (missing parameters, invalid JSON) |
+| `404` | Not Found (model doesn't exist) |
+| `500` | Internal Server Error |
+
+## Error Response Format
+
+Non-streaming errors return `application/json`:
+
+```json
+{
+ "error": "the model failed to generate a response"
+}
+```
+
+## Errors During Streaming
+
+If an error occurs **mid-stream**, the error appears as an ndjson line with an `error` property. The HTTP status code will still be `200` since the response already started:
+
+```
+{"model":"gemma3","created_at":"...","response":" Yes","done":false}
+{"model":"gemma3","created_at":"...","response":".","done":false}
+{"error":"an error was encountered while running the model"}
+```
+
+When parsing streamed chunks, always check for an `error` field on each line before processing `response`/`message` fields.
diff --git a/.rules/docs/ollama/streaming.md b/.rules/docs/ollama/streaming.md
new file mode 100644
index 0000000..3c91d94
--- /dev/null
+++ b/.rules/docs/ollama/streaming.md
@@ -0,0 +1,86 @@
+# Streaming Responses
+
+Streaming is **enabled by default** on `/api/generate`, `/api/chat`, and `/api/embed`. Set `"stream": false` to disable.
+
+## Transport
+
+- Content-Type: `application/x-ndjson` (newline-delimited JSON).
+- Each line is a complete JSON object (one chunk).
+- The **final chunk** has `"done": true` and includes duration/token stats.
+
+## Chunk Fields by Endpoint
+
+### `/api/generate`
+
+| Field | Present when | Description |
+|---|---|---|
+| `response` | always | Partial generated text |
+| `thinking` | `think` enabled | Partial thinking/reasoning trace |
+| `done` | always | `false` until final chunk |
+
+### `/api/chat`
+
+| Field | Present when | Description |
+|---|---|---|
+| `message.content` | always | Partial assistant reply |
+| `message.thinking` | `think` enabled | Partial thinking trace |
+| `message.tool_calls` | model requests tool | Streamed tool call objects |
+| `done` | always | `false` until final chunk |
+
+## Accumulating Chunks
+
+You **must** accumulate partial fields across chunks to reconstruct the full response. This is critical for:
+
+1. **Content** — concatenate `response` (generate) or `message.content` (chat) from every chunk.
+2. **Thinking** — concatenate `thinking` or `message.thinking` from every chunk.
+3. **Tool calls** — collect `message.tool_calls` from chunks; pass the accumulated tool call back into the next request along with the tool result.
+
+### Pseudocode (REST / `requestUrl`)
+
+```
+let content = ''
+let thinking = ''
+let toolCalls = []
+
+for each line in response body (split by newline):
+ chunk = JSON.parse(line)
+
+ // Chat endpoint:
+ if chunk.message.thinking: thinking += chunk.message.thinking
+ if chunk.message.content: content += chunk.message.content
+ if chunk.message.tool_calls: toolCalls.push(...chunk.message.tool_calls)
+
+ // Generate endpoint:
+ if chunk.thinking: thinking += chunk.thinking
+ if chunk.response: content += chunk.response
+
+ if chunk.done:
+ // final chunk — stats available (total_duration, eval_count, etc.)
+ break
+```
+
+## Maintaining Conversation History (Chat)
+
+After streaming completes, append the accumulated assistant message to `messages` for the next request:
+
+```json
+{
+ "role": "assistant",
+ "content": "<accumulated content>",
+ "thinking": "<accumulated thinking>",
+ "tool_calls": [ ... ]
+}
+```
+
+If tool calls were returned, also append tool results before the next request:
+
+```json
+{ "role": "tool", "content": "<tool result>" }
+```
+
+## When to Disable Streaming
+
+Set `"stream": false` when:
+- You only need the final result (simpler parsing).
+- Using structured output (`format`) — the full JSON is easier to parse in one shot.
+- Batch processing where incremental display is unnecessary.
diff --git a/.rules/docs/ollama/structured-outputs.md b/.rules/docs/ollama/structured-outputs.md
new file mode 100644
index 0000000..191cfbb
--- /dev/null
+++ b/.rules/docs/ollama/structured-outputs.md
@@ -0,0 +1,68 @@
+# Structured Outputs
+
+Force model responses to conform to a JSON schema using the `format` field on `/api/chat` or `/api/generate`.
+
+## `format` Parameter
+
+| Value | Behavior |
+|---|---|
+| `"json"` | Model returns free-form JSON (no schema enforcement) |
+| `{ ... }` (JSON schema object) | Model output is constrained to match the provided schema |
+
+Works with both `/api/chat` and `/api/generate`. Set `"stream": false` for easiest parsing.
+
+## Basic JSON (no schema)
+
+```json
+{
+ "model": "gpt-oss",
+ "messages": [{"role": "user", "content": "Tell me about Canada."}],
+ "stream": false,
+ "format": "json"
+}
+```
+
+## Schema-Constrained JSON
+
+Pass a full JSON Schema object as `format`. The model's output will match the schema structure.
+
+**Tip:** Also include the schema (or a description of the expected fields) in the prompt text to ground the model.
+
+```json
+{
+ "model": "gpt-oss",
+ "messages": [{"role": "user", "content": "Tell me about Canada."}],
+ "stream": false,
+ "format": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string"},
+ "capital": {"type": "string"},
+ "languages": {
+ "type": "array",
+ "items": {"type": "string"}
+ }
+ },
+ "required": ["name", "capital", "languages"]
+ }
+}
+```
+
+Response `message.content` (chat) or `response` (generate) will be a JSON string matching the schema.
+
+## Parsing the Response
+
+The response content is a **JSON string** — you must `JSON.parse()` it:
+
+```typescript
+const data = JSON.parse(response.message.content);
+// data.name, data.capital, data.languages are now typed fields
+```
+
+## Tips for Reliable Structured Output
+
+- **Lower temperature** (e.g. `0`) for more deterministic results.
+- **Include the schema in the prompt** text to help the model understand expected fields.
+- **Use `stream: false`** — parsing a complete JSON blob is simpler than assembling streamed chunks.
+- **Validate after parsing** — the schema constrains the model but always validate in your application code.
+- Works with vision/multimodal models too — pass `images` alongside `format` for structured image descriptions.
diff --git a/.rules/docs/ollama/thinking.md b/.rules/docs/ollama/thinking.md
new file mode 100644
index 0000000..eceb064
--- /dev/null
+++ b/.rules/docs/ollama/thinking.md
@@ -0,0 +1,64 @@
+# Thinking / Reasoning Traces
+
+Thinking-capable models emit a separate `thinking` field containing their reasoning trace, distinct from the final answer in `content`/`response`.
+
+## Enabling Thinking
+
+Set the `think` field on `/api/chat` or `/api/generate` requests:
+
+| `think` value | Behavior |
+|---|---|
+| `true` | Enable thinking (most models) |
+| `false` | Disable thinking |
+| `"low"` / `"medium"` / `"high"` | GPT-OSS only — controls trace length; `true`/`false` is ignored for this model |
+
+Thinking is **enabled by default** for supported models.
+
+## Response Fields
+
+| Endpoint | Thinking field | Answer field |
+|---|---|---|
+| `/api/chat` | `message.thinking` | `message.content` |
+| `/api/generate` | `thinking` | `response` |
+
+### Non-streaming example
+
+```bash
+curl http://localhost:11434/api/chat -d '{
+ "model": "qwen3",
+ "messages": [{"role": "user", "content": "How many r's in strawberry?"}],
+ "think": true,
+ "stream": false
+}'
+```
+
+Response includes both `message.thinking` (reasoning) and `message.content` (final answer).
+
+## Streaming Thinking
+
+When streaming with `think` enabled, chunks arrive in two phases:
+
+1. **Thinking phase** — chunks have `message.thinking` (or `thinking`) populated, `content` empty.
+2. **Answer phase** — chunks have `message.content` (or `response`) populated, `thinking` empty.
+
+Detect the transition by checking which field is non-empty on each chunk. See `streaming.md` for accumulation details.
+
+## Supported Models
+
+- Qwen 3 (`qwen3`)
+- GPT-OSS (`gpt-oss`) — requires `"low"` / `"medium"` / `"high"` instead of boolean
+- DeepSeek V3.1 (`deepseek-v3.1`)
+- DeepSeek R1 (`deepseek-r1`)
+- Browse latest: [thinking models](https://ollama.com/search?c=thinking)
+
+## Conversation History with Thinking
+
+When maintaining chat history, include the `thinking` field in the assistant message so the model retains context of its reasoning:
+
+```json
+{
+ "role": "assistant",
+ "thinking": "<accumulated thinking>",
+ "content": "<accumulated content>"
+}
+```
diff --git a/.rules/docs/ollama/tool-calling.md b/.rules/docs/ollama/tool-calling.md
new file mode 100644
index 0000000..23f7104
--- /dev/null
+++ b/.rules/docs/ollama/tool-calling.md
@@ -0,0 +1,144 @@
+# Tool Calling (Function Calling)
+
+Tool calling lets the model invoke functions you define and incorporate their results. Uses `/api/chat` with the `tools` parameter.
+
+## Flow Overview
+
+1. Send a request with `tools` definitions and a user message.
+2. Model responds with `message.tool_calls` (instead of or alongside `content`).
+3. Execute the requested function(s) locally.
+4. Append the assistant message (with `tool_calls`) and tool result messages back to `messages`.
+5. Send follow-up request — model generates final answer using tool results.
+
+## Tool Definition Schema
+
+```json
+{
+ "type": "function",
+ "function": {
+ "name": "get_temperature",
+ "description": "Get the current temperature for a city",
+ "parameters": {
+ "type": "object",
+ "required": ["city"],
+ "properties": {
+ "city": { "type": "string", "description": "The name of the city" }
+ }
+ }
+ }
+}
+```
+
+## Tool Result Message
+
+After executing a tool, append a message with `role: "tool"`:
+
+```json
+{ "role": "tool", "tool_name": "get_temperature", "content": "22°C" }
+```
+
+## Single Tool Call
+
+```json
+{
+ "model": "qwen3",
+ "messages": [
+ { "role": "user", "content": "What is the temperature in New York?" }
+ ],
+ "tools": [ /* tool definitions */ ],
+ "stream": false
+}
+```
+
+Response includes `message.tool_calls`:
+
+```json
+{
+ "message": {
+ "role": "assistant",
+ "tool_calls": [{
+ "type": "function",
+ "function": {
+ "index": 0,
+ "name": "get_temperature",
+ "arguments": { "city": "New York" }
+ }
+ }]
+ }
+}
+```
+
+Follow up with the full conversation history:
+
+```json
+{
+ "model": "qwen3",
+ "messages": [
+ { "role": "user", "content": "What is the temperature in New York?" },
+ { "role": "assistant", "tool_calls": [ /* from response above */ ] },
+ { "role": "tool", "tool_name": "get_temperature", "content": "22°C" }
+ ],
+ "stream": false
+}
+```
+
+## Parallel Tool Calls
+
+The model may return **multiple** `tool_calls` in a single response. Execute all of them locally and append each result as a separate `role: "tool"` message, in the same order as the calls.
+
+```json
+"tool_calls": [
+ { "function": { "index": 0, "name": "get_temperature", "arguments": { "city": "New York" } } },
+ { "function": { "index": 1, "name": "get_conditions", "arguments": { "city": "New York" } } },
+ { "function": { "index": 2, "name": "get_temperature", "arguments": { "city": "London" } } },
+ { "function": { "index": 3, "name": "get_conditions", "arguments": { "city": "London" } } }
+]
+```
+
+Then append tool results in order:
+
+```json
+{ "role": "tool", "tool_name": "get_temperature", "content": "22°C" },
+{ "role": "tool", "tool_name": "get_conditions", "content": "Partly cloudy" },
+{ "role": "tool", "tool_name": "get_temperature", "content": "15°C" },
+{ "role": "tool", "tool_name": "get_conditions", "content": "Rainy" }
+```
+
+## Agent Loop (Multi-Turn Tool Calling)
+
+For complex tasks the model may need multiple rounds of tool calls. Use a loop:
+
+```
+messages = [user message]
+while true:
+ response = chat(model, messages, tools)
+ messages.append(response.message)
+
+ if no tool_calls in response:
+ break // model is done, response.message.content has the answer
+
+ for each tool_call:
+ result = execute(tool_call.function.name, tool_call.function.arguments)
+ messages.append({ role: "tool", tool_name: ..., content: result })
+
+ // loop continues — model sees tool results and may call more tools
+```
+
+**Tip:** Include in the system prompt that the model is in a loop and can make multiple tool calls.
+
+## Tool Calling with Streaming
+
+When `stream: true`, accumulate `message.tool_calls` from chunks alongside `content` and `thinking`. After the stream ends:
+
+1. Append the accumulated assistant message (with all fields) to `messages`.
+2. Execute tool calls and append results.
+3. Continue the loop.
+
+See `streaming.md` for chunk accumulation details.
+
+## Key Points
+
+- Always pass `tools` in follow-up requests so the model knows tools are still available.
+- The `tool_calls[].function.index` field indicates call ordering for parallel calls.
+- Tool calling works with `think: true` — the model reasons before deciding which tools to call.
+- `stream: false` is simpler for tool calling; streaming requires accumulating partial tool call chunks.
diff --git a/.rules/docs/ollama/vision.md b/.rules/docs/ollama/vision.md
new file mode 100644
index 0000000..bab9ea0
--- /dev/null
+++ b/.rules/docs/ollama/vision.md
@@ -0,0 +1,87 @@
+# Vision (Multimodal Image Input)
+
+Vision-capable models accept images alongside text prompts for description, classification, and visual Q&A.
+
+## How to Send Images
+
+Add an `images` array to a message in `/api/chat` or to the top-level request in `/api/generate`.
+
+**REST API:** Images must be **base64-encoded** strings (no file paths or URLs).
+
+### `/api/chat` example
+
+```json
+{
+ "model": "gemma3",
+ "messages": [{
+ "role": "user",
+ "content": "What is in this image?",
+ "images": ["<base64-encoded image data>"]
+ }],
+ "stream": false
+}
+```
+
+### `/api/generate` example
+
+```json
+{
+ "model": "gemma3",
+ "prompt": "Describe this image.",
+ "images": ["<base64-encoded image data>"],
+ "stream": false
+}
+```
+
+## Base64 Encoding
+
+In an Obsidian plugin context (TypeScript), convert an `ArrayBuffer` to base64:
+
+```typescript
+const buffer = await vault.readBinary(file);
+const bytes = new Uint8Array(buffer);
+let binary = '';
+for (const b of bytes) binary += String.fromCharCode(b);
+const base64 = btoa(binary);
+```
+
+## Multiple Images
+
+Pass multiple base64 strings in the `images` array. The model will consider all of them in context.
+
+```json
+"images": ["<base64_image_1>", "<base64_image_2>"]
+```
+
+## Combining with Structured Output
+
+Vision works with the `format` parameter — use a JSON schema to get structured descriptions:
+
+```json
+{
+ "model": "gemma3",
+ "messages": [{
+ "role": "user",
+ "content": "Describe the objects in this photo.",
+ "images": ["<base64>"]
+ }],
+ "stream": false,
+ "format": {
+ "type": "object",
+ "properties": {
+ "objects": {
+ "type": "array",
+ "items": {"type": "string"}
+ }
+ },
+ "required": ["objects"]
+ }
+}
+```
+
+## Supported Models
+
+Any model with vision/multimodal capability, e.g.:
+- `gemma3`
+- `llava`
+- Browse: [vision models](https://ollama.com/search?c=vision)
diff --git a/.rules/plan/plan.md b/.rules/plan/plan.md
new file mode 100644
index 0000000..f4106ac
--- /dev/null
+++ b/.rules/plan/plan.md
@@ -0,0 +1,255 @@
+# AI Organizer — Stage 1: Chat Sidebar with Ollama Connection
+
+## Goal
+
+Replace the sample plugin scaffolding with a functional Ollama chat sidebar. The sidebar view contains a chat area (top half) and a settings panel (bottom half). The user configures the Ollama URL, tests the connection, selects a model, and chats with the AI.
+
+---
+
+## Existing State
+
+- Project is the Obsidian sample plugin template (TypeScript, esbuild).
+- `manifest.json` has `id: "sample-plugin"`, `isDesktopOnly: false`.
+- `src/main.ts` contains `MyPlugin` with boilerplate commands, ribbon icon, status bar, and a modal.
+- `src/settings.ts` contains `MyPluginSettings` with a single `mySetting: string` field and `SampleSettingTab`.
+- `styles.css` is empty (comments only).
+- Build: `npm run dev` (esbuild watch), `npm run build` (tsc + esbuild production).
+
+---
+
+## File Plan
+
+| File | Action | Purpose |
+|------|--------|---------|
+| `manifest.json` | Modify | Update `id`, `name`, `description`, `author`, `authorUrl`. Remove `fundingUrl`. |
+| `package.json` | Modify | Update `name` and `description`. |
+| `src/main.ts` | Rewrite | New plugin class `AIOrganizer`. Register view, register command, load/save settings. Remove all sample code. |
+| `src/settings.ts` | Rewrite | New `AIOrganizerSettings` interface with `ollamaUrl` and `model`. New `DEFAULT_SETTINGS`. Remove `SampleSettingTab` (settings live in the sidebar view, not a settings tab). |
+| `src/chat-view.ts` | Create | `ItemView` subclass for the sidebar. Contains chat UI (top) and settings panel (bottom). |
+| `src/ollama-client.ts` | Create | Functions: `testConnection`, `listModels`, `sendChatMessage`. All use `requestUrl`. |
+| `styles.css` | Rewrite | Styles for the chat view layout, messages, input area, settings panel. |
+
+---
+
+## Step-by-Step Tasks
+
+### Step 1 — Update Metadata
+
+**`manifest.json`**:
+- Set `id` to `"ai-organizer"`.
+- Set `name` to `"AI Organizer"`.
+- Set `description` to `"Organize notes via AI powered by Ollama."`.
+- Set `author` to the repo owner's name.
+- Set `authorUrl` to the repo URL.
+- Remove `fundingUrl`.
+- Keep `isDesktopOnly` as `false`.
+- Keep `minAppVersion` as `"0.15.0"`.
+
+**`package.json`**:
+- Set `name` to `"ai-organizer"`.
+- Set `description` to match `manifest.json`.
+
+### Step 2 — Settings Interface
+
+**`src/settings.ts`** — delete all existing content and replace:
+
+- Define `AIOrganizerSettings` interface:
+ - `ollamaUrl: string` — the Ollama server base URL.
+ - `model: string` — the selected model name (empty string means none selected).
+- Define `DEFAULT_SETTINGS: AIOrganizerSettings`:
+ - `ollamaUrl`: `"http://localhost:11434"`
+ - `model`: `""`
+- Export both.
+- Do NOT create a `PluginSettingTab`. Settings are embedded in the sidebar view.
+
+### Step 3 — Ollama Client
+
+**`src/ollama-client.ts`** — create:
+
+#### `testConnection(ollamaUrl: string): Promise<string>`
+- `GET {ollamaUrl}/api/version` using `requestUrl` with `throw: false`.
+- On success (status 200): return the version string from `response.json.version`.
+- On failure: throw an `Error` with a descriptive message. If `status` is 0 or the error message contains `"net"` or `"fetch"`, the message must say Ollama is unreachable. Otherwise include the status code.
+
+#### `listModels(ollamaUrl: string): Promise<string[]>`
+- `GET {ollamaUrl}/api/tags` using `requestUrl`.
+- Return `response.json.models.map((m: {name: string}) => m.name)`.
+- On failure: throw an `Error` with a descriptive message.
+
+#### `sendChatMessage(ollamaUrl: string, model: string, messages: ChatMessage[]): Promise<string>`
+- Define `ChatMessage` interface: `{ role: "system" | "user" | "assistant"; content: string }`. Export it.
+- `POST {ollamaUrl}/api/chat` using `requestUrl`.
+- Body: `{ model, messages, stream: false }`.
+- Return `response.json.message.content`.
+- On failure: throw an `Error` with a descriptive message.
+
+All three functions are standalone exports (no class). All use `import { requestUrl } from "obsidian"`.
+
+### Step 4 — Chat View
+
+**`src/chat-view.ts`** — create:
+
+- Export `VIEW_TYPE_CHAT = "ai-organizer-chat"`.
+- Export class `ChatView extends ItemView`.
+
+#### Constructor
+- Accept `leaf: WorkspaceLeaf` and a reference to the plugin instance (`AIOrganizer`). Store the plugin reference as a private property.
+
+#### `getViewType()` → return `VIEW_TYPE_CHAT`.
+
+#### `getDisplayText()` → return `"AI Chat"`.
+
+#### `getIcon()` → return `"message-square"`.
+
+#### `onOpen()`
+
+Build the entire UI inside `this.contentEl`. The layout is a vertical flexbox split into two regions:
+
+**Top region — Chat area** (flexbox column, `flex: 1`, overflow-y scroll):
+- A message container `div` that holds chat message elements.
+- Each message is a `div` with a CSS class indicating the role (`"user"` or `"assistant"`).
+- Below the message container: an input row (flexbox row) with:
+ - A `textarea` for user input (flex: 1, placeholder: `"Type a message..."`). Pressing Enter (without Shift) sends the message. Shift+Enter inserts a newline.
+ - A send `button` (text: `"Send"`).
+- The send button and Enter key trigger the send flow (defined below).
+- While waiting for a response, disable the textarea and send button and change the button text to `"..."`.
+
+**Bottom region — Settings panel** (fixed height, border-top separator, padding, overflow-y auto):
+- A heading element: `"Settings"`.
+- **Ollama URL**: Use an Obsidian `Setting` component.
+ - Name: `"Ollama URL"`.
+ - Description: `"Base URL of the Ollama server."`.
+ - `addText` input pre-filled with `plugin.settings.ollamaUrl`.
+ - `onChange`: update `plugin.settings.ollamaUrl` and call `plugin.saveSettings()`.
+- **Test Connection**: Use an Obsidian `Setting` component.
+ - Name: `"Test Connection"`.
+ - Description: initially empty. This description element will be used to display the result.
+ - `addButton` with text `"Test"`.
+ - `onClick`: call `testConnection(plugin.settings.ollamaUrl)`.
+ - On success: set the description to `"Connected — Ollama v{version}"`. Then automatically call `listModels` and populate the model dropdown (see below).
+ - On failure: set the description to the error message.
+- **Model Selection**: Use an Obsidian `Setting` component.
+ - Name: `"Model"`.
+ - Description: `"Select the model to use."`.
+ - `addDropdown`.
+ - Initially the dropdown has one option: `{ value: "", display: "Test connection first" }` and is disabled.
+ - After a successful `testConnection` + `listModels`:
+ - Clear the dropdown options (use `selectEl.empty()` on the underlying `<select>` element).
+ - Add a placeholder option `{ value: "", display: "Select a model..." }`.
+ - Add one option per model name returned by `listModels` (value and display are both the model name).
+ - If `plugin.settings.model` matches one of the returned models, set the dropdown value to it.
+ - Enable the dropdown.
+ - `onChange`: update `plugin.settings.model` and call `plugin.saveSettings()`.
+
+#### Send flow
+
+1. Read the textarea value. If empty (after trim), do nothing.
+2. If `plugin.settings.model` is empty, show a `Notice`: `"Select a model first."` and return.
+3. Append a user message `div` to the message container with the textarea content.
+4. Clear the textarea.
+5. Scroll the message container to the bottom.
+6. Maintain a local `ChatMessage[]` array as instance state on the view. Push `{ role: "user", content: text }`.
+7. Disable input (textarea + button).
+8. Call `sendChatMessage(plugin.settings.ollamaUrl, plugin.settings.model, messages)`.
+9. On success:
+ - Push `{ role: "assistant", content: response }` to the messages array.
+ - Append an assistant message `div` to the message container.
+ - Scroll to bottom.
+10. On failure:
+ - Show a `Notice` with the error message.
+ - Append an assistant message `div` with class `"error"` and the text `"Error: {message}"`.
+11. Re-enable input.
+
+#### `onClose()`
+- Empty `this.contentEl`.
+
+#### Instance state
+- `messages: ChatMessage[]` — starts as empty array. Resets when the view is re-opened.
+
+### Step 5 — Main Plugin Class
+
+**`src/main.ts`** — delete all existing content and replace:
+
+- Import `Plugin`, `WorkspaceLeaf`, `Notice` from `"obsidian"`.
+- Import `AIOrganizerSettings`, `DEFAULT_SETTINGS` from `"./settings"`.
+- Import `ChatView`, `VIEW_TYPE_CHAT` from `"./chat-view"`.
+
+- Export default class `AIOrganizer extends Plugin`:
+ - Property: `settings: AIOrganizerSettings`.
+
+ - `async onload()`:
+ 1. Call `await this.loadSettings()`.
+ 2. Register the chat view: `this.registerView(VIEW_TYPE_CHAT, (leaf) => new ChatView(leaf, this))`.
+ 3. Add a ribbon icon:
+ - Icon: `"message-square"`.
+ - Tooltip: `"Open AI Chat"`.
+ - Callback: call `this.activateView()`.
+ 4. Add a command:
+ - `id`: `"open-chat"`.
+ - `name`: `"Open AI Chat"`.
+ - `callback`: call `this.activateView()`.
+
+ - `onunload()`:
+ 1. `this.app.workspace.detachLeavesOfType(VIEW_TYPE_CHAT)`.
+
+ - `async activateView()`:
+ 1. Get existing leaves: `this.app.workspace.getLeavesOfType(VIEW_TYPE_CHAT)`.
+ 2. If a leaf exists, call `this.app.workspace.revealLeaf(leaf)` on the first one.
+ 3. Otherwise:
+ - Get a right sidebar leaf: `this.app.workspace.getRightLeaf(false)`.
+ - Set its view state: `await leaf.setViewState({ type: VIEW_TYPE_CHAT, active: true })`.
+ - Reveal it: `this.app.workspace.revealLeaf(leaf)`.
+
+ - `async loadSettings()`:
+ - `this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData())`.
+
+ - `async saveSettings()`:
+ - `await this.saveData(this.settings)`.
+
+### Step 6 — Styles
+
+**`styles.css`** — delete all existing content and replace with styles for:
+
+- `.ai-organizer-chat-container`: vertical flexbox, full height (`height: 100%`).
+- `.ai-organizer-messages-area`: top region. `flex: 1`, `overflow-y: auto`, `display: flex`, `flex-direction: column`.
+- `.ai-organizer-messages`: the scrollable message list inside the messages area. `flex: 1`, `overflow-y: auto`, padding.
+- `.ai-organizer-message`: individual message. Padding, margin-bottom, border-radius.
+- `.ai-organizer-message.user`: right-aligned background. Use `--interactive-accent` for background, `--text-on-accent` for text color.
+- `.ai-organizer-message.assistant`: left-aligned background. Use `--background-secondary` for background.
+- `.ai-organizer-message.error`: use `--text-error` for text color.
+- `.ai-organizer-input-row`: flexbox row, gap, padding.
+- `.ai-organizer-input-row textarea`: `flex: 1`, resize vertical, use Obsidian CSS variables for background/border/text.
+- `.ai-organizer-settings-panel`: bottom region. Fixed `min-height` (do NOT use a fixed pixel height — let it size to content). `border-top: 1px solid var(--background-modifier-border)`, padding, `overflow-y: auto`.
+
+All class names are prefixed with `ai-organizer-` to avoid collisions. Use Obsidian CSS variables everywhere (no hardcoded colors).
+
+---
+
+## Verification Checklist
+
+After completing all steps, verify:
+
+1. `npm run build` succeeds with zero errors.
+2. The plugin loads in Obsidian without console errors.
+3. The ribbon icon and command `"Open AI Chat"` both open the chat sidebar.
+4. The sidebar opens in the right panel on desktop.
+5. The sidebar opens in the right drawer on mobile.
+6. Entering an Ollama URL and clicking "Test" with Ollama running shows the version and populates the model dropdown.
+7. Clicking "Test" with Ollama stopped shows an error in the description.
+8. Selecting a model persists across plugin reload.
+9. Typing a message and pressing Enter sends it and displays the AI response.
+10. Pressing Shift+Enter in the textarea inserts a newline instead of sending.
+11. The send button and textarea are disabled while waiting for a response.
+12. A network error during chat shows a `Notice` and an error message in the chat.
+13. The Ollama URL persists across plugin reload.
+
+---
+
+## Constraints
+
+- Do NOT use `PluginSettingTab`. All settings are in the sidebar view.
+- Do NOT use streaming for chat in this stage. Set `stream: false` on all Ollama requests.
+- Do NOT store chat history in `data.json`. Chat history lives only in view instance memory and resets on close/reopen.
+- Do NOT hardcode colors. Use Obsidian CSS variables.
+- All CSS class names must be prefixed with `ai-organizer-`.
+- `manifest.json` must have `isDesktopOnly: false`.