Spec review

This commit is contained in:
Mateusz Tymek
2026-01-17 10:46:21 +01:00
parent 4dd9af83c3
commit d9cee8be1a
4 changed files with 103 additions and 54 deletions

View File

@@ -2,7 +2,7 @@
## Context
The plugin embeds OpenCode in an iframe and spawns a local server. OpenCode has an SDK and HTTP API that allows programmatic interaction with sessions. Obsidian's Workspace API provides access to open files, emits events when the workspace changes, and provides access to the active editor's selection.
The plugin embeds OpenCode in an iframe and spawns a local server. OpenCode exposes an HTTP API that allows programmatic interaction with sessions. Obsidian's Workspace API provides access to open files, emits events when the workspace changes, and provides access to the active editor's selection.
**Stakeholders:** Users who want AI to be aware of their open notes and selected text without manual input.
@@ -27,24 +27,25 @@ The plugin embeds OpenCode in an iframe and spawns a local server. OpenCode has
## Decisions
### Decision 1: Use OpenCode SDK for API communication
**What:** Add `@opencode-ai/sdk` as a dependency and use `createOpencodeClient()` to interact with the server.
### Decision 1: Use direct HTTP calls for API communication
**What:** Use direct `fetch()` calls to the local OpenCode server for session management and message/part updates.
**Why:** Type-safe API, officially supported, handles serialization and error handling.
**Why:** The plugin needs a few specific endpoints (create session, prompt with `noReply`, update/ignore parts). Using `fetch()` avoids adding SDK bundle size and keeps implementation explicit.
**Alternatives considered:**
- Direct fetch calls: Simpler but no type safety, more error-prone
- postMessage to iframe: Not supported by OpenCode web UI
- `@opencode-ai/sdk`: Type-safe, but adds dependency/bundle size and still requires careful session targeting.
- postMessage to iframe: Not supported by OpenCode web UI.
### Decision 2: Revert + Re-inject pattern for context updates
**What:** Track the message ID of injected context. Before injecting new context, revert the previous message using `session.revert()`, then inject fresh context.
### Decision 2: Replace context via update/ignore (never revert)
**What:** Track the injected context part ID. On updates, prefer updating that part in-place. If in-place update is not available, mark the previous part as `ignored: true` and create a new context injection.
**Why:** Prevents accumulation of stale context messages that would bloat the context window and confuse the AI.
**Why:** In OpenCode, `session.revert()` implements user-visible undo semantics and can delete messages after the revert point during cleanup, which is unsafe for automatic context refresh.
**Alternatives considered:**
- Append only: Would accumulate redundant messages
- System prompt field: Unclear if it replaces or appends
- One-time injection: Context becomes stale if user opens/closes files
- Revert + re-inject: Rejected (destructive semantics).
- Append only: Would accumulate redundant messages.
- One-time injection: Context becomes stale.
- System prompt field: Not specified as replace-only.
### Decision 3: Debounce workspace events (2 seconds)
**What:** Use Obsidian's `debounce()` utility with a 2-second delay before sending context updates.
@@ -60,10 +61,10 @@ The plugin embeds OpenCode in an iframe and spawns a local server. OpenCode has
- Selected text provides immediate, relevant context without overwhelming
- Full content injection could easily exceed context limits
### Decision 5: Track context per session
**What:** Maintain a `Map<sessionId, messageId>` to track injected context for each session.
### Decision 5: Track context for the active iframe session
**What:** Maintain a single tracked session and context reference (session ID + injected context part reference) based on the current iframe URL.
**Why:** OpenCode may have multiple sessions. Each session needs its own context tracking to properly revert previous injections.
**Why:** This plugin assumes only one OpenCode tab exists at a time. The injected context must follow the session the user is actively viewing in the embedded UI, which is determined by the iframe URL at injection time.
### Decision 6: Include selection source file
**What:** When including selected text, also indicate which file it's from.
@@ -80,7 +81,7 @@ The plugin embeds OpenCode in an iframe and spawns a local server. OpenCode has
│ │ WorkspaceContext│ │ OpenCodeClient │ │
│ │ │ │ │ │
│ │ - getOpenPaths()│ │ - updateContext()│ │
│ │ - getSelection()│ │ - revert + inject│ │
│ │ - getSelection()│ │ - update/ignore │ │
│ │ - formatContext │ │ │ │
│ └────────┬────────┘ └────────┬─────────┘ │
│ │ │ │
@@ -95,12 +96,13 @@ The plugin embeds OpenCode in an iframe and spawns a local server. OpenCode has
│ └──────────┬──────────┘ │
│ │ │
└──────────────────────┼──────────────────────────────────────┘
│ HTTP (SDK)
│ HTTP
┌────────▼────────┐
│ OpenCode Server │
│ │
│ - session.create│
│ - session.prompt│
│ - session.revert
│ - part.update
└─────────────────┘
```
@@ -113,10 +115,11 @@ The plugin embeds OpenCode in an iframe and spawns a local server. OpenCode has
a. `getOpenNotePaths()` - current open files
b. `getSelectedText()` - current selection (if any)
5. `OpenCodeClient.updateContext()`:
a. Gets current session ID from server
b. Reverts previous context message (if tracked)
c. Injects new context with `noReply: true`
d. Stores new message ID for future revert
a. Determines the current session ID by parsing the iframe URL (`.../session/<sessionID>`)
b. If no session ID is available (iframe not on a session route), do nothing
c. Updates or ignores the previously injected context part (if any)
d. Injects fresh context with `noReply: true`
e. Stores injected message/part IDs for future updates
6. OpenCode AI now has updated context for next interaction
## Context Format
@@ -142,11 +145,11 @@ When no text is selected, the "Selected text" section is omitted.
| Risk | Mitigation |
|------|------------|
| Revert fails (message already gone) | Catch error, continue with fresh inject |
| Session changes between revert and inject | Track per-session, clear tracking on session change |
| Session changes between updates | Parse iframe URL at injection time; update tracked session and context reference |
| Iframe not on a session route | No-op (do not inject) |
| Server not running when events fire | Check `getProcessState() === "running"` before attempting |
| SDK adds bundle size | SDK is lightweight; alternative (fetch) adds complexity |
| Message ID tracking lost on plugin reload | Acceptable - next update injects fresh context, old message becomes orphaned but harmless |
| Context replacement is destructive | Avoid `session.revert()`; update or mark the previous part as `ignored` |
| Tracking lost on plugin reload | Acceptable - next update injects fresh context, old message becomes stale but harmless |
| Large selection could bloat context | Truncate selection to reasonable limit (e.g., 2000 chars) |
## Migration Plan
@@ -159,7 +162,7 @@ No migration needed. New feature enabled by default but can be disabled in setti
- Resolved: The selected text section includes the source file, which serves this purpose.
2. Should context be injected on view open or only on workspace changes?
- Decision: Both. Initial injection when view opens, then updates on changes.
- Decision: Inject on workspace changes and once a tracked session exists. The plugin creates a session and sets the iframe URL when the view is first opened, enabling immediate injection thereafter.
3. Should selection changes trigger immediate updates or use the same debounce?
- Decision: Use same 2-second debounce to avoid excessive updates during text selection.