feat: inline prompt history area — scrollable session messages
The inline prompt now shows the current session's full message
history (user + assistant), scrollable, replacing the single-output
display from the previous commit. The user can scroll back through
prior exchanges in the same notebook's OpenCode session.
Server (opencode_bridge/):
- New route: GET /opencode-bridge/session-messages?notebook=<path>
- Resolves notebook to session via SessionManager.peek (no create).
- If no session: {"messages": []} (no OpenCode call).
- Else: GET /session/{id}/message on OpenCode Serve, project the
raw {info, parts}[] into a frontend-friendly {role, content}[].
- OpenCodeClient.list_session_messages(sid).
- SessionManager.peek(notebook_path) -> Optional[str] (read without
creating, to avoid spawning a session just to report emptiness).
- Wired the new route in setup_route_handlers.
Client (src/):
- types.ts: OpenCodeMessage { role, content } + OpenCodeMessagesResponse.
- api/opencode_client.ts: callOpenCodeSessionMessages(notebook, serverSettings).
- components/opencode_inline_prompt.ts:
- Replaces the single .opencode-inline-output area with a
scrollable .opencode-inline-history (max-height 320px, overflow-y
auto, auto-scrolls to bottom on update).
- setMessages(messages): renders user messages as plain text,
assistant messages via marked.parse. No more setOutput/hideOutput.
- components/opencode_cell_actions.ts:
- _showPrompt now fires a _refreshHistory(notebookPath) which
fetches and calls prompt.setMessages.
- _handleResponse on success also calls _refreshHistory (the new
assistant message appears as the last item in the history).
- Cell source is still NOT replaced.
- api/opencode_client module is mocked in the cell_actions test to
avoid jsdom network calls.
style/base.css:
- .opencode-inline-output* rules replaced with .opencode-inline-history
(max-height 320px, overflow-y auto, border, padding) and
.opencode-msg / .opencode-msg-user / .opencode-msg-assistant.
- pre/code/p/h1-3 content styling scoped under .opencode-inline-history.
Tests:
- pytest 37/37: FakeOpenCodeClient.list_session_messages + 3 new
session_messages route tests (no session, projects messages, 400).
- jest 29/29: setMessages renders user/assistant + history area tests.
- FakeSessionManager.peek (returns None when session_id is falsy).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
1d7f5da9d4
commit
ce59502e97
@@ -1,17 +1,19 @@
|
||||
/**
|
||||
* Inline prompt widget attached to a cell's DOM when the AI button is clicked.
|
||||
* Renders two <select> boxes (provider + model), a textarea, and send/cancel
|
||||
* buttons. The model select's default for the chosen provider is
|
||||
* `default[providerID]` (from the /config/providers response) when that
|
||||
* modelID is present in the provider's models; otherwise the first model.
|
||||
* Renders two <select> boxes (provider + model), a textarea, send/cancel
|
||||
* buttons, and a scrollable history area that shows the current session's
|
||||
* messages (user + assistant). The model select's default for the chosen
|
||||
* provider is `default[providerID]` from the /config/providers response.
|
||||
*
|
||||
* v3-final: no settings — picks come from the OpenCode Serve providers cache.
|
||||
* v3-final: no settings — picks come from the OpenCode Serve providers
|
||||
* cache; history comes from /session-messages; cell source is NOT replaced
|
||||
* (the AI reply is rendered as markdown inside this history area).
|
||||
*/
|
||||
import { CodeCell } from '@jupyterlab/cells';
|
||||
import { Widget } from '@lumino/widgets';
|
||||
import { marked } from 'marked';
|
||||
|
||||
import type { OpenCodeProvidersResponse } from '../types';
|
||||
import type { OpenCodeMessage, OpenCodeProvidersResponse } from '../types';
|
||||
|
||||
export interface IOpenCodeInlinePromptOptions {
|
||||
onSubmit: (text: string, providerId?: string, modelId?: string) => void;
|
||||
@@ -70,8 +72,7 @@ export class OpenCodeInlinePrompt extends Widget {
|
||||
private _modelSelect: HTMLSelectElement | null = null;
|
||||
private _providerModels: FlatProvider[];
|
||||
private _defaultMap: { [pid: string]: string };
|
||||
private _outputArea: HTMLDivElement;
|
||||
private _outputContent: HTMLDivElement;
|
||||
private _historyEl: HTMLDivElement;
|
||||
|
||||
constructor(
|
||||
_cell: CodeCell,
|
||||
@@ -154,33 +155,15 @@ export class OpenCodeInlinePrompt extends Widget {
|
||||
}
|
||||
this.node.appendChild(row);
|
||||
}
|
||||
|
||||
// Scrollable history of the current session's messages. Empty until
|
||||
// setMessages() is called by the owning cell action.
|
||||
this._historyEl = document.createElement('div');
|
||||
this._historyEl.className = 'opencode-inline-history';
|
||||
this.node.appendChild(this._historyEl);
|
||||
|
||||
this.node.appendChild(this._textarea);
|
||||
this.node.appendChild(actions);
|
||||
|
||||
// Output area: hidden by default; revealed by setOutput() with the
|
||||
// markdown-rendered AI response. The cell source is NOT replaced.
|
||||
this._outputArea = document.createElement('div');
|
||||
this._outputArea.className = 'opencode-inline-output';
|
||||
this._outputArea.style.display = 'none';
|
||||
const outputHeader = document.createElement('div');
|
||||
outputHeader.className = 'opencode-inline-output-header';
|
||||
const outputTitle = document.createElement('span');
|
||||
outputTitle.textContent = '结果';
|
||||
const outputCloseBtn = document.createElement('button');
|
||||
outputCloseBtn.className = 'opencode-inline-output-close';
|
||||
outputCloseBtn.type = 'button';
|
||||
outputCloseBtn.textContent = '✕';
|
||||
outputCloseBtn.title = '关闭输出';
|
||||
outputCloseBtn.addEventListener('click', () => {
|
||||
this.hideOutput();
|
||||
});
|
||||
outputHeader.appendChild(outputTitle);
|
||||
outputHeader.appendChild(outputCloseBtn);
|
||||
this._outputContent = document.createElement('div');
|
||||
this._outputContent.className = 'opencode-inline-output-content';
|
||||
this._outputArea.appendChild(outputHeader);
|
||||
this._outputArea.appendChild(this._outputContent);
|
||||
this.node.appendChild(this._outputArea);
|
||||
}
|
||||
|
||||
private _rebuildModelSelect(providerId: string | undefined): void {
|
||||
@@ -226,18 +209,23 @@ export class OpenCodeInlinePrompt extends Widget {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the AI response as markdown and show the output area in place
|
||||
* of replacing the cell source. Safe for local-trusted content from the
|
||||
* user's OpenCode Serve; do not feed untrusted markdown here without
|
||||
* adding a sanitizer (e.g. DOMPurify).
|
||||
* Render the current session's messages into the scrollable history
|
||||
* area. User messages are plain text; assistant messages are rendered
|
||||
* as markdown via marked. Auto-scrolls to the bottom so the most
|
||||
* recent message is visible.
|
||||
*/
|
||||
setOutput(markdown: string): void {
|
||||
this._outputContent.innerHTML = marked.parse(markdown) as string;
|
||||
this._outputArea.style.display = 'block';
|
||||
}
|
||||
|
||||
hideOutput(): void {
|
||||
this._outputArea.style.display = 'none';
|
||||
this._outputContent.textContent = '';
|
||||
setMessages(messages: OpenCodeMessage[]): void {
|
||||
this._historyEl.textContent = '';
|
||||
for (const msg of messages) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = `opencode-msg opencode-msg-${msg.role}`;
|
||||
if (msg.role === 'user') {
|
||||
wrap.textContent = msg.content;
|
||||
} else {
|
||||
wrap.innerHTML = marked.parse(msg.content) as string;
|
||||
}
|
||||
this._historyEl.appendChild(wrap);
|
||||
}
|
||||
this._historyEl.scrollTop = this._historyEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user