diff options
| author | Adam Malczewski <[email protected]> | 2026-03-24 12:45:55 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-03-24 12:45:55 +0900 |
| commit | e5583b836d4fe2f7f9806ed85a190254a6ea3990 (patch) | |
| tree | 99f306f25b40046bddd212d358e4b1751dd44909 /.rules | |
| parent | e2c88087f3926ec477ea099fd771d1bc9d11d7c5 (diff) | |
| download | ai-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.md | 19 | ||||
| -rw-r--r-- | .rules/changelog/2026-03/24/04.md | 4 | ||||
| -rw-r--r-- | .rules/default/ollama.md | 38 | ||||
| -rw-r--r-- | .rules/docs/ollama/embed.md | 16 | ||||
| -rw-r--r-- | .rules/docs/ollama/errors.md | 34 | ||||
| -rw-r--r-- | .rules/docs/ollama/streaming.md | 86 | ||||
| -rw-r--r-- | .rules/docs/ollama/structured-outputs.md | 68 | ||||
| -rw-r--r-- | .rules/docs/ollama/thinking.md | 64 | ||||
| -rw-r--r-- | .rules/docs/ollama/tool-calling.md | 144 | ||||
| -rw-r--r-- | .rules/docs/ollama/vision.md | 87 | ||||
| -rw-r--r-- | .rules/plan/plan.md | 255 |
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`. |
