Entry point: AGENTS.md points here for sidebar/chat work. Area-specific rules also live in module docstrings under plugin/chatbot/.
Goal: Move "Chat with Document" from a menu item to a sidebar panel (or right-dockable toolbar) so users can have it persistently visible on the right side of Writer.
Current state: The sidebar panel is working. "Chat with Document" appears in the WriterAgent deck in Writer's sidebar with Response area, Ask field, and Send button. The menu item can remain as fallback.
The sidebar is a chat window: assistant text is stored in session history and shown in the Response area (HTML when rich_text_control_sidebar is on). That is separate from editing the open document, which the main Writer agent does with apply_document_content (and related read/search tools).
| User intent | Main agent action |
|---|---|
| Answer, explain, discuss without changing the doc | Assistant chat reply only |
| Look up public or local file info (no doc edit) | Delegate web_research / document_research, then chat summary |
| Write, draft, replace, or translate into the document | apply_document_content (and reads as needed) |
| Research then write (e.g. web report in the doc) | Delegate research (sub-agent returns plain text in result), main agent formats HTML and apply_document_content in the same send |
| Large edit plus acknowledgment | Tool call(s) + brief chat confirmation |
Prompt text lives in plugin/framework/constants.py: blocks are ordered to match runtime assembly in get_chat_system_prompt_for_document (see Chat prompt constants below). Key pieces: SIDEBAR_VS_DOCUMENT (routing) and WRITER_CORE_DIRECTIVES (delegation, including research plain-text → format → apply_document_content), composed by get_chat_system_prompt_for_document. Generic delegate task guidance in the specialized block is one short line; per-domain detail is injected on the sub-agent at runtime (specialized_base.py). Sub-agents (web research, librarian, specialized delegate) use different completion tools (final_answer, reply_to_user, delegate task)—not this routing block.
Menu chat (non-sidebar entry) has no tool-calling; it is conversational only.
Sidebar mode dropdown (Chat, Image, Web Research, …) is session-only: the panel always wires to Chat on load. Users can switch modes during the session; the choice is not persisted to writeragent.json.
During a tool-loop send, the sidebar can show provider reasoning under [Thinking] while tools still run from native tool_calls (or content fallback parsers)—reasoning text is never parsed as a tool invocation. Reasoning is display-only for that turn: it is not written into session messages for the next API round (only content + tool_calls are). That matches common OpenAI-compat streaming behavior; provider docs often ask clients to echo reasoning back on later tool-loop turns for quality on reasoning models—a possible future change, not current behavior.
See streaming-and-threading.md §§3.3–3.4 for implementation paths and notes to revisit.
Writer main-chat system prompt blocks in constants.py are ordered to match runtime assembly in get_chat_system_prompt_for_document (persona → chat format → sidebar routing → core directives → tools → translation → usage patterns → review modes → apply_document_content HTML rules → specialized delegation → memory). The delivery is hybrid: the reference pieces (search, navigation, images) are NOT ambient — the sidebar pulls them on demand via the get_guidance tool (topics mapped in plugin/framework/agent_manual.py, the same single source the MCP serves).
Design notes for prompt authors live in this section so the source file stays scannable.
Scope: document tool only (apply_document_content). Not sidebar chat (reply_to_user / final_answer) or web-research final_answer.
- OpenAI tool schema declares
contentas typearray(plugin/writer/content.py). Each element is one heading/paragraph fragment;execute()joins with\nbefore Writer HTML import. - Long answers: block-sized strings are easier for models to emit than one giant tool argument, and JSON parsers handle quotes per element instead of one nested escape soup.
- Whether array still wins vs a single string is unsettled;
execute()already accepts a list or a string that parses as JSON array.
- JSON/tool-call escaping: inside a tool argument, literal
"in HTML must be\"in the JSON wire format. Prompt EXAMPLES show valid JSON (hence\"quotes\"in strings). - HTML entity escaping: do NOT send
<p>instead of<p>— the import path expects real tags (HTML_FRAGMENT_RULES). Entity soup is wrong for both array elements and sidebar.
- Web research returns plain text (
WEB_RESEARCH_PLAIN_TEXT_FORMAT); the main agent formats HTML forapply_document_content. - Librarian uses
reply_to_userwithCHAT_SIDEBAR_HTML_EXAMPLES(single HTML string). - Do not tell sub-agents to wrap answers in
apply_document_contentJSON arrays — that shape is for the main agent's document tool only.
Possible but non-trivial: tool JSON schema, eval harness, and years of prompt habit. Could widen schema to string | array later if single-string documents prove more reliable. Until then: array for documents, single string for sidebar — keep examples separate.
Recommend \(...\) inline delimiters only. The import path still parses $, $$, and \[...\] (html_math_segment.py) for pasted or legacy content — we just do not steer models toward display delimiters.
display_block in insert_writer_math_formula only wraps the formula in paragraph breaks; the OLE stays AS_CHARACTER, so display math is not centered and looks like inline on its own line — no visual win, extra delimiter choice confuses LLMs.
If we implement true block/centered math (e.g. paragraph anchor + alignment), revisit split inline vs display rules here and in math-tex.md.
Wire format: web research and librarian finish with a smolagents JSON tool call (final_answer / reply_to_user). The tool arguments object is JSON on the wire; the answer field value should be one HTML string like the Good example — not ["<p>…</p>"] and not <entity> markup.
Why examples look like plain quoted HTML while document rules show ["…"] arrays:
- Document path:
contentparameter is schema-typed array; join happens incontent.py. - Sidebar path: no array join — StarWriter HTML filter imports the string as one fragment.
- JSON escaping still applies at the tool-call layer (
\") but that is not HTML<escaping.
Do not copy WRITER_APPLY_DOCUMENT_HTML_RULES EXAMPLES into sidebar examples; array-shaped examples train models to wrap final_answer in a list, which the sidebar does not unwrap.
Brainstorming and writing-plan sub-agents (get_brainstorming_sub_agent_instructions, get_writing_sub_agent_instructions):
- Mode-specific instructions (
BRAINSTORMING_SUB_AGENT_INSTRUCTIONS/WRITING_SUB_AGENT_INSTRUCTIONS) WRITER_APPLY_DOCUMENT_HTML_RULES(forsave_design_spec/write_document_sectionarray content)get_chat_response_format_instructions(sidebar HTML or plain text)
See also math-tex.md (TeX/MathML import) and brainstorming-mode.md (sub-agent HTML surfaces).
-
Separate classes (following LibreOffice's Python ToolPanel example in
odk/examples/python/toolpanel/):- ChatPanelElement – implements
XUIElement; creates the panel window in getRealInterface() (called by the framework when the panel is set), not ingetWindow(). - ChatToolPanel – implements
XToolPanelandXSidebarPanel; holds the window reference and returns it fromgetWindow(), implementsgetHeightForWidth()(return acom.sun.star.ui.LayoutSizestruct). - ChatPanelFactory – implements
XUIElementFactory; createsChatPanelElementwithFrameandParentWindowfrom the factory args.
- ChatPanelElement – implements
-
Create the panel window with ContainerWindowProvider + XDL (not manual Toolkit/UnoControl):
- In
getRealInterface(), get the extension base URL viaPackageInformationProvider.getPackageLocation(EXTENSION_ID). - Use
ContainerWindowProvider.createContainerWindow(dialog_url, "", parent_window, None)with the path to your XDL (e.g.WriterAgentDialogs/ChatPanelDialog.xdl). - Critical: After
createContainerWindow()returns, callsetVisible(True)on the returned window. The sidebar framework does not make the panel content visible; without this call the panel shows only the title bar and empty white space.
- In
-
Config:
- Sidebar.xcu: Panel has
WantsAWTset totrue(explicit is best; default is true). - ChatPanelDialog.xdl: Use
dlg:withtitlebar="false"on the root window so the panel doesn’t show a dialog title bar inside the sidebar.
- Sidebar.xcu: Panel has
- Parent window type: Passing
ParentWindowdirectly tocreateContainerWindow()works; usingparent_window.getPeer()(XWindowPeer) was tried and is not required. - Visibility: Omitting
setVisible(True)causes the panel content to not appear; the panel is created but stays blank. So only the explicitsetVisible(True)was required to fix the “no sub-controls” issue.
All extension logging goes through plugin/framework/logging.py (init_logging). The single log file is writeragent_debug.log in the LibreOffice user config directory (same folder as writeragent.json, e.g. ~/.config/libreoffice/4/user/ on Linux). Use log_level in writeragent.json and standard logging.getLogger(...) calls; optional structured agent traces use agent_log() when enable_agent_log is set (same file).
| File | Role |
|---|---|
chat_panel.py |
ChatPanelFactory, ChatPanelElement, ChatToolPanel, SendButtonListener; ContainerWindowProvider + setVisible(True) |
WriterAgentDialogs/ChatPanelDialog.xdl |
Panel UI (response, query, send); withtitlebar="false" |
registry/org/openoffice/Office/UI/Sidebar.xcu |
WriterAgent deck + ChatPanel; WantsAWT true |
registry/org/openoffice/Office/UI/Factories.xcu |
ChatPanelFactory registration |
META-INF/manifest.xml |
Registers chat_panel.py, Sidebar.xcu, Factories.xcu |
| Type | Location | API | Notes |
|---|---|---|---|
| Sidebar Panel | Right sidebar (same area as Properties, Styles) | XSidebarPanel, XToolPanel, XUIElementFactory |
Integrates with existing sidebar decks. Most documentation targets Java. |
| Tool Panel | Can appear in task pane / sidebar | Same XToolPanel interface |
LibreOffice has a Python ToolPanelPoc example for Calc (not Writer). |
| Toolbar | Top or dockable | Add-on toolbar | Simpler; can be docked to the right. |
Required components:
- Sidebar.xcu – Registers deck(s) and panel(s). Merged into global Sidebar config on extension install.
- Factories.xcu – Registers the Panel Factory (implements
XUIElementFactory). Merged into global Factories config. - Panel Factory – UNO service implementing
XUIElementFactory.createUIElement(URL, arguments). - Panel Implementation – Implements
XToolPanel(required:getWindow()), optionallyXSidebarPanel(forgetHeightForWidth).
Key interfaces:
com.sun.star.ui.XUIElementFactory– Factory that creates panelscom.sun.star.ui.XToolPanel– Panel must implement;getWindow()returns the panel's XWindowcom.sun.star.ui.XSidebarPanel– Optional; providesgetHeightForWidth()for layout
ImplementationURL format:
private:resource/toolpanel/<FactoryName>/<PanelName>
Context format (when to show the panel):
ApplicationName, ContextName, visible|hidden
- Application:
WriterVariants(Writer),com.sun.star.text.TextDocument,any - Context:
Text,any,default - For Writer-only, always visible:
WriterVariants, any, visible
1. allotropia/libreoffice-sidebar-extension (Java)
- GitHub: https://github.com/allotropia/libreoffice-sidebar-extension
- Creates new "Tools" deck with "My Sidebar Panel"
- Structure:
registry/org/openoffice/Office/UI/Sidebar.xcu,Factories.xcu, Java PanelFactory, Panel classes
✅ Factory Registration Successful
ChatPanelFactory.createUIElement()was called correctly- Panel instances were created without errors
- UNO service registration worked properly
✅ Configuration Loading
Sidebar.xcuandFactories.xcuwere properly loaded- Panel appeared in the sidebar deck list
- Context-based visibility worked (panel only showed in Writer)
✅ Resolved: Panel Content Not Showing
- Symptom: Panel showed "Chat with Document" header but sub-controls were missing (empty white area).
- Root cause: The sidebar framework does not call
setVisible(True)on the window returned by the panel. The window is created and parented but left non-visible. - Fix: After
ContainerWindowProvider.createContainerWindow()returns, callsetVisible(True)on the returned window. No need to useparent.getPeer(); passingParentWindowdirectly is fine. - Pattern: Create the panel window in getRealInterface() (not in
getWindow()), using ContainerWindowProvider + XDL, so the window exists when the framework uses the panel. Then make it visible explicitly.
✅ Basic Panel Structure
- Separate
XUIElementwrapper (ChatPanelElement) andXToolPanel/XSidebarPanel(ChatToolPanel) - Factory creates the element; element creates window lazily in getRealInterface() via ContainerWindowProvider + XDL
- Proper UNO component registration with
unohelper.ImplementationHelper
🔴 Panel Content Invisible (resolved)
- Symptom: Panel title visible, content area blank.
- Resolution: Explicit
setVisible(True)on the container window (see above).
🔴 getWindow() Not Used for Display
- Finding: The sidebar does not call
getWindow()during normal panel display; it callsgetRealInterface()and uses the panel for layout (e.g.getHeightForWidth()). So any window creation that only runs ingetWindow()never runs. Creating the window ingetRealInterface()fixes this.
🔴 Configuration Issues (historical)
- Empty Titles: Initially had empty
<value></value>for deck and panel titles - Context Format:
WriterVariantsvscom.sun.star.text.TextDocumentconfusion - Deck Management: Custom deck (
WriterAgentDeck) vs existing deck (PropertyDeck) tradeoffs
graph TD
A[Factory.createUIElement] --> B[ChatPanelElement created]
B --> C[SetUIElement / getRealInterface called]
C --> D[createContainerWindow with ParentWindow]
D --> E[setVisible true on returned window]
E --> F[Panel content visible]
Critical: Create and show the window in getRealInterface(); do not rely on getWindow() being called for initial display.
- The framework parents the window but does not set it visible. Extensions must call
setVisible(True)on the panel content window.
Minimum Viable Sidebar.xcu Panel Configuration:
<node oor:name="ChatPanel" oor:op="replace">
<prop oor:name="Title" oor:type="xs:string">
<value xml:lang="en-US">Chat with Document</value> <!-- MUST have title -->
</prop>
<prop oor:name="Id" oor:type="xs:string">
<value>ChatPanel</value> <!-- Unique ID -->
</prop>
<prop oor:name="DeckId" oor:type="xs:string">
<value>PropertyDeck</value> <!-- Use existing deck for reliability -->
</prop>
<prop oor:name="ContextList">
<value oor:separator=";">com.sun.star.text.TextDocument, any, visible ;</value> <!-- Proper format -->
</prop>
<prop oor:name="ImplementationURL" oor:type="xs:string">
<value>private:resource/toolpanel/ChatPanelFactory/ChatPanel</value> <!-- Exact match -->
</prop>
<prop oor:name="OrderIndex" oor:type="xs:int">
<value>100</value> <!-- Position in deck -->
</prop>
<prop oor:name="WantsCanvas" oor:type="xs:boolean">
<value>false</value> <!-- AWT vs UNO controls -->
</prop>
</node>Use init_logging(ctx) at panel/bootstrap entry and logging.getLogger(__name__).debug(...) (or log from plugin.framework.logging). All messages land in writeragent_debug.log under the LO user config dir.
- Factory creation → Panel initialization → Property access → Window creation → Control creation
- Identified exact point of failure (panel created but never activated)
try:
# Panel creation code
self._create_panel()
except Exception as e:
log.exception("ERROR in panel creation: %s", e)
# Graceful fallback
try:
from main import MainJob
job = MainJob(self.ctx)
job.show_error(f"Panel error: {e}", "Chat Panel")
except Exception:
pass
raise # Re-throw for framework to handle| Approach | Pros | Cons | Complexity |
|---|---|---|---|
| Sidebar Panel | Native integration, persistent, professional UI | Complex lifecycle, crash-prone, Java-centric docs | ⭐⭐⭐⭐⭐ |
| Dockable Toolbar | Persistent, simpler API, no collapse issues | Limited UI space, less integrated | ⭐⭐⭐ |
| Modeless Dialog | Full control, stable, flexible UI | Not dockable, floats over document | ⭐⭐ |
| Menu + Dialog | Simple, stable, proven pattern | Not persistent, requires user action | ⭐ |
Now that the sidebar panel works, possible next steps:
- Send button wiring: Ensure the panel’s Send button is connected via
getControl()on the container window (or an event handler) so chat works from the sidebar. - Remove or reduce debug logging in
chat_panel.pyfor production if desired (or keep for diagnostics). - Optional: Dockable toolbar or modeless dialog as an alternative entry point; sidebar remains the primary UX.
- Optional: Add panel to an existing Writer deck instead of a custom deck to reduce clutter.
Working pattern: create window in getRealInterface() with ContainerWindowProvider + setVisible(True):
def getRealInterface(self):
if not self.toolpanel:
root_window = self._getOrCreatePanelRootWindow()
self.toolpanel = ChatToolPanel(root_window, self.ctx)
self._wireSendButton(root_window)
return self.toolpanel
def _getOrCreatePanelRootWindow(self):
pip = self.ctx.getValueByName(
"/singletons/com.sun.star.deployment.PackageInformationProvider")
base_url = pip.getPackageLocation(EXTENSION_ID)
dialog_url = base_url + "/" + XDL_PATH
provider = self.ctx.getServiceManager().createInstanceWithContext(
"com.sun.star.awt.ContainerWindowProvider", self.ctx)
self.m_panelRootWindow = provider.createContainerWindow(
dialog_url, "", self.xParentWindow, None)
# Required: sidebar does not show content without this
if self.m_panelRootWindow and hasattr(self.m_panelRootWindow, "setVisible"):
self.m_panelRootWindow.setVisible(True)
return self.m_panelRootWindowChatToolPanel holds the window and implements XToolPanel / XSidebarPanel; getHeightForWidth must return a com.sun.star.ui.LayoutSize struct (e.g. uno.createUnoStruct("com.sun.star.ui.LayoutSize", 280, -1, 280)).
The sidebar panel is working. The implementation:
- Registers the panel factory and creates panel instances
- Creates the panel UI in getRealInterface() via ContainerWindowProvider + XDL (so the framework sees the panel without relying on
getWindow()) - Calls setVisible(True) on the container window so the sidebar actually shows the content
Critical fix: The framework does not make the panel content visible; extensions must call setVisible(True) on the window returned by createContainerWindow().
Reference: Sidebar.xcu snippet (deck + panel):
<node oor:name="ToolsDeck" oor:op="replace">
<prop oor:name="Title" oor:type="xs:string"><value xml:lang="en-US">Tools</value></prop>
<prop oor:name="Id" oor:type="xs:string"><value>ToolsDeck</value></prop>
<prop oor:name="IconURL" oor:type="xs:string"><value>vnd.sun.star.extension://org.libreoffice.example.sidebar/images/actionOne_26.png</value></prop>
<prop oor:name="ContextList"><value oor:separator=";">WriterVariants, any, visible ;</value></prop>
</node>
<node oor:name="MySidebarPanel" oor:op="replace">
<prop oor:name="Title" oor:type="xs:string"><value xml:lang="en-US">My Sidebar Panel</value></prop>
<prop oor:name="Id" oor:type="xs:string"><value>MySidebarPanel</value></prop>
<prop oor:name="DeckId" oor:type="xs:string"><value>ToolsDeck</value></prop>
<prop oor:name="ContextList"><value oor:separator=";">WriterVariants, any, visible ;</value></prop>
<prop oor:name="ImplementationURL" oor:type="xs:string"><value>private:resource/toolpanel/MySidebarFactory/MySidebarPanel</value></prop>
<prop oor:name="OrderIndex" oor:type="xs:int"><value>100</value></prop>
</node>2. LibreOffice Python ToolPanelPoc
- Location:
api.libreoffice.org/examples/python/toolpanel/ - Files:
toolpanel.py,Factory.xcu,CalcWindowState.xcu,toolpanel.component - Targets Calc, not Writer – but shows Python UNO pattern for tool panels
- Factory creates panels in response to
private:resource/toolpanel/...URLs
3. Apache OpenOffice Sidebar for Developers
- Wiki: https://wiki.openoffice.org/wiki/Sidebar_for_Developers
- Analog Clock extension example (Java) with full deck/panel setup
- Explains ContextList, ImplementationURL, DeckId, Factories.xcu
For WriterAgent, add:
writeragent/
├── registry/
│ └── org/
│ └── openoffice/
│ └── Office/
│ └── UI/
│ ├── Sidebar.xcu # Deck + Chat panel definition
│ └── Factories.xcu # ChatPanelFactory registration
├── META-INF/
│ └── manifest.xml # Add registry entries, Python component
├── main.py # Add ChatPanelFactory, ChatPanel classes
└── assets/
└── chat-panel.png # Icon for deck/panel (optional)
manifest.xml must register:
registry/org/openoffice/Office/UI/Sidebar.xcuwith typeapplication/vnd.sun.star.configuration-dataregistry/org/openoffice/Office/UI/Factories.xcuwith typeapplication/vnd.sun.star.configuration-data- Python component for the factory (or .rdb if using separate component)
The panel factory receives arguments including:
Frame–XFrameof the applicationParentWindow–XWindowto create the panel as childSidebar–XSidebarwithrequestLayout()for resizeApplicationName,ContextName– context info
The panel must:
- Create a child window under
ParentWindow(using Toolkit) - Populate it with controls: text field for query, button "Send", text area for response
- Implement
XToolPanel.getWindow()to return that window - On "Send": get document from Frame, get full text, call API, display response
Challenge: Python UNO panel examples are rare. The Calc ToolPanelPoc may need adaptation for Writer and for the sidebar (vs task pane). Java examples are more complete.
To add "Chat with Document" to the Properties deck (existing Writer sidebar):
- Create only the panel entry in Sidebar.xcu (no new deck)
- Set
DeckIdto the Properties deck ID (e.g.PropertyDeckor similar – check LibreOffice source for exact ID) - Reduces UI clutter; less control over deck appearance
Simpler approach:
- Add a toolbar via Addons.xcu or similar
- Toolbar can be docked to the right
- Toolbar contains a "Chat" button that opens a dialog (current behavior) or a dropdown
- May not feel as integrated as a real sidebar panel
Option A – New Sidebar Deck (preferred for UX)
- Create
registry/org/openoffice/Office/UI/Sidebar.xcuwith WriterAgent deck + Chat panel - Create
Factories.xcuregisteringChatPanelFactory - Implement
ChatPanelFactory(XUIElementFactory) andChatPanel(XToolPanel) in Python - Panel UI: query field, Send button, response area (multiline, read-only)
- Wire panel to existing
get_full_document_text,stream_completionlogic in main.py - Update manifest.xml
- Remove "Chat with Document" from Addons.xcu menu (or keep as fallback)
Option B – Add to Existing Writer Deck
- Identify correct DeckId for Writer's Properties (or Styles) deck
- Create panel entry only, reference existing deck
- Same factory/panel implementation as Option A
Option C – Dockable Toolbar (fallback)
- Add toolbar definition
- Toolbar opens current dialog on click
- Less work, less integrated
| File | Action |
|---|---|
registry/org/openoffice/Office/UI/Sidebar.xcu |
Deck + panel; include WantsAWT true |
registry/org/openoffice/Office/UI/Factories.xcu |
ChatPanelFactory registration |
chat_panel.py |
ChatPanelFactory, ChatPanelElement, ChatToolPanel; ContainerWindowProvider + setVisible(True) |
WriterAgentDialogs/ChatPanelDialog.xdl |
Panel UI; dlg:withtitlebar="false" |
META-INF/manifest.xml |
Register chat_panel.py, Sidebar.xcu, Factories.xcu |
build.sh |
Include chat_panel.py, registry/ in zip |
Addons.xcu |
Optional: keep Chat with Document menu as fallback |
- Sidebar for Developers (OpenOffice Wiki)
- allotropia/libreoffice-sidebar-extension
- LibreOffice API – XToolPanel
- LibreOffice API – XUIElementFactory
- Python ToolPanel example (Calc)
- Framework/Article/Tool Panels (OpenOffice Wiki)
get_full_document_text(model, max_chars)– gets document textstream_completion(prompt, system_prompt, max_tokens, api_type, append_callback)– API callshow_error(message, title)– error display- Config keys:
chat_max_tokens,additional_instructions(document excerpt cap is internalCHAT_DOCUMENT_CONTEXT_MAX_CHARS)
The panel needs access to MainJob or equivalent to call these. Options:
- Instantiate MainJob from panel (need ctx)
- Extract shared module (e.g.
api.py,document.py) and import in panel - Pass frame/document to panel; panel gets document from frame's controller