Repository navigation
feat(cli): add HTML session export with an interactive viewer - #11977
Conversation
Add `goose session export --format html`, producing a single self-contained HTML file with the session embedded as base64 JSON and rendered client-side (markdown via marked, syntax highlighting via highlight.js). Includes a table-of-contents sidebar with scroll-spy, semantic tool-call rendering (shell `$` prompt, write/todo_write content, computed edit diffs), and collapsible output. The viewer is highly inspired by Pi's HTML session exporter: the overall approach (one self-contained file, base64-embedded session, client-side rendering) and the visual language of the transcript follow Pi. Directly copied from Pi: only the vendored third-party libraries (marked.min.js, highlight.min.js). The Rust module, template.html, template.css, and template.js were written for goose's own session schema — Pi's renderer targets a different, branching session model and was not reused.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8dded84e3b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const response = responsesById.get(block.id); | ||
| if (response) { | ||
| const result = response.toolResult; | ||
| status = result && result.status === 'error' ? 'error' : 'success'; |
There was a problem hiding this comment.
Treat MCP isError results as failures
When a tool returns Ok(CallToolResult::error(...)), its serialized wrapper has status: "success" and the actual failure flag is value.isError: true, so this marks the call as successful. For write, edit, and todo_write, the following suppression also hides the error output entirely, making a failed filesystem operation appear as a successful change in the exported transcript. Determine status from both the wrapper and result.value.isError before suppressing output.
Useful? React with 👍 / 👎.
| const blocks = Array.isArray(msg.content) ? msg.content : []; | ||
| const bodyParts = blocks.map(renderContentBlock).filter((h) => h); |
There was a problem hiding this comment.
Filter assistant-only blocks before rendering visible messages
When a user-visible message contains text, image, or tool-result content annotated for the Assistant audience, this renders every block in the default transcript. Goose normally applies Message::user_visible_content and filters nested tool-result content by audience, so sessions containing model-only MCP or chat-recall content will display that internal content even before the user selects “Show internal messages.” Apply the user-audience projection before mapping blocks to HTML.
Useful? React with 👍 / 👎.
| function formatTimestamp(unixSeconds) { | ||
| if (!unixSeconds) return ''; | ||
| const d = new Date(unixSeconds * 1000); | ||
| if (isNaN(d.getTime())) return ''; |
There was a problem hiding this comment.
Normalize millisecond message timestamps
For persisted sessions whose message timestamps are in milliseconds, multiplying by 1000 again produces dates tens of thousands of years in the future instead of the actual message time. SessionManager::message_timestamp_to_datetime explicitly supports both seconds and milliseconds using MILLISECOND_TIMESTAMP_THRESHOLD, so the HTML formatter needs the same normalization for older or imported sessions.
Useful? React with 👍 / 👎.
|
Thanks for contributing. Goose uses an issue-first workflow. Please open or identify the issue for this work, wait until it reaches Ready, and link it from this pull request with See From Issue to Pull Request. If no issue is linked and nobody replies here, this pull request will be closed after three days. |
put in an issue here: #11978 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eda98fc6f3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| default: | ||
| return ''; |
There was a problem hiding this comment.
Render action-required blocks in exported transcripts
When a persisted message contains actionRequired—for example, an elicitation prompt or a tool-confirmation decision—this falls through to the empty default renderer. If that is the message's only content, renderMessage drops the entire message, so the exported transcript omits an interaction that the CLI and Markdown exporter preserve. Add rendering for the action-required variants instead of silently discarding them.
Useful? React with 👍 / 👎.
| const suppress = status === 'success' && | ||
| (tool === 'write' || tool === 'edit' || tool === 'todo_write'); |
There was a problem hiding this comment.
Restrict file-tool suppression to built-in tools
When an MCP extension exposes a tool such as storage__write, toolNameParts reduces it to the leaf name write, causing this branch to suppress its successful response as though it were Goose's built-in file writer. Such an extension's result may contain important output unrelated to the submitted content, so the export becomes incomplete; gate suppression on the full unqualified built-in tool name rather than the parsed leaf.
Useful? React with 👍 / 👎.
|
Cool @salman1993! Something simple built in makes sense. To share something similar, I also built a session inspector as a side project a couple weeks back. Deployed: https://www.sessioninspector.ai/ Supports drag and drop of goose session export files. A bit more of a debugging tool in its approach. Supports Claude Code and goose, and can easily be expanded to other harnesses too with the implementation of a TS interface over the underlying data file.
|
|
Thanks — the viewer looks useful, but I don't think this should land as a CLI-only feature. The desktop already exports sessions through
That keeps format support in one shared API instead of having the CLI load a session and call the renderer directly. More broadly, the browser renderer is reimplementing Goose's message semantics from serialized JSON. At minimum, user-visible projection should happen in Rust before embedding the session, so the HTML viewer does not need to duplicate privacy/audience rules that already exist in |
|
Thanks for contributing. Goose uses an issue-first workflow. Please open or identify the issue for this work, wait until it reaches Ready, and link it from this pull request with See From Issue to Pull Request. If no issue is linked and nobody replies here, this pull request will be closed after three days. |
alexhancock
left a comment
There was a problem hiding this comment.
LGTM if you fix up the red check @salman1993!
|
@alexhancock CI check for MCP Conformance is failing but i don't think its related to this PR. Can you please re-run the failed job and see if that fixes it? The failing assertion is SSE reconnection timing. The likely cause is the late-reconnection threshold under CI load. The same failure occurred on main yesterday. This PR changes none of the MCP transport, driver, dependencies, or workflow. |
* main: fix: support hyphenated GPT 6.1 Sol effort aliases (#12689) Resolve extensions into a per-inference lease (#12161) fix: fixed unstable tests (#12679) fix(desktop): use a system Node.js in the Windows npx wrapper (#12176) fix(cli): show thinking when GOOSE_CLI_SHOW_THINKING is set to any value (#12454) fix(cost): price Databricks GLM aliases from the zhipuai catalog (#12673) fix(tom): read messages from config (#12634) fix(bedrock): preserve the service total when folding cache input tokens (#12586) fix(provider): refresh revoked Copilot tokens when listing models (#12627) feat(cli): prompt for goose mode during first-time configure (#12629) fix: refresh revoked GitHub Copilot API token once (#12616) docs: update Desktop permission mode instructions (#12626) fix(desktop): recognize ASAR when validating update target (#12599) docs: clarify self-serve LLM provider path (#12552) fix(acp): stamp the implemented protocol version in initialize (#12580) feat(cli): add HTML session export with an interactive viewer (#11977) fix: session naming/"none" reasoning effort fails for gpt-6.1-sol (#12605) fix: reflect loading state of chats (#12603) Remove Nostr session sharing from goose core (#12453)



What
Adds
goose session export --format html, producing a single self-containedHTML file with the session embedded as base64 JSON and rendered client-side
(markdown via marked, syntax highlighting via highlight.js).
Highlights:
$prompt,write/todo_writecontent,computed
editdiffs, collapsible output with a "show more" buttonCredit / prior art
The viewer is highly inspired by Pi's HTML session exporter — the overall
approach (one self-contained file, base64-embedded session, client-side
rendering) and the visual language of the transcript follow Pi.
Directly copied from Pi: only the vendored third-party libraries
(
marked.min.js,highlight.min.js). The Rust module,template.html,template.css, andtemplate.jswere written for goose's own session schema —Pi's renderer targets a different, branching session model and was not reused.
Verification
cargo test -p goose --lib session::export_htmlcargo fmt/cargo clippycleantool errors) via headless Chrome
Closes #11978