The web app8 of 12
Reports
On this page
A report is a formatted write-up of a substantial job. She produces one with a tool; the browser only shows it and saves it.
Screenshot placeholder: the Report panel open beside a narrowed stage — a coral document icon and the report's title in the header, a green "PDF" button, and the rendered report filling the body.
How one is produced
You do not make a report from the browser. There is no "make a report" button anywhere in the app. She calls the tool herself, at her discretion, for work that warrants it.
When the tool finishes it emits a report_ready event carrying the title. The browser reacts by
storing the title, opening the panel and playing a sound
(components/CompanionExperience.tsx:262-266). The panel then fetches the HTML from
GET /api/session/{id}/report (components/panels/ReportPanel.tsx:33).
The Toggle report button only exists in the dock once a report exists
(components/UIOverlay.tsx:163-167). Until then there is nothing to open.
What the panel shows
The report's own HTML, rendered in an iframe from srcDoc (ReportPanel.tsx:144-151).
The iframe's sandbox is allow-modals allow-popups allow-popups-to-escape-sandbox — and
not allow-scripts. The popup permissions are there because the report's source citations are
target="_blank" anchors and without them a click would silently do nothing. With scripting off, only
a real click can open one (ReportPanel.tsx:147-149).
Two states are kept apart on purpose (ReportPanel.tsx:25-27, 153-155):
- "Preparing your report…" — the fetch is in flight.
- "No report yet — ask me for one and it opens here." — the backend has none.
A spinner for a report that will never arrive reads as a report that is late.
Saving it as a PDF
The PDF button downloads GET /api/session/{id}/report.pdf as a blob and saves it as
kotoba-report.pdf (ReportPanel.tsx:51-68). The filename is fixed; it is not derived from the
report's title.
The PDF is rendered server-side by a headless Chromium. That is deliberate: the browser's own
print dialog drops background colours unless you tick "Background graphics", and rendering server-side
honours the report's colours with no checkbox (api/src/kotoba/core/pdf.py:1-8).
That means the PDF needs a Chromium, Chrome or Brave binary on the machine running the backend
(api/src/kotoba/core/pdf.py:21-32). Without one, the endpoint answers 503
(api/src/kotoba/server.py:888-890).
When PDF rendering is unavailable, the button does nothing visible. The panel checks the response and returns silently on a non-OK status; the "Saving…" label reverts and no message is shown (
ReportPanel.tsx:55-56, 65-68). The comment in the PDF module says the frontend falls back towindow.print()— it does not. If the button appears to do nothing, check that a Chromium-family binary is installed where the backend runs.
Where a report lives, and how long
Two copies, with very different lifetimes.
The live copy is in memory, keyed by session. GET /api/session/{id}/report reads a process-level
dictionary that nothing evicts and nothing writes to disk (api/src/kotoba/core/reports.py:1-20).
The browser mints a new session id on every page load, so after a reload the panel can no longer
find that report — and the report title is cleared when the call ends anyway
(lib/store.ts:293-296).
The durable copy is a file. The same tool saves the finished HTML into the file library under
reports/<title-slug>-<date>.html and announces it as an artefact
(api/src/kotoba/tools/action/make_report.py:306-317). That copy survives reloads, restarts and the
end of the call.
So: to reopen an old report, use the Files panel, not the Report panel. The file library's viewer renders HTML, and the row's arrow control opens it as a full page in a new tab.
