Spec review
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -6,11 +6,13 @@ Users working in Obsidian have multiple notes open that provide context for thei
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add integration with OpenCode SDK (`@opencode-ai/sdk`) to communicate with the server
|
||||
- Collect currently open note paths from Obsidian's workspace API
|
||||
- Collect currently selected text from the active editor (if any)
|
||||
- Create a new OpenCode session when the OpenCode view is first opened (per Obsidian run)
|
||||
- Preserve and restore the last OpenCode iframe URL when the view is closed/reopened (until Obsidian is restarted)
|
||||
- Resolve the target session by parsing the `sessionID` from the current iframe URL when context is about to be injected
|
||||
- Inject open notes and selected text as context into the current OpenCode session using `session.prompt({ noReply: true })`
|
||||
- Use revert + re-inject pattern to prevent context accumulation (each update replaces previous context)
|
||||
- Replace previous injected context without using `session.revert()` (update/ignore the previous context part instead)
|
||||
- Debounce workspace change events to avoid flooding the server
|
||||
- Add settings to enable/disable the feature (enabled by default) and limit the number of notes included
|
||||
|
||||
@@ -18,9 +20,9 @@ Users working in Obsidian have multiple notes open that provide context for thei
|
||||
|
||||
- Affected specs: `001-mvp-opencode-embed`
|
||||
- Affected code:
|
||||
- `package.json` - new SDK dependency
|
||||
- `src/types.ts` - new settings fields
|
||||
- `src/main.ts` - workspace event registration, context injection orchestration
|
||||
- `src/main.ts` - session tracking, URL persistence, context injection orchestration
|
||||
- `src/OpenCodeView.ts` - use cached URL when available
|
||||
- `src/SettingsTab.ts` - new settings UI
|
||||
- `src/OpenCodeClient.ts` - **new file** for SDK wrapper
|
||||
- `src/OpenCodeClient.ts` - **new file** for OpenCode HTTP client wrapper
|
||||
- `src/WorkspaceContext.ts` - **new file** for workspace data collection (open notes + selection)
|
||||
|
||||
@@ -29,14 +29,15 @@ The plugin SHALL collect the currently selected text from the active editor, if
|
||||
- **AND** an indicator is added showing truncation occurred
|
||||
|
||||
### Requirement: Context Injection to OpenCode
|
||||
The plugin SHALL inject the workspace context (open notes and selected text) into the current OpenCode session.
|
||||
The plugin SHALL inject the workspace context (open notes and selected text) into the OpenCode session currently displayed in the embedded iframe.
|
||||
|
||||
#### Scenario: Inject context on workspace change
|
||||
- **WHEN** the user opens, closes, or switches between notes
|
||||
- **AND** the setting "Inject workspace context" is enabled
|
||||
- **AND** the OpenCode server is running
|
||||
- **THEN** the plugin sends the workspace context to the current OpenCode session
|
||||
- **AND** the context is injected using the SDK with `noReply: true` (no AI response triggered)
|
||||
- **AND** a `sessionID` can be resolved from the current iframe URL
|
||||
- **THEN** the plugin sends the workspace context to that session
|
||||
- **AND** the context is injected using `session.prompt({ noReply: true })` (no AI response triggered)
|
||||
|
||||
#### Scenario: Inject context on selection change
|
||||
- **WHEN** the user changes their text selection in an editor
|
||||
@@ -47,26 +48,69 @@ The plugin SHALL inject the workspace context (open notes and selected text) int
|
||||
#### Scenario: Initial context injection
|
||||
- **WHEN** the OpenCode server transitions to running state
|
||||
- **AND** the setting is enabled
|
||||
- **AND** a tracked session exists (created on first open)
|
||||
- **THEN** the plugin injects the current workspace context
|
||||
|
||||
### Requirement: Context Replacement via Revert
|
||||
### Requirement: Context Replacement (Non-Destructive)
|
||||
The plugin SHALL replace previous context injections rather than accumulating them.
|
||||
|
||||
#### Scenario: Revert before re-inject
|
||||
#### Scenario: Update previous context part
|
||||
- **WHEN** the plugin injects new context
|
||||
- **AND** a previous context message exists for the session
|
||||
- **THEN** the plugin reverts the previous context message using `session.revert()`
|
||||
- **AND** then injects the new context
|
||||
- **AND** stores the new message ID for future revert
|
||||
- **AND** a previous context part exists for the session
|
||||
- **THEN** the plugin updates the previous context part in-place to match the new context
|
||||
- **AND** no new context message is added to the session history
|
||||
|
||||
#### Scenario: Revert failure handling
|
||||
- **WHEN** the revert operation fails (message already gone or session changed)
|
||||
#### Scenario: Ignore previous context part and re-inject
|
||||
- **WHEN** in-place update is not available
|
||||
- **AND** a previous context part exists for the session
|
||||
- **THEN** the plugin marks the previous context part as `ignored: true`
|
||||
- **AND** injects new context with `noReply: true`
|
||||
- **AND** stores the new message/part IDs for future updates
|
||||
|
||||
#### Scenario: Replacement failure handling
|
||||
- **WHEN** a previous context part cannot be updated or ignored (already removed, invalid IDs)
|
||||
- **THEN** the plugin continues with fresh context injection
|
||||
- **AND** logs the error to console for debugging
|
||||
|
||||
#### Scenario: Never revert
|
||||
- **WHEN** updating context
|
||||
- **THEN** the plugin MUST NOT call `session.revert()` as part of this feature
|
||||
|
||||
### Requirement: Debounced Context Updates
|
||||
The plugin SHALL debounce workspace change events to prevent excessive API calls.
|
||||
|
||||
### Requirement: Session Tracking and URL Persistence
|
||||
The plugin SHALL create and track an OpenCode session and preserve the iframe URL for the duration of the Obsidian process.
|
||||
|
||||
#### Scenario: Create session on first view open
|
||||
- **WHEN** the OpenCode view is opened for the first time in the current Obsidian run
|
||||
- **AND** the OpenCode server is running
|
||||
- **THEN** the plugin creates a new OpenCode session
|
||||
- **AND** updates the iframe URL to the session route
|
||||
- **AND** stores that URL for later restores
|
||||
|
||||
#### Scenario: Restore last URL on reopen
|
||||
- **WHEN** the OpenCode view is closed and reopened
|
||||
- **AND** a previous iframe URL was stored in memory
|
||||
- **THEN** the iframe is loaded with the stored URL
|
||||
|
||||
#### Scenario: Adopt user-changed session
|
||||
- **WHEN** the user navigates to a different session within the iframe UI
|
||||
- **AND** the plugin is about to inject context
|
||||
- **THEN** the plugin reads the iframe `src` URL
|
||||
- **AND** resolves the session ID from that URL
|
||||
- **AND** updates the tracked session and stored URL to match
|
||||
|
||||
#### Scenario: No session route
|
||||
- **WHEN** the iframe URL does not contain a session route
|
||||
- **AND** the plugin is about to inject context
|
||||
- **THEN** the plugin does not inject any context
|
||||
|
||||
#### Scenario: Invalidate stored URL when base changes
|
||||
- **WHEN** hostname, port, or project directory changes
|
||||
- **THEN** the plugin clears the stored URL and tracked session
|
||||
- **AND** a new session is created on the next first open
|
||||
|
||||
#### Scenario: Rapid file switching
|
||||
- **WHEN** the user rapidly opens or closes multiple files
|
||||
- **THEN** the plugin waits 2 seconds after the last change before sending context update
|
||||
@@ -120,4 +164,4 @@ The plugin SHALL format the context as a system reminder containing file paths a
|
||||
- **WHEN** no markdown files are open
|
||||
- **AND** no text is selected
|
||||
- **THEN** no context message is injected
|
||||
- **AND** any previous context message is reverted
|
||||
- **AND** any previous injected context part is marked as `ignored: true`
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
## 1. Dependencies
|
||||
|
||||
- [ ] 1.1 Add `@opencode-ai/sdk` to package.json dependencies
|
||||
- [ ] 1.2 Run `bun install` to install the SDK
|
||||
- [ ] 1.1 No new dependencies required (use direct `fetch()` calls)
|
||||
|
||||
## 2. Types and Settings
|
||||
|
||||
@@ -14,10 +13,10 @@
|
||||
|
||||
## 3. OpenCode Client Module
|
||||
|
||||
- [ ] 3.1 Create `src/OpenCodeClient.ts` with SDK client wrapper
|
||||
- [ ] 3.2 Implement `getCurrentSessionId()` method to get active session
|
||||
- [ ] 3.3 Implement `updateContext()` method with revert + inject logic
|
||||
- [ ] 3.4 Add `contextMessageIds` Map for tracking injected messages per session
|
||||
- [ ] 3.1 Create `src/OpenCodeClient.ts` with a small HTTP wrapper
|
||||
- [ ] 3.2 Implement `createSession()` and session URL helpers
|
||||
- [ ] 3.3 Implement `updateContext()` using update-part or ignore+reinject (no revert)
|
||||
- [ ] 3.4 Track last injected context message/part IDs for the tracked session
|
||||
- [ ] 3.5 Add error handling for API failures (silent catch, log to console)
|
||||
|
||||
## 4. Workspace Context Module
|
||||
@@ -37,9 +36,10 @@
|
||||
- [ ] 5.4 Register `active-leaf-change` event listener (conditional on setting)
|
||||
- [ ] 5.5 Register `layout-change` event listener (conditional on setting)
|
||||
- [ ] 5.6 Register `editor-change` event listener for selection changes (conditional on setting)
|
||||
- [ ] 5.7 Initialize `OpenCodeClient` lazily when first needed
|
||||
- [ ] 5.8 Add server running check before attempting context updates
|
||||
- [ ] 5.9 Trigger initial context injection when server reaches running state
|
||||
- [ ] 5.7 Create an OpenCode session on first view open and store the iframe URL (in-memory)
|
||||
- [ ] 5.8 Before injecting, resolve `sessionID` by parsing the current iframe URL (if no session route, no-op)
|
||||
- [ ] 5.9 Add server running check before attempting context updates
|
||||
- [ ] 5.10 Trigger initial context injection when server reaches running state and a session exists
|
||||
|
||||
## 6. Settings UI
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
- [ ] 7.1 Manual test: Open multiple notes, verify context appears in OpenCode
|
||||
- [ ] 7.2 Manual test: Select text, verify selection appears in context with source file
|
||||
- [ ] 7.3 Manual test: Close notes, verify context updates (old reverted, new injected)
|
||||
- [ ] 7.3 Manual test: Close notes, verify context updates (old ignored/updated, new injected)
|
||||
- [ ] 7.4 Manual test: Clear selection, verify selection section is removed
|
||||
- [ ] 7.5 Manual test: Disable setting, verify no context injection
|
||||
- [ ] 7.6 Manual test: Server not running, verify no errors thrown
|
||||
|
||||
Reference in New Issue
Block a user