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,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)
|
||||
|
||||
Reference in New Issue
Block a user