diff --git a/opencode_bridge/opencode_client.py b/opencode_bridge/opencode_client.py index 60ca98e..99b48a0 100644 --- a/opencode_bridge/opencode_client.py +++ b/opencode_bridge/opencode_client.py @@ -91,6 +91,16 @@ class OpenCodeClient: result = await self._request("DELETE", "/session/%s" % session_id) return result is not None + async def list_session_messages(self, session_id: str) -> list[dict[str, Any]]: + """List all messages in the given OpenCode session. + + Returns the raw OpenCode response: a list of + `{ info: { role: "user"|"assistant", ... }, parts: [...] }`. + The server extension is responsible for projecting this into a + frontend-friendly `{role, content}[]` shape. + """ + return await self._request("GET", "/session/%s/message" % session_id) + @property def endpoint(self) -> str: return self._config.url diff --git a/opencode_bridge/routes.py b/opencode_bridge/routes.py index 0b9beaf..582537f 100644 --- a/opencode_bridge/routes.py +++ b/opencode_bridge/routes.py @@ -249,6 +249,58 @@ class SessionReleaseHandler(APIHandler): })) +class SessionMessagesHandler(APIHandler): + """List the current session's messages for a notebook (scrollable history). + + Query param: notebook= + Returns: { messages: [{ role: "user"|"assistant", content: string }] } + If no session exists for the notebook yet, returns { messages: [] } + (does NOT create a session just to report emptiness). + """ + + @tornado.web.authenticated + async def get(self): + notebook_path = self.get_query_argument("notebook", "") + if not notebook_path: + self.set_status(400) + self.finish(json.dumps({"error": "missing 'notebook' query parameter"})) + return + sm = get_session_manager(self) + sid = sm.peek(notebook_path) + if sid is None: + self.finish(json.dumps({"messages": []})) + return + try: + client = make_client(self) + raw = await client.list_session_messages(sid) + except OpenCodeError as e: + if "404" in str(e) or "not found" in str(e).lower(): + sm.invalidate(notebook_path) + log.warning("invalidated dead session for %s", notebook_path) + log.exception("list session messages failed") + self.set_status(502) + self.finish(json.dumps({"ok": False, "error": str(e)})) + return + except Exception as e: + log.exception("list session messages failed") + self.set_status(502) + self.finish(json.dumps({"ok": False, "error": str(e)})) + return + + # Project OpenCode's {info, parts}[] into a frontend-friendly + # {role, content}[] by joining the text parts. + messages = [] + for m in raw or []: + info = m.get("info") or {} + role = info.get("role") or "assistant" + parts = m.get("parts") or [] + content = "\n".join( + p.get("text", "") for p in parts if p.get("type") == "text" + ).strip() + messages.append({"role": role, "content": content}) + self.finish(json.dumps({"messages": messages})) + + def setup_route_handlers(web_app): host_pattern = ".*$" base_url = web_app.settings["base_url"] @@ -260,6 +312,7 @@ def setup_route_handlers(web_app): (url_path_join(base_url, "opencode-bridge", "edit"), EditHandler), (url_path_join(base_url, "opencode-bridge", "sessions"), SessionListHandler), (url_path_join(base_url, "opencode-bridge", "session"), SessionReleaseHandler), + (url_path_join(base_url, "opencode-bridge", "session-messages"), SessionMessagesHandler), ] web_app.add_handlers(host_pattern, handlers) diff --git a/opencode_bridge/session_manager.py b/opencode_bridge/session_manager.py index 14b88e8..8409b33 100644 --- a/opencode_bridge/session_manager.py +++ b/opencode_bridge/session_manager.py @@ -83,6 +83,13 @@ class SessionManager: def has_session(self, notebook_path: str) -> bool: return notebook_path in self._sessions + def peek(self, notebook_path: str) -> Optional[str]: + """Return the cached sessionID for the notebook, or None if no + session has been created yet. Does NOT create one (unlike + get_or_create) — used by the history endpoint to avoid spawning + a session just to report that there is none.""" + return self._sessions.get(notebook_path) + def list_sessions(self) -> list[dict]: return [ {"notebookPath": path, "sessionId": sid} diff --git a/opencode_bridge/tests/test_routes.py b/opencode_bridge/tests/test_routes.py index 0832481..7bf78c3 100644 --- a/opencode_bridge/tests/test_routes.py +++ b/opencode_bridge/tests/test_routes.py @@ -1,5 +1,8 @@ import json +import pytest +import tornado.httpclient + class FakeOpenCodeClient: """Drop-in replacement for OpenCodeClient with recording + canned responses.""" @@ -13,6 +16,18 @@ class FakeOpenCodeClient: "info": {"id": "msg-1"}, "parts": [{"type": "text", "text": "def foo():\n return 42\n"}], } + # Canned session-messages list response (list_session_messages). + # Default: one user + one assistant message, mixed text parts. + self.messages_response = [ + { + "info": {"role": "user", "id": "m1"}, + "parts": [{"type": "text", "text": "fix the bug"}], + }, + { + "info": {"role": "assistant", "id": "m2"}, + "parts": [{"type": "text", "text": "```python\nx = 1\n```"}], + }, + ] @property def endpoint(self): @@ -38,6 +53,10 @@ class FakeOpenCodeClient: self.calls.append(("delete_session", session_id)) return True + async def list_session_messages(self, session_id): + self.calls.append(("list_session_messages", session_id)) + return self.messages_response + class FakeSessionManager: """Drop-in replacement for SessionManager with recording.""" @@ -50,6 +69,12 @@ class FakeSessionManager: self.calls.append(("get_or_create", notebook_path)) return self._session_id + def peek(self, notebook_path: str): + # Mirror SessionManager.peek: return the session id without + # creating one. None means "no session yet" (used by the no-session + # test to short-circuit the history route). + return self._session_id or None + async def release(self, notebook_path: str) -> bool: self.calls.append(("release", notebook_path)) return True @@ -177,6 +202,71 @@ async def test_session_list_handler(monkeypatch, jp_fetch): assert paths == {"foo.ipynb", "bar.ipynb"} +async def test_session_messages_handler_returns_empty_when_no_session( + monkeypatch, jp_fetch +) -> None: + # No session registered for this notebook -> handler returns + # {"messages": []} WITHOUT calling OpenCodeClient (peek short-circuits). + fake = FakeOpenCodeClient() + monkeypatch.setattr("opencode_bridge.routes.make_client", lambda h: fake) + + fake_sm = FakeSessionManager(session_id="") + monkeypatch.setattr( + "opencode_bridge.routes.get_session_manager", lambda h: fake_sm + ) + + response = await jp_fetch( + "opencode-bridge", "session-messages", + method="GET", + params={"notebook": "fresh.ipynb"}, + ) + assert response.code == 200 + payload = json.loads(response.body) + assert payload == {"messages": []} + # No OpenCode call was made. + assert all(c[0] != "list_session_messages" for c in fake.calls) + + +async def test_session_messages_handler_projects_opencode_messages( + monkeypatch, jp_fetch +) -> None: + fake = FakeOpenCodeClient() + monkeypatch.setattr("opencode_bridge.routes.make_client", lambda h: fake) + + fake_sm = FakeSessionManager(session_id="fake-session-123") + monkeypatch.setattr( + "opencode_bridge.routes.get_session_manager", lambda h: fake_sm + ) + + response = await jp_fetch( + "opencode-bridge", "session-messages", + method="GET", + params={"notebook": "test.ipynb"}, + ) + assert response.code == 200 + payload = json.loads(response.body) + assert payload == { + "messages": [ + {"role": "user", "content": "fix the bug"}, + {"role": "assistant", "content": "```python\nx = 1\n```"}, + ] + } + # The OpenCode client was called with the session id from the manager. + assert ("list_session_messages", "fake-session-123") in fake.calls + + +async def test_session_messages_handler_requires_notebook( + monkeypatch, jp_fetch +) -> None: + fake = FakeOpenCodeClient() + monkeypatch.setattr("opencode_bridge.routes.make_client", lambda h: fake) + # jp_fetch raises HTTPClientError on 4xx; assert the handler 400s + # (the body would contain "missing 'notebook' query parameter"). + with pytest.raises(tornado.httpclient.HTTPClientError) as exc_info: + await jp_fetch("opencode-bridge", "session-messages", method="GET") + assert exc_info.value.code == 400 + + async def test_session_release_handler(monkeypatch, jp_fetch): fake = FakeOpenCodeClient() monkeypatch.setattr("opencode_bridge.routes.make_client", lambda h: fake) diff --git a/src/__tests__/opencode_cell_actions.spec.ts b/src/__tests__/opencode_cell_actions.spec.ts index 64d1d8f..5090bf8 100644 --- a/src/__tests__/opencode_cell_actions.spec.ts +++ b/src/__tests__/opencode_cell_actions.spec.ts @@ -52,6 +52,15 @@ jest.mock('marked', () => ({ marked: { parse: (md: string) => `${md}` } })); +// Mock the network-touching API module so jsdom tests don't try to hit +// a real notebook server (and to keep the refresh-history logic decoupled +// from the network). The cell_actions tests focus on widget behavior. +jest.mock('../api/opencode_client', () => ({ + callOpenCodeEdit: jest.fn(), + callOpenCodeProviders: jest.fn(), + callOpenCodeSessionMessages: jest.fn().mockResolvedValue({ messages: [] }) +})); + import { OpenCodeCellActions, setOpenCodeProviders, @@ -330,17 +339,21 @@ describe('OpenCodeCellActions', () => { ).toBeNull(); }); - it('on a successful response, does NOT replace the cell source (renders to inline output instead)', () => { + it('on a successful response, does NOT replace the cell source and triggers a history refresh', () => { setOpenCodeProviders(null); const cell = makeFakeCell('x = 1'); const actions = new OpenCodeCellActions(cell); - // Click once to create the inline prompt (so setOutput has a target). + // Click once to create the inline prompt (so the history refresh + // has a target and the cell source check has a baseline). Object.defineProperty(actions, 'parent', { value: cell, configurable: true }); (actions as any).onAfterAttach({} as any); const btn = actions.node.querySelector('button') as HTMLButtonElement; btn.click(); - // Simulate a successful response. + // The history refresh on show is async; spy on _refreshHistory to + // confirm it gets called (without depending on the network). + const refreshSpy = jest.spyOn(actions as any, '_refreshHistory'); + (actions as any)._handleResponse({ ok: true, markdown: '# AI says hi\n\n```python\nprint("hi")\n```', @@ -350,11 +363,8 @@ describe('OpenCodeCellActions', () => { // The cell source must remain unchanged. expect((cell as any).model.sharedModel.setSource).not.toHaveBeenCalled(); - // The inline output area is now visible with the rendered markdown. - const output = cell.node.querySelector( - '.opencode-inline-prompt .opencode-inline-output' - ) as HTMLElement; - expect(output.style.display).toBe('block'); + // A history refresh was triggered (to pick up the new assistant msg). + expect(refreshSpy).toHaveBeenCalledWith('foo.ipynb'); }); }); @@ -368,11 +378,12 @@ describe('OpenCodeInlinePrompt', () => { providers: null }); expect(prompt.node.querySelector('textarea')).not.toBeNull(); - // 3 buttons: 发送, 取消, and the output area close (✕). - expect(prompt.node.querySelectorAll('button').length).toBe(3); + // 2 buttons: 发送 and 取消 (no more output-area close button; the + // scrollable history area is the display). + expect(prompt.node.querySelectorAll('button').length).toBe(2); }); - it('hides the output area by default; setOutput reveals it with rendered markdown', () => { + it('renders a history area; setMessages renders user text + assistant markdown and scrolls to bottom', () => { const cell = makeFakeCell('x = 1'); const prompt = new OpenCodeInlinePrompt(cell, { onSubmit: jest.fn(), @@ -380,22 +391,30 @@ describe('OpenCodeInlinePrompt', () => { disabled: false, providers: null }); - const output = prompt.node.querySelector( - '.opencode-inline-prompt .opencode-inline-output' + const history = prompt.node.querySelector( + '.opencode-inline-prompt .opencode-inline-history' ) as HTMLElement; - expect(output).not.toBeNull(); - expect(output.style.display).toBe('none'); + expect(history).not.toBeNull(); + // Initially empty (no messages yet). + expect(history.children.length).toBe(0); - prompt.setOutput('# Hello\n\n```python\nx = 1\n```'); - expect(output.style.display).toBe('block'); - const content = prompt.node.querySelector( - '.opencode-inline-prompt .opencode-inline-output-content' - ) as HTMLElement; - // marked is mocked to wrap the input in .... - expect(content.innerHTML).toContain(''); - expect(content.innerHTML).toContain('# Hello'); - - prompt.hideOutput(); - expect(output.style.display).toBe('none'); + prompt.setMessages([ + { role: 'user', content: 'fix the bug' }, + { + role: 'assistant', + content: '# Here you go\n\n```python\nx = 1\n```' + } + ]); + expect(history.children.length).toBe(2); + const userMsg = history.querySelector('.opencode-msg-user') as HTMLElement; + const asstMsg = history.querySelector('.opencode-msg-assistant') as HTMLElement; + expect(userMsg).not.toBeNull(); + // User messages are plain text (not rendered as HTML). + expect(userMsg.textContent).toBe('fix the bug'); + expect(userMsg.innerHTML).not.toContain('<'); + // Assistant messages are rendered as markdown via marked (mocked). + expect(asstMsg).not.toBeNull(); + expect(asstMsg.innerHTML).toContain(''); + expect(asstMsg.innerHTML).toContain('# Here you go'); }); }); diff --git a/src/api/opencode_client.ts b/src/api/opencode_client.ts index 99ff878..c2b74f6 100644 --- a/src/api/opencode_client.ts +++ b/src/api/opencode_client.ts @@ -4,7 +4,7 @@ import { URLExt } from '@jupyterlab/coreutils'; import { ServerConnection } from '@jupyterlab/services'; -import type { OpenCodeProvidersResponse, OpenCodeRequest, OpenCodeResponse } from '../types'; +import type { OpenCodeProvidersResponse, OpenCodeRequest, OpenCodeResponse, OpenCodeMessagesResponse } from '../types'; /** * Call POST /opencode-bridge/edit. Returns parsed response (success or failure). @@ -54,6 +54,37 @@ export async function callOpenCodeEdit( return parsed as OpenCodeResponse; } +/** + * Call GET /opencode-bridge/session-messages?notebook=. Returns the + * current session's messages (projected to {role, content}[]) for the + * given notebook, or an empty list if no session exists yet. + */ +export async function callOpenCodeSessionMessages( + notebookPath: string, + serverSettings: ServerConnection.ISettings +): Promise { + const url = + URLExt.join(serverSettings.baseUrl, 'opencode-bridge', 'session-messages') + + '?notebook=' + + encodeURIComponent(notebookPath); + + const init: RequestInit = { method: 'GET' }; + let response: Response; + try { + response = await ServerConnection.makeRequest(url, init, serverSettings); + } catch (error) { + throw new Error( + `network error calling opencode-bridge/session-messages: ${(error as Error).message}` + ); + } + if (!response.ok) { + throw new Error( + `opencode-bridge/session-messages failed: ${response.status} ${response.statusText}` + ); + } + return (await response.json()) as OpenCodeMessagesResponse; +} + /** * Call GET /opencode-bridge/providers. Returns the parsed JSON response. * Throws on network error or non-2xx status. diff --git a/src/components/opencode_cell_actions.ts b/src/components/opencode_cell_actions.ts index 191113e..d64bfaf 100644 --- a/src/components/opencode_cell_actions.ts +++ b/src/components/opencode_cell_actions.ts @@ -16,7 +16,7 @@ import type { NotebookPanel } from '@jupyterlab/notebook'; import { ServerConnection } from '@jupyterlab/services'; import { Widget } from '@lumino/widgets'; -import { callOpenCodeEdit } from '../api/opencode_client'; +import { callOpenCodeEdit, callOpenCodeSessionMessages } from '../api/opencode_client'; import { extractCellContext } from '../context/cell_context'; import type { CellContext, @@ -123,6 +123,31 @@ export class OpenCodeCellActions extends Widget { }); Widget.attach(prompt, this._cell.node); this._prompt = prompt; + + // Fetch the current session's message history and render it into + // the prompt's scrollable history area. Empty list (no session yet) + // is a normal no-op render. + const notebookPath = this._context?.notebookPath; + if (notebookPath) { + void this._refreshHistory(notebookPath); + } + } + + private async _refreshHistory(notebookPath: string): Promise { + if (!_serverSettings || !this._prompt) { + return; + } + try { + const resp = await callOpenCodeSessionMessages( + notebookPath, + _serverSettings + ); + this._prompt.setMessages(resp.messages); + } catch (e) { + // Non-fatal: the history is a convenience. The user can still + // send new prompts; just the scrollback won't update. + console.warn('opencode_bridge: failed to fetch session messages', e); + } } private _hidePrompt(): void { @@ -172,11 +197,12 @@ export class OpenCodeCellActions extends Widget { private _handleResponse(resp: OpenCodeResponse): void { this._status = 'idle'; if (resp.ok) { - // Display the AI response in the inline prompt's output area as - // rendered markdown. The cell source is NOT replaced — the user - // decides what to do with the output (close it, or resend). - if (this._prompt) { - this._prompt.setOutput(resp.markdown); + // Refetch the (now-updated) session history and render the new + // assistant message as the last item in the scrollable history. + // The cell source is NOT replaced. + const notebookPath = this._context?.notebookPath; + if (notebookPath) { + void this._refreshHistory(notebookPath); } Notification.info( `OpenCode 完成 (${resp.markdown.length} chars). Session: ${resp.sessionId.slice(0, 8)}…` diff --git a/src/components/opencode_inline_prompt.ts b/src/components/opencode_inline_prompt.ts index 949d9ee..ab4a4d1 100644 --- a/src/components/opencode_inline_prompt.ts +++ b/src/components/opencode_inline_prompt.ts @@ -1,17 +1,19 @@ /** * Inline prompt widget attached to a cell's DOM when the AI button is clicked. - * Renders two 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; } } diff --git a/src/types.ts b/src/types.ts index 8fc36b3..1f8a416 100644 --- a/src/types.ts +++ b/src/types.ts @@ -49,13 +49,20 @@ export interface OpenCodeFailure { export type OpenCodeResponse = OpenCodeSuccess | OpenCodeFailure; +/** Response from GET /opencode-bridge/session-messages?notebook=... . + * Projected from OpenCode's {info, parts}[] by the server into a + * frontend-friendly {role, content}[]. */ +export interface OpenCodeMessage { + role: "user" | "assistant"; + content: string; +} + +export interface OpenCodeMessagesResponse { + messages: OpenCodeMessage[]; +} + /** Response from GET /opencode-bridge/providers (proxies OpenCode /config/providers). - * - * Per the OpenCode server docs, `/config/providers` returns - * `{ providers: Provider[], default: { [providerID]: modelID } }`. Each - * `Provider.models` is a Record keyed by modelID (NOT an array), so we - * can reference a model by its string ID (also matching `default[providerID]`). - */ + * Each Provider.models is a Record keyed by modelID (NOT an array). */ export interface OpenCodeModel { id: string; name?: string; diff --git a/style/base.css b/style/base.css index 7748d43..b742521 100644 --- a/style/base.css +++ b/style/base.css @@ -75,48 +75,43 @@ cursor: default; } -/* Markdown output area: hidden until setOutput() reveals it. */ -.opencode-inline-prompt .opencode-inline-output { - display: none; - margin-top: 4px; - padding: 6px 8px; +/* Scrollable history of the current session's messages. The owning + cell action populates it via setMessages(). max-height keeps the + inline prompt compact; the user scrolls to see older messages. */ +.opencode-inline-prompt .opencode-inline-history { + max-height: 320px; + overflow-y: auto; + margin: 4px 0; + padding: 4px 6px; border: 1px solid var(--jp-border-color1, #c0c0c0); border-radius: 3px; background: var(--jp-layout-color2, #f7f7f7); - max-height: 320px; - overflow-y: auto; -} - -.opencode-inline-prompt .opencode-inline-output-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 4px; - font-size: var(--jp-ui-font-size1); - font-weight: 600; - color: var(--jp-ui-font-color1, #333); -} - -.opencode-inline-prompt .opencode-inline-output-close { - border: none; - background: transparent; - cursor: pointer; - font-size: var(--jp-ui-font-size1); - line-height: 1; - padding: 0 4px; - color: var(--jp-ui-font-color2, #666); -} - -.opencode-inline-prompt .opencode-inline-output-close:hover { - color: var(--jp-ui-font-color0, #000); -} - -.opencode-inline-prompt .opencode-inline-output-content { font-size: var(--jp-ui-font-size1); line-height: 1.45; } -.opencode-inline-prompt .opencode-inline-output-content pre { +.opencode-inline-prompt .opencode-msg { + margin-bottom: 6px; +} + +.opencode-inline-prompt .opencode-msg:last-child { + margin-bottom: 0; +} + +.opencode-inline-prompt .opencode-msg-user { + white-space: pre-wrap; + padding: 4px 6px; + border-radius: 3px; + background: var(--jp-layout-color3, #eaeaea); + color: var(--jp-ui-font-color1, #333); +} + +.opencode-inline-prompt .opencode-msg-assistant { + padding: 4px 6px; + color: var(--jp-ui-font-color0, #000); +} + +.opencode-inline-prompt .opencode-inline-history pre { background: var(--jp-layout-color1, #fff); padding: 6px 8px; border-radius: 3px; @@ -124,18 +119,18 @@ margin: 4px 0; } -.opencode-inline-prompt .opencode-inline-output-content code { +.opencode-inline-prompt .opencode-inline-history code { font-family: var(--jp-code-font-family, monospace); font-size: var(--jp-code-font-size, 13px); } -.opencode-inline-prompt .opencode-inline-output-content p { +.opencode-inline-prompt .opencode-inline-history p { margin: 2px 0; } -.opencode-inline-prompt .opencode-inline-output-content h1, -.opencode-inline-prompt .opencode-inline-output-content h2, -.opencode-inline-prompt .opencode-inline-output-content h3 { +.opencode-inline-prompt .opencode-inline-history h1, +.opencode-inline-prompt .opencode-inline-history h2, +.opencode-inline-prompt .opencode-inline-history h3 { margin: 6px 0 4px; font-size: var(--jp-ui-font-size2, 14px); }