Add read-only initial context for chat sessions for TUI and web app

This commit is contained in:
Yiorgis Gozadinos 2026-01-27 13:59:00 +02:00
parent bb634e68a4
commit 614c34b98f
No known key found for this signature in database
11 changed files with 282 additions and 51 deletions

View file

@ -1,6 +1,16 @@
# Changelog # Changelog
## [Unreleased] ## [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 ## [0.27.1] - 2026-01-27
### Added ### Added

View file

@ -83,6 +83,7 @@ async def stream_chat(request: Request) -> Response:
initial_qa_history: list[QAResponse] = [] initial_qa_history: list[QAResponse] = []
session_id: str | None = None session_id: str | None = None
document_filter: list[str] = [] document_filter: list[str] = []
initial_context: str | None = None
state = getattr(run_input, "state", None) state = getattr(run_input, "state", None)
if state: if state:
chat_state = state.get(AGUI_STATE_KEY, 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") session_id = chat_state.get("session_id")
document_filter = chat_state.get("document_filter", []) document_filter = chat_state.get("document_filter", [])
initial_context = chat_state.get("initial_context")
deps = ChatDeps( deps = ChatDeps(
client=get_client(db_path), client=get_client(db_path),
@ -99,6 +101,7 @@ async def stream_chat(request: Request) -> Response:
session_state=ChatSessionState( session_state=ChatSessionState(
qa_history=initial_qa_history, qa_history=initial_qa_history,
document_filter=document_filter, document_filter=document_filter,
initial_context=initial_context,
**({"session_id": session_id} if session_id else {}), **({"session_id": session_id} if session_id else {}),
), ),
state_key=AGUI_STATE_KEY, state_key=AGUI_STATE_KEY,

View file

@ -42,6 +42,7 @@ interface SessionContext {
interface ChatSessionState { interface ChatSessionState {
session_id: string; session_id: string;
initial_context: string | null;
citations: Citation[]; citations: Citation[];
qa_history: QAResponse[]; qa_history: QAResponse[];
session_context: SessionContext | null; session_context: SessionContext | null;
@ -398,6 +399,7 @@ function ChatContentInner() {
initialState: { initialState: {
[AGUI_STATE_KEY]: { [AGUI_STATE_KEY]: {
session_id: "", session_id: "",
initial_context: null,
citations: [], citations: [],
qa_history: [], qa_history: [],
session_context: null, 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 sessionContext = agentState?.[AGUI_STATE_KEY]?.session_context ?? null;
const documentFilter = agentState?.[AGUI_STATE_KEY]?.document_filter ?? []; 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[]) => { const handleFilterApply = (selected: string[]) => {
setAgentState({ setAgentState({
@ -417,6 +424,7 @@ function ChatContentInner() {
[AGUI_STATE_KEY]: { [AGUI_STATE_KEY]: {
...agentState?.[AGUI_STATE_KEY], ...agentState?.[AGUI_STATE_KEY],
session_id: agentState?.[AGUI_STATE_KEY]?.session_id ?? "", session_id: agentState?.[AGUI_STATE_KEY]?.session_id ?? "",
initial_context: agentState?.[AGUI_STATE_KEY]?.initial_context ?? null,
citations: agentState?.[AGUI_STATE_KEY]?.citations ?? [], citations: agentState?.[AGUI_STATE_KEY]?.citations ?? [],
qa_history: agentState?.[AGUI_STATE_KEY]?.qa_history ?? [], qa_history: agentState?.[AGUI_STATE_KEY]?.qa_history ?? [],
session_context: agentState?.[AGUI_STATE_KEY]?.session_context ?? null, 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>({ useCoAgentStateRender<AgentState>({
name: "chat_agent", name: "chat_agent",
render: ({ state }) => { render: ({ state }) => {
@ -568,12 +592,16 @@ function ChatContentInner() {
</button> </button>
<button <button
type="button" type="button"
className={`header-btn ${sessionContext?.summary ? "has-content" : ""}`} className={`header-btn ${initialContext || sessionContext?.summary ? "has-content" : ""}`}
onClick={() => setContextOpen(true)} onClick={() => setContextOpen(true)}
title={ title={
sessionContext?.summary isContextLocked
? "View session context" ? sessionContext?.summary
: "No session context yet" ? "View session context"
: "No session context yet"
: initialContext
? "Edit initial context"
: "Set initial context"
} }
> >
<BrainIcon /> <BrainIcon />
@ -596,6 +624,9 @@ function ChatContentInner() {
isOpen={contextOpen} isOpen={contextOpen}
onClose={() => setContextOpen(false)} onClose={() => setContextOpen(false)}
sessionContext={sessionContext} sessionContext={sessionContext}
initialContext={initialContext}
onInitialContextChange={handleInitialContextChange}
isLocked={isContextLocked}
/> />
<DocumentFilter <DocumentFilter
isOpen={filterOpen} isOpen={filterOpen}

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { useCallback, useId } from "react"; import { useCallback, useEffect, useId, useState } from "react";
interface SessionContext { interface SessionContext {
summary: string; summary: string;
@ -11,6 +11,9 @@ interface ContextPanelProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
sessionContext: SessionContext | null; sessionContext: SessionContext | null;
initialContext?: string;
onInitialContextChange?: (value: string) => void;
isLocked?: boolean;
} }
function formatRelativeTime(isoString: string): string { function formatRelativeTime(isoString: string): string {
@ -62,8 +65,18 @@ export default function ContextPanel({
isOpen, isOpen,
onClose, onClose,
sessionContext, sessionContext,
initialContext = "",
onInitialContextChange,
isLocked = false,
}: ContextPanelProps) { }: ContextPanelProps) {
const titleId = useId(); const titleId = useId();
const [localValue, setLocalValue] = useState(initialContext);
useEffect(() => {
if (isOpen) {
setLocalValue(initialContext);
}
}, [isOpen, initialContext]);
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => { (e: React.KeyboardEvent) => {
@ -74,11 +87,18 @@ export default function ContextPanel({
[onClose], [onClose],
); );
const handleSave = useCallback(() => {
onInitialContextChange?.(localValue);
onClose();
}, [localValue, onInitialContextChange, onClose]);
if (!isOpen) { if (!isOpen) {
return null; 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 ( return (
<> <>
@ -146,6 +166,24 @@ export default function ContextPanel({
max-height: 400px; max-height: 400px;
overflow-y: auto; 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 { .context-empty {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -175,13 +213,15 @@ export default function ContextPanel({
font-size: 0.75rem; font-size: 0.75rem;
color: #94a3b8; color: #94a3b8;
} }
.context-btn-close { .context-btn {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
transition: all 0.15s; transition: all 0.15s;
}
.context-btn-close {
background: white; background: white;
color: #475569; color: #475569;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
@ -190,6 +230,20 @@ export default function ContextPanel({
background: #f8fafc; background: #f8fafc;
border-color: #cbd5e1; 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> `}</style>
<div <div
className="context-modal-overlay" className="context-modal-overlay"
@ -210,14 +264,22 @@ export default function ContextPanel({
<BrainIcon /> <BrainIcon />
</div> </div>
<h2 id={titleId} className="context-modal-title"> <h2 id={titleId} className="context-modal-title">
Session Context {isEditMode ? "Initial Context" : "Session Context"}
</h2> </h2>
</div> </div>
<p className="context-modal-description"> <p className="context-modal-description">
This is what the assistant has learned from your conversation so {isEditMode
far. It uses this context to provide more relevant answers. ? "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> </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-content">{sessionContext.summary}</div>
) : ( ) : (
<div className="context-empty"> <div className="context-empty">
@ -235,13 +297,24 @@ export default function ContextPanel({
? `Last updated: ${formatRelativeTime(sessionContext.last_updated)}` ? `Last updated: ${formatRelativeTime(sessionContext.last_updated)}`
: ""} : ""}
</span> </span>
<button <div className="context-footer-buttons">
type="button" <button
className="context-btn-close" type="button"
onClick={onClose} className="context-btn context-btn-close"
> onClick={onClose}
Close >
</button> {isEditMode ? "Cancel" : "Close"}
</button>
{isEditMode && (
<button
type="button"
className="context-btn context-btn-save"
onClick={handleSave}
>
Save
</button>
)}
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -31,8 +31,8 @@ Press `Ctrl+P` to open the command palette:
| Command | Description | | Command | Description |
|---------|-------------| |---------|-------------|
| Memory | Edit initial context (before first message) or view session context (after) |
| Filter documents | Select documents to restrict searches | | Filter documents | Select documents to restrict searches |
| Show context | View current session context |
| Show database info | View document/chunk counts and storage info | | Show database info | View document/chunk counts and storage info |
| Visual grounding | View chunk source location in document | | Visual grounding | View chunk source location in document |
| Clear chat | Clear chat history and reset session | | 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 - Previous Q/A pairs are used as context for follow-up questions
- Citations are tracked per response and can be inspected - Citations are tracked per response and can be inspected
- Document filter restricts all searches to selected documents - 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 ## Web Application
@ -55,8 +57,7 @@ Browser-based conversational RAG with a CopilotKit frontend.
- Expandable citations with source documents, pages, and headings - Expandable citations with source documents, pages, and headings
- Visual grounding to view chunk source locations in documents - Visual grounding to view chunk source locations in documents
- Document filter to restrict searches to selected documents - Document filter to restrict searches to selected documents
- Session context that summarizes conversation history - Memory panel: set initial context before first message, view session context after
- Settings panel for background context configuration
### Quick Start ### Quick Start

View file

@ -178,10 +178,9 @@ haiku-rag chat
haiku-rag chat --db /path/to/database.lancedb haiku-rag chat --db /path/to/database.lancedb
``` ```
Provide background context for the conversation: Provide initial background context for the conversation:
```bash ```bash
haiku-rag chat --context "Focus on Python programming concepts" haiku-rag chat --initial-context "Focus on Python programming concepts"
haiku-rag chat --context-file domain-context.txt
``` ```
!!! note !!! note
@ -193,12 +192,18 @@ The chat interface provides:
- Expandable citations with source metadata - Expandable citations with source metadata
- Session memory for context-aware follow-up questions - Session memory for context-aware follow-up questions
- Visual grounding to inspect chunk source locations - 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: Flags:
- `--context`: Background context for the conversation - `--initial-context`: Initial background context for the conversation (editable until first message)
- `--context-file`: Path to a file containing background context
See [Applications](apps.md#chat-tui) for keyboard shortcuts and features. See [Applications](apps.md#chat-tui) for keyboard shortcuts and features.

View file

@ -133,8 +133,10 @@ class ChatDeps:
) )
if "citation_registry" in state_data: if "citation_registry" in state_data:
self.session_state.citation_registry = state_data["citation_registry"] self.session_state.citation_registry = state_data["citation_registry"]
# NOTE: session_context intentionally NOT updated from client if "initial_context" in state_data:
# The agent owns this via server-side cache 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 @dataclass

View file

@ -6,6 +6,7 @@ def run_chat(
db_path: Path | None = None, db_path: Path | None = None,
read_only: bool = False, read_only: bool = False,
before: datetime | None = None, before: datetime | None = None,
initial_context: str | None = None,
) -> None: ) -> None:
"""Run the chat TUI. """Run the chat TUI.
@ -13,6 +14,7 @@ def run_chat(
db_path: Path to the LanceDB database. If None, uses default from config. db_path: Path to the LanceDB database. If None, uses default from config.
read_only: Whether to open the database in read-only mode. read_only: Whether to open the database in read-only mode.
before: Query database as it existed before this datetime. before: Query database as it existed before this datetime.
initial_context: Initial background context to provide to the conversation.
""" """
try: try:
from haiku.rag.chat.app import ChatApp from haiku.rag.chat.app import ChatApp
@ -31,5 +33,6 @@ def run_chat(
db_path, db_path,
read_only=read_only, read_only=read_only,
before=before, before=before,
initial_context=initial_context,
) )
app.run() app.run()

View file

@ -88,11 +88,14 @@ class ChatApp(App):
db_path: Path, db_path: Path,
read_only: bool = False, read_only: bool = False,
before: datetime | None = None, before: datetime | None = None,
initial_context: str | None = None,
) -> None: ) -> None:
super().__init__() super().__init__()
self.db_path = db_path self.db_path = db_path
self.read_only = read_only self.read_only = read_only
self.before = before self.before = before
self._initial_context = initial_context
self._context_locked = False
self.client: HaikuRAG | None = None self.client: HaikuRAG | None = None
self.config = get_config() self.config = get_config()
self.agent: Agent[ChatDeps, str] | None = None self.agent: Agent[ChatDeps, str] | None = None
@ -135,8 +138,8 @@ class ChatApp(App):
self.action_show_info, self.action_show_info,
) )
yield SystemCommand( yield SystemCommand(
"Session context", "Memory",
"Show current session context", "View/edit context (editable before first message)",
self.action_show_context, self.action_show_context,
) )
@ -153,6 +156,7 @@ class ChatApp(App):
# Create agent and session state # Create agent and session state
self.agent = create_chat_agent(self.config) self.agent = create_chat_agent(self.config)
self.session_state = ChatSessionState( self.session_state = ChatSessionState(
initial_context=self._initial_context,
document_filter=self._document_filter, document_filter=self._document_filter,
) )
@ -216,6 +220,9 @@ class ChatApp(App):
if not self.client or not self.agent: if not self.client or not self.agent:
return return
# Lock context after first message
self._context_locked = True
# Clear the input # Clear the input
event.input.clear() event.input.clear()
@ -297,8 +304,10 @@ class ChatApp(App):
await chat_history.clear_messages() await chat_history.clear_messages()
self._last_citations.clear() self._last_citations.clear()
self._message_history.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( self.session_state = ChatSessionState(
initial_context=self._initial_context,
document_filter=self._document_filter, document_filter=self._document_filter,
) )
@ -349,10 +358,17 @@ class ChatApp(App):
await self.push_screen(InfoModal(self.client, self.db_path)) await self.push_screen(InfoModal(self.client, self.db_path))
async def action_show_context(self) -> None: 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 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: def on_citation_widget_selected(self, event: CitationWidget.Selected) -> None:
"""Handle citation selection.""" """Handle citation selection."""

View file

@ -2,20 +2,25 @@ from typing import TYPE_CHECKING
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.binding import Binding 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.screen import ModalScreen
from textual.widgets import Markdown, Static from textual.widgets import Button, Markdown, Static, TextArea
if TYPE_CHECKING: if TYPE_CHECKING:
from haiku.rag.agents.chat.state import ChatSessionState from haiku.rag.agents.chat.state import ChatSessionState
class ContextModal(ModalScreen): # pragma: no cover 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 = [ BINDINGS = [
Binding("escape", "dismiss", "Close", show=True), Binding("escape", "cancel", "Close", show=False),
Binding("ctrl+o", "dismiss", "Close", show=True), Binding("ctrl+o", "cancel", "Close", show=False),
] ]
CSS = """ CSS = """
@ -25,11 +30,9 @@ class ContextModal(ModalScreen): # pragma: no cover
} }
#context-container { #context-container {
width: auto; width: 70;
min-width: 40;
max-width: 80;
height: auto; height: auto;
max-height: 20; max-height: 32;
background: $surface; background: $surface;
border: tall $primary; border: tall $primary;
padding: 1 2; padding: 1 2;
@ -40,23 +43,87 @@ class ContextModal(ModalScreen): # pragma: no cover
margin-bottom: 1; 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 { #context-content {
height: 1fr; height: 1fr;
max-height: 16;
scrollbar-gutter: stable; 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__() super().__init__()
self.session_state = session_state 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: def compose(self) -> ComposeResult:
with Vertical(id="context-container"): with Vertical(id="context-container"):
yield Static("[bold]Session Context[/bold]", id="context-header") if self._is_edit_mode:
with VerticalScroll(id="context-content"): yield Static("[bold]Initial Context[/bold]", id="context-header")
yield Markdown(self._get_content()) 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: if not self.session_state:
return "*No 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}" 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() self.app.pop_screen()

View file

@ -593,6 +593,11 @@ def chat(
"--db", "--db",
help="Path to the LanceDB database file", 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.""" """Launch the chat TUI for conversational RAG."""
from haiku.rag.chat import run_chat from haiku.rag.chat import run_chat
@ -603,6 +608,7 @@ def chat(
db_path, db_path,
read_only=_read_only, read_only=_read_only,
before=_before, before=_before,
initial_context=initial_context,
) )