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:
tao.chen
2026-07-23 18:45:03 +08:00
co-authored by Claude Fable 5
parent 1d7f5da9d4
commit ce59502e97
10 changed files with 350 additions and 124 deletions
+33 -45
View File
@@ -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;
}
}