Add read-only initial context for chat sessions for TUI and web app
This commit is contained in:
parent
bb634e68a4
commit
614c34b98f
11 changed files with 282 additions and 51 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -1,6 +1,16 @@
|
|||
# Changelog
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- **Read-Only Initial Context**: Initial context is now locked after the first message, providing consistent session context
|
||||
- Chat TUI: `--initial-context` CLI option sets background context for the session
|
||||
- Context can be edited via command palette before the first message is sent
|
||||
- After first message, context becomes read-only (view only)
|
||||
- Clearing chat resets context to CLI value and unlocks editing
|
||||
- Web app: Memory panel now serves dual purpose - edit initial context before first message, view session context after
|
||||
- Agent uses `initial_context` as fallback when `session_context` is empty
|
||||
|
||||
## [0.27.1] - 2026-01-27
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ async def stream_chat(request: Request) -> Response:
|
|||
initial_qa_history: list[QAResponse] = []
|
||||
session_id: str | None = None
|
||||
document_filter: list[str] = []
|
||||
initial_context: str | None = None
|
||||
state = getattr(run_input, "state", None)
|
||||
if state:
|
||||
chat_state = state.get(AGUI_STATE_KEY, state)
|
||||
|
|
@ -92,6 +93,7 @@ async def stream_chat(request: Request) -> Response:
|
|||
]
|
||||
session_id = chat_state.get("session_id")
|
||||
document_filter = chat_state.get("document_filter", [])
|
||||
initial_context = chat_state.get("initial_context")
|
||||
|
||||
deps = ChatDeps(
|
||||
client=get_client(db_path),
|
||||
|
|
@ -99,6 +101,7 @@ async def stream_chat(request: Request) -> Response:
|
|||
session_state=ChatSessionState(
|
||||
qa_history=initial_qa_history,
|
||||
document_filter=document_filter,
|
||||
initial_context=initial_context,
|
||||
**({"session_id": session_id} if session_id else {}),
|
||||
),
|
||||
state_key=AGUI_STATE_KEY,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ interface SessionContext {
|
|||
|
||||
interface ChatSessionState {
|
||||
session_id: string;
|
||||
initial_context: string | null;
|
||||
citations: Citation[];
|
||||
qa_history: QAResponse[];
|
||||
session_context: SessionContext | null;
|
||||
|
|
@ -398,6 +399,7 @@ function ChatContentInner() {
|
|||
initialState: {
|
||||
[AGUI_STATE_KEY]: {
|
||||
session_id: "",
|
||||
initial_context: null,
|
||||
citations: [],
|
||||
qa_history: [],
|
||||
session_context: null,
|
||||
|
|
@ -407,9 +409,14 @@ function ChatContentInner() {
|
|||
},
|
||||
);
|
||||
|
||||
// Extract session context and document filter from agent state
|
||||
// Extract session context, document filter, and initial context from agent state
|
||||
const sessionContext = agentState?.[AGUI_STATE_KEY]?.session_context ?? null;
|
||||
const documentFilter = agentState?.[AGUI_STATE_KEY]?.document_filter ?? [];
|
||||
const initialContext = agentState?.[AGUI_STATE_KEY]?.initial_context ?? "";
|
||||
|
||||
// Context is locked after first message (qa_history has entries)
|
||||
const isContextLocked =
|
||||
(agentState?.[AGUI_STATE_KEY]?.qa_history?.length ?? 0) > 0;
|
||||
|
||||
const handleFilterApply = (selected: string[]) => {
|
||||
setAgentState({
|
||||
|
|
@ -417,6 +424,7 @@ function ChatContentInner() {
|
|||
[AGUI_STATE_KEY]: {
|
||||
...agentState?.[AGUI_STATE_KEY],
|
||||
session_id: agentState?.[AGUI_STATE_KEY]?.session_id ?? "",
|
||||
initial_context: agentState?.[AGUI_STATE_KEY]?.initial_context ?? null,
|
||||
citations: agentState?.[AGUI_STATE_KEY]?.citations ?? [],
|
||||
qa_history: agentState?.[AGUI_STATE_KEY]?.qa_history ?? [],
|
||||
session_context: agentState?.[AGUI_STATE_KEY]?.session_context ?? null,
|
||||
|
|
@ -425,6 +433,22 @@ function ChatContentInner() {
|
|||
});
|
||||
};
|
||||
|
||||
const handleInitialContextChange = (value: string) => {
|
||||
if (isContextLocked) return;
|
||||
setAgentState({
|
||||
...agentState,
|
||||
[AGUI_STATE_KEY]: {
|
||||
...agentState?.[AGUI_STATE_KEY],
|
||||
session_id: agentState?.[AGUI_STATE_KEY]?.session_id ?? "",
|
||||
initial_context: value || null,
|
||||
citations: agentState?.[AGUI_STATE_KEY]?.citations ?? [],
|
||||
qa_history: agentState?.[AGUI_STATE_KEY]?.qa_history ?? [],
|
||||
session_context: agentState?.[AGUI_STATE_KEY]?.session_context ?? null,
|
||||
document_filter: agentState?.[AGUI_STATE_KEY]?.document_filter ?? [],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useCoAgentStateRender<AgentState>({
|
||||
name: "chat_agent",
|
||||
render: ({ state }) => {
|
||||
|
|
@ -568,12 +592,16 @@ function ChatContentInner() {
|
|||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`header-btn ${sessionContext?.summary ? "has-content" : ""}`}
|
||||
className={`header-btn ${initialContext || sessionContext?.summary ? "has-content" : ""}`}
|
||||
onClick={() => setContextOpen(true)}
|
||||
title={
|
||||
sessionContext?.summary
|
||||
? "View session context"
|
||||
: "No session context yet"
|
||||
isContextLocked
|
||||
? sessionContext?.summary
|
||||
? "View session context"
|
||||
: "No session context yet"
|
||||
: initialContext
|
||||
? "Edit initial context"
|
||||
: "Set initial context"
|
||||
}
|
||||
>
|
||||
<BrainIcon />
|
||||
|
|
@ -596,6 +624,9 @@ function ChatContentInner() {
|
|||
isOpen={contextOpen}
|
||||
onClose={() => setContextOpen(false)}
|
||||
sessionContext={sessionContext}
|
||||
initialContext={initialContext}
|
||||
onInitialContextChange={handleInitialContextChange}
|
||||
isLocked={isContextLocked}
|
||||
/>
|
||||
<DocumentFilter
|
||||
isOpen={filterOpen}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useId } from "react";
|
||||
import { useCallback, useEffect, useId, useState } from "react";
|
||||
|
||||
interface SessionContext {
|
||||
summary: string;
|
||||
|
|
@ -11,6 +11,9 @@ interface ContextPanelProps {
|
|||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
sessionContext: SessionContext | null;
|
||||
initialContext?: string;
|
||||
onInitialContextChange?: (value: string) => void;
|
||||
isLocked?: boolean;
|
||||
}
|
||||
|
||||
function formatRelativeTime(isoString: string): string {
|
||||
|
|
@ -62,8 +65,18 @@ export default function ContextPanel({
|
|||
isOpen,
|
||||
onClose,
|
||||
sessionContext,
|
||||
initialContext = "",
|
||||
onInitialContextChange,
|
||||
isLocked = false,
|
||||
}: ContextPanelProps) {
|
||||
const titleId = useId();
|
||||
const [localValue, setLocalValue] = useState(initialContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setLocalValue(initialContext);
|
||||
}
|
||||
}, [isOpen, initialContext]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
|
|
@ -74,11 +87,18 @@ export default function ContextPanel({
|
|||
[onClose],
|
||||
);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
onInitialContextChange?.(localValue);
|
||||
onClose();
|
||||
}, [localValue, onInitialContextChange, onClose]);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasContext = sessionContext?.summary && sessionContext.summary.trim();
|
||||
const hasSessionContext = sessionContext?.summary?.trim();
|
||||
// Show edit mode when: not locked AND no session context yet
|
||||
const isEditMode = !isLocked && !hasSessionContext;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -146,6 +166,24 @@ export default function ContextPanel({
|
|||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.context-textarea {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
padding: 1rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
color: #334155;
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
}
|
||||
.context-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
.context-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -175,13 +213,15 @@ export default function ContextPanel({
|
|||
font-size: 0.75rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.context-btn-close {
|
||||
.context-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.context-btn-close {
|
||||
background: white;
|
||||
color: #475569;
|
||||
border: 1px solid #e2e8f0;
|
||||
|
|
@ -190,6 +230,20 @@ export default function ContextPanel({
|
|||
background: #f8fafc;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
.context-btn-save {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: 1px solid #3b82f6;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.context-btn-save:hover {
|
||||
background: #2563eb;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
.context-footer-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
`}</style>
|
||||
<div
|
||||
className="context-modal-overlay"
|
||||
|
|
@ -210,14 +264,22 @@ export default function ContextPanel({
|
|||
<BrainIcon />
|
||||
</div>
|
||||
<h2 id={titleId} className="context-modal-title">
|
||||
Session Context
|
||||
{isEditMode ? "Initial Context" : "Session Context"}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="context-modal-description">
|
||||
This is what the assistant has learned from your conversation so
|
||||
far. It uses this context to provide more relevant answers.
|
||||
{isEditMode
|
||||
? "Set background context to guide the conversation. This will be locked after you send your first message."
|
||||
: "This is what the assistant has learned from your conversation so far. It uses this context to provide more relevant answers."}
|
||||
</p>
|
||||
{hasContext ? (
|
||||
{isEditMode ? (
|
||||
<textarea
|
||||
className="context-textarea"
|
||||
placeholder="Enter any background context or instructions for the assistant..."
|
||||
value={localValue}
|
||||
onChange={(e) => setLocalValue(e.target.value)}
|
||||
/>
|
||||
) : hasSessionContext ? (
|
||||
<div className="context-content">{sessionContext.summary}</div>
|
||||
) : (
|
||||
<div className="context-empty">
|
||||
|
|
@ -235,13 +297,24 @@ export default function ContextPanel({
|
|||
? `Last updated: ${formatRelativeTime(sessionContext.last_updated)}`
|
||||
: ""}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="context-btn-close"
|
||||
onClick={onClose}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<div className="context-footer-buttons">
|
||||
<button
|
||||
type="button"
|
||||
className="context-btn context-btn-close"
|
||||
onClick={onClose}
|
||||
>
|
||||
{isEditMode ? "Cancel" : "Close"}
|
||||
</button>
|
||||
{isEditMode && (
|
||||
<button
|
||||
type="button"
|
||||
className="context-btn context-btn-save"
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ Press `Ctrl+P` to open the command palette:
|
|||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| Memory | Edit initial context (before first message) or view session context (after) |
|
||||
| Filter documents | Select documents to restrict searches |
|
||||
| Show context | View current session context |
|
||||
| Show database info | View document/chunk counts and storage info |
|
||||
| Visual grounding | View chunk source location in document |
|
||||
| Clear chat | Clear chat history and reset session |
|
||||
|
|
@ -43,7 +43,9 @@ Press `Ctrl+P` to open the command palette:
|
|||
- Previous Q/A pairs are used as context for follow-up questions
|
||||
- Citations are tracked per response and can be inspected
|
||||
- Document filter restricts all searches to selected documents
|
||||
- Clearing chat resets session state but preserves document filter
|
||||
- Initial context can be set via CLI (`--initial-context`) or command palette
|
||||
- Initial context is editable until the first message is sent, then becomes read-only
|
||||
- Clearing chat resets session state, restores CLI-provided context, and unlocks editing
|
||||
|
||||
## Web Application
|
||||
|
||||
|
|
@ -55,8 +57,7 @@ Browser-based conversational RAG with a CopilotKit frontend.
|
|||
- Expandable citations with source documents, pages, and headings
|
||||
- Visual grounding to view chunk source locations in documents
|
||||
- Document filter to restrict searches to selected documents
|
||||
- Session context that summarizes conversation history
|
||||
- Settings panel for background context configuration
|
||||
- Memory panel: set initial context before first message, view session context after
|
||||
|
||||
### Quick Start
|
||||
|
||||
|
|
|
|||
17
docs/cli.md
17
docs/cli.md
|
|
@ -178,10 +178,9 @@ haiku-rag chat
|
|||
haiku-rag chat --db /path/to/database.lancedb
|
||||
```
|
||||
|
||||
Provide background context for the conversation:
|
||||
Provide initial background context for the conversation:
|
||||
```bash
|
||||
haiku-rag chat --context "Focus on Python programming concepts"
|
||||
haiku-rag chat --context-file domain-context.txt
|
||||
haiku-rag chat --initial-context "Focus on Python programming concepts"
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
|
@ -193,12 +192,18 @@ The chat interface provides:
|
|||
- Expandable citations with source metadata
|
||||
- Session memory for context-aware follow-up questions
|
||||
- Visual grounding to inspect chunk source locations
|
||||
- Background context that persists across the entire conversation
|
||||
- Initial context that can be edited before the first message
|
||||
|
||||
**Initial Context Behavior:**
|
||||
|
||||
- Edit initial context via command palette before sending your first message
|
||||
- Once you send a message, initial context becomes read-only
|
||||
- The agent uses initial context as a starting point for session summarization
|
||||
- Clearing chat resets to the CLI-provided context and unlocks editing
|
||||
|
||||
Flags:
|
||||
|
||||
- `--context`: Background context for the conversation
|
||||
- `--context-file`: Path to a file containing background context
|
||||
- `--initial-context`: Initial background context for the conversation (editable until first message)
|
||||
|
||||
See [Applications](apps.md#chat-tui) for keyboard shortcuts and features.
|
||||
|
||||
|
|
|
|||
|
|
@ -133,8 +133,10 @@ class ChatDeps:
|
|||
)
|
||||
if "citation_registry" in state_data:
|
||||
self.session_state.citation_registry = state_data["citation_registry"]
|
||||
# NOTE: session_context intentionally NOT updated from client
|
||||
# The agent owns this via server-side cache
|
||||
if "initial_context" in state_data:
|
||||
self.session_state.initial_context = state_data.get("initial_context")
|
||||
# NOTE: session_context is server-managed; we don't accept it from the client
|
||||
# to maintain server-side ownership of conversation summarization
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ def run_chat(
|
|||
db_path: Path | None = None,
|
||||
read_only: bool = False,
|
||||
before: datetime | None = None,
|
||||
initial_context: str | None = None,
|
||||
) -> None:
|
||||
"""Run the chat TUI.
|
||||
|
||||
|
|
@ -13,6 +14,7 @@ def run_chat(
|
|||
db_path: Path to the LanceDB database. If None, uses default from config.
|
||||
read_only: Whether to open the database in read-only mode.
|
||||
before: Query database as it existed before this datetime.
|
||||
initial_context: Initial background context to provide to the conversation.
|
||||
"""
|
||||
try:
|
||||
from haiku.rag.chat.app import ChatApp
|
||||
|
|
@ -31,5 +33,6 @@ def run_chat(
|
|||
db_path,
|
||||
read_only=read_only,
|
||||
before=before,
|
||||
initial_context=initial_context,
|
||||
)
|
||||
app.run()
|
||||
|
|
|
|||
|
|
@ -88,11 +88,14 @@ class ChatApp(App):
|
|||
db_path: Path,
|
||||
read_only: bool = False,
|
||||
before: datetime | None = None,
|
||||
initial_context: str | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.db_path = db_path
|
||||
self.read_only = read_only
|
||||
self.before = before
|
||||
self._initial_context = initial_context
|
||||
self._context_locked = False
|
||||
self.client: HaikuRAG | None = None
|
||||
self.config = get_config()
|
||||
self.agent: Agent[ChatDeps, str] | None = None
|
||||
|
|
@ -135,8 +138,8 @@ class ChatApp(App):
|
|||
self.action_show_info,
|
||||
)
|
||||
yield SystemCommand(
|
||||
"Session context",
|
||||
"Show current session context",
|
||||
"Memory",
|
||||
"View/edit context (editable before first message)",
|
||||
self.action_show_context,
|
||||
)
|
||||
|
||||
|
|
@ -153,6 +156,7 @@ class ChatApp(App):
|
|||
# Create agent and session state
|
||||
self.agent = create_chat_agent(self.config)
|
||||
self.session_state = ChatSessionState(
|
||||
initial_context=self._initial_context,
|
||||
document_filter=self._document_filter,
|
||||
)
|
||||
|
||||
|
|
@ -216,6 +220,9 @@ class ChatApp(App):
|
|||
if not self.client or not self.agent:
|
||||
return
|
||||
|
||||
# Lock context after first message
|
||||
self._context_locked = True
|
||||
|
||||
# Clear the input
|
||||
event.input.clear()
|
||||
|
||||
|
|
@ -297,8 +304,10 @@ class ChatApp(App):
|
|||
await chat_history.clear_messages()
|
||||
self._last_citations.clear()
|
||||
self._message_history.clear()
|
||||
# Reset session state for fresh conversation (preserve document filter)
|
||||
# Reset context lock and session state (reset to CLI value)
|
||||
self._context_locked = False
|
||||
self.session_state = ChatSessionState(
|
||||
initial_context=self._initial_context,
|
||||
document_filter=self._document_filter,
|
||||
)
|
||||
|
||||
|
|
@ -349,10 +358,17 @@ class ChatApp(App):
|
|||
await self.push_screen(InfoModal(self.client, self.db_path))
|
||||
|
||||
async def action_show_context(self) -> None:
|
||||
"""Show current session context in a modal."""
|
||||
"""Show context modal (edit initial context or view session context)."""
|
||||
from haiku.rag.chat.widgets.context_modal import ContextModal
|
||||
|
||||
await self.push_screen(ContextModal(self.session_state))
|
||||
await self.push_screen(
|
||||
ContextModal(self.session_state, is_locked=self._context_locked)
|
||||
)
|
||||
|
||||
def on_context_modal_context_updated(self, event: Any) -> None:
|
||||
"""Handle context updates from modal."""
|
||||
if self.session_state and not self._context_locked:
|
||||
self.session_state.initial_context = event.context or None
|
||||
|
||||
def on_citation_widget_selected(self, event: CitationWidget.Selected) -> None:
|
||||
"""Handle citation selection."""
|
||||
|
|
|
|||
|
|
@ -2,20 +2,25 @@ from typing import TYPE_CHECKING
|
|||
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.containers import Vertical, VerticalScroll
|
||||
from textual.containers import Horizontal, Vertical, VerticalScroll
|
||||
from textual.message import Message
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Markdown, Static
|
||||
from textual.widgets import Button, Markdown, Static, TextArea
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from haiku.rag.agents.chat.state import ChatSessionState
|
||||
|
||||
|
||||
class ContextModal(ModalScreen): # pragma: no cover
|
||||
"""Modal screen for displaying session context."""
|
||||
"""Modal screen for viewing/editing context.
|
||||
|
||||
Before first message (not locked, no session context): Edit initial context
|
||||
After first message (locked or has session context): View session context
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("escape", "dismiss", "Close", show=True),
|
||||
Binding("ctrl+o", "dismiss", "Close", show=True),
|
||||
Binding("escape", "cancel", "Close", show=False),
|
||||
Binding("ctrl+o", "cancel", "Close", show=False),
|
||||
]
|
||||
|
||||
CSS = """
|
||||
|
|
@ -25,11 +30,9 @@ class ContextModal(ModalScreen): # pragma: no cover
|
|||
}
|
||||
|
||||
#context-container {
|
||||
width: auto;
|
||||
min-width: 40;
|
||||
max-width: 80;
|
||||
width: 70;
|
||||
height: auto;
|
||||
max-height: 20;
|
||||
max-height: 32;
|
||||
background: $surface;
|
||||
border: tall $primary;
|
||||
padding: 1 2;
|
||||
|
|
@ -40,23 +43,87 @@ class ContextModal(ModalScreen): # pragma: no cover
|
|||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#context-description {
|
||||
height: auto;
|
||||
margin-bottom: 1;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
#context-editor {
|
||||
height: 12;
|
||||
min-height: 8;
|
||||
max-height: 16;
|
||||
}
|
||||
|
||||
#context-content {
|
||||
height: 1fr;
|
||||
max-height: 16;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
#button-row {
|
||||
height: auto;
|
||||
margin-top: 1;
|
||||
align: right middle;
|
||||
}
|
||||
|
||||
#button-row Button {
|
||||
margin-left: 1;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, session_state: "ChatSessionState | None"):
|
||||
class ContextUpdated(Message):
|
||||
"""Emitted when the context is saved."""
|
||||
|
||||
def __init__(self, context: str) -> None:
|
||||
super().__init__()
|
||||
self.context = context
|
||||
|
||||
def __init__(
|
||||
self, session_state: "ChatSessionState | None", is_locked: bool = False
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.session_state = session_state
|
||||
self._is_locked = is_locked
|
||||
|
||||
@property
|
||||
def _is_edit_mode(self) -> bool:
|
||||
"""Edit mode when not locked and no session context yet."""
|
||||
has_session_context = (
|
||||
self.session_state
|
||||
and self.session_state.session_context
|
||||
and self.session_state.session_context.summary
|
||||
)
|
||||
return not self._is_locked and not has_session_context
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="context-container"):
|
||||
yield Static("[bold]Session Context[/bold]", id="context-header")
|
||||
with VerticalScroll(id="context-content"):
|
||||
yield Markdown(self._get_content())
|
||||
if self._is_edit_mode:
|
||||
yield Static("[bold]Initial Context[/bold]", id="context-header")
|
||||
yield Static(
|
||||
"Set background context to guide the conversation. "
|
||||
"This will be locked after you send your first message.",
|
||||
id="context-description",
|
||||
)
|
||||
initial_value = ""
|
||||
if self.session_state and self.session_state.initial_context:
|
||||
initial_value = self.session_state.initial_context
|
||||
yield TextArea(initial_value, id="context-editor")
|
||||
with Horizontal(id="button-row"):
|
||||
yield Button("Cancel", id="cancel-btn", variant="default")
|
||||
yield Button("Save", id="save-btn", variant="primary")
|
||||
else:
|
||||
yield Static("[bold]Session Context[/bold]", id="context-header")
|
||||
yield Static(
|
||||
"What the assistant has learned from your conversation.",
|
||||
id="context-description",
|
||||
)
|
||||
with VerticalScroll(id="context-content"):
|
||||
yield Markdown(self._get_session_content())
|
||||
with Horizontal(id="button-row"):
|
||||
yield Button("Close", id="cancel-btn", variant="primary")
|
||||
|
||||
def _get_content(self) -> str:
|
||||
def _get_session_content(self) -> str:
|
||||
if not self.session_state:
|
||||
return "*No session state.*"
|
||||
|
||||
|
|
@ -72,5 +139,19 @@ class ContextModal(ModalScreen): # pragma: no cover
|
|||
|
||||
return f"**Last updated:** {updated}\n\n---\n\n{ctx.summary}"
|
||||
|
||||
async def action_dismiss(self, result=None) -> None:
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handle button presses."""
|
||||
if event.button.id == "cancel-btn":
|
||||
self.action_cancel()
|
||||
elif event.button.id == "save-btn":
|
||||
self.action_save()
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
"""Cancel and close without saving."""
|
||||
self.app.pop_screen()
|
||||
|
||||
def action_save(self) -> None:
|
||||
"""Save context and close."""
|
||||
editor = self.query_one("#context-editor", TextArea)
|
||||
self.post_message(self.ContextUpdated(editor.text))
|
||||
self.app.pop_screen()
|
||||
|
|
|
|||
|
|
@ -593,6 +593,11 @@ def chat(
|
|||
"--db",
|
||||
help="Path to the LanceDB database file",
|
||||
),
|
||||
initial_context: str | None = typer.Option(
|
||||
None,
|
||||
"--initial-context",
|
||||
help="Initial background context to provide to the conversation",
|
||||
),
|
||||
):
|
||||
"""Launch the chat TUI for conversational RAG."""
|
||||
from haiku.rag.chat import run_chat
|
||||
|
|
@ -603,6 +608,7 @@ def chat(
|
|||
db_path,
|
||||
read_only=_read_only,
|
||||
before=_before,
|
||||
initial_context=initial_context,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue