feat(bots): Bot automations, a feed of every Hermes cron run - #169
Conversation
A second Official extension beside Bots (botAutomationsHidden, off by default, offered only where the Hermes CLI is installed and usable only while Bots is on). It pins an Automations row to the top of the Bots sidebar with the runs delivered today, and the row opens a native feed view: one #channel per Hermes cron job across every profile, #all (which can be hidden), groups the user makes to read several jobs together, unread counts, a bot filter, and newest-activity or manual order, all remembered in client storage (ghostex.botFeed.v1). Runs render as Markdown; runs Hermes held back as silent are left out, and runs Hermes marked failed are badged. gxserver reads cron/jobs.json and cron/output/<job>/*.md read-only through /api/listBotFeed (desktop-local only) and publishes each bot's runs today on its project row. The rules live in gx-core (packages/gx-core/src/bot_feed.rs) with tests. Help gains the Bot automations paragraph and settings row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zbDZWJZLMu96hXMsxiE6a
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (70)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| | TitlebarMode::Kanban | ||
| | TitlebarMode::Automate | ||
| | TitlebarMode::Manage | ||
| | TitlebarMode::BotFeed |
There was a problem hiding this comment.
🟡 Medium model/shell_focus_state.rs:125
When active_mode is BotFeed, default_shell_focus_for_mode returns ProjectEditorSurface(BotFeed), but valid_non_command_shell_focus_with_browser_tabs rejects that target. The feed focus is therefore not saved as previous_non_command_focus; closing the command pane restores the remembered Agents pane and routes keyboard focus to the hidden pane instead of the active feed. Update the validator to recognize BotFeed as a valid project-editor target.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/desktop/src/app/model/shell_focus_state.rs around line 125:
When `active_mode` is `BotFeed`, `default_shell_focus_for_mode` returns `ProjectEditorSurface(BotFeed)`, but `valid_non_command_shell_focus_with_browser_tabs` rejects that target. The feed focus is therefore not saved as `previous_non_command_focus`; closing the command pane restores the remembered Agents pane and routes keyboard focus to the hidden pane instead of the active feed. Update the validator to recognize `BotFeed` as a valid project-editor target.
| ); | ||
| output.insert( | ||
| "botRunsToday".to_string(), | ||
| Value::from(crate::bot_feed::published_bot_runs_today(profile)), |
There was a problem hiding this comment.
🟡 Medium presentation/session_projection.rs:103
Building any snapshot for a bot project performs the Bot automations disk scan and publishes botRunsToday even when Bot automations is disabled. On a cold PublishedBotFacts cache, published_bot_runs_today(profile) executes its loader, which enumerates and reads every current-day cron output file; gate this call on the Bot automations setting so disabled projects leave botRunsToday absent.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @server/src/presentation/session_projection.rs around line 103:
Building any snapshot for a bot project performs the Bot automations disk scan and publishes `botRunsToday` even when Bot automations is disabled. On a cold `PublishedBotFacts` cache, `published_bot_runs_today(profile)` executes its loader, which enumerates and reads every current-day cron output file; gate this call on the Bot automations setting so disabled projects leave `botRunsToday` absent.
| /// The first launch with unread counts: every run so far counts as read. True when it marked | ||
| /// any. | ||
| pub fn start_reading<J: FeedJob>(&mut self, jobs: &[J]) -> bool { | ||
| if !self.read.is_empty() { |
There was a problem hiding this comment.
🟡 Medium src/bot_feed.rs:259
start_reading leaves an initially empty feed uninitialized, so the first run delivered by a later refresh is marked read instead of appearing unread. Because the empty read map is both the valid state for “no jobs have been seen” and the initialization sentinel, track initialization separately (while treating existing non-empty maps as already initialized).
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/gx-core/src/bot_feed.rs around line 259:
`start_reading` leaves an initially empty feed uninitialized, so the first run delivered by a later refresh is marked read instead of appearing unread. Because the empty `read` map is both the valid state for “no jobs have been seen” and the initialization sentinel, track initialization separately (while treating existing non-empty maps as already initialized).
| ); | ||
| if project.bot_profile.is_some() { | ||
| bot_groups.insert(plan.group_id.as_str()); | ||
| bot_runs_today += project.bot_runs_today; |
There was a problem hiding this comment.
🟡 Medium sidebar_view/assemble.rs:74
automations_today continues to include cached bot_runs_today for Hermes projects whose profiles have been removed, so the Automations count remains nonzero indefinitely after those profiles and runs are gone. sync_bot_projects retains these project rows and the refresh pass does not clear their cached count; only add runs from currently discovered profiles or clear stale bot_runs_today during sync.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/gx-core/src/sidebar_view/assemble.rs around line 74:
`automations_today` continues to include cached `bot_runs_today` for Hermes projects whose profiles have been removed, so the Automations count remains nonzero indefinitely after those profiles and runs are gone. `sync_bot_projects` retains these project rows and the refresh pass does not clear their cached count; only add runs from currently discovered profiles or clear stale `bot_runs_today` during sync.
| /// What stays the same for this row across a reload, so the list can keep its place. | ||
| pub(crate) fn key(&self, jobs: &[BotFeedJob]) -> SharedString { | ||
| match self { | ||
| FeedRow::Day(label) => label.clone().into(), |
There was a problem hiding this comment.
🟡 Medium native_bot_feed/model.rs:114
FeedRow::key returns the rendered day label, so dates in different years such as 2020-09-27 and 2026-09-27 share the key Sunday, September 27. On reload, refresh therefore restores an anchor to the first matching day instead of the anchored date; use a year-inclusive, stable date key for FeedRow::Day.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/desktop/src/app/native_bot_feed/model.rs around line 114:
`FeedRow::key` returns the rendered day label, so dates in different years such as `2020-09-27` and `2026-09-27` share the key `Sunday, September 27`. On reload, `refresh` therefore restores an anchor to the first matching day instead of the anchored date; use a year-inclusive, stable date key for `FeedRow::Day`.
| .min_w_0() | ||
| .max_w(gpui::relative(1.0)) | ||
| // Cron output is agent-written, so only web links open. | ||
| .on_link_click(|href, _, _, _| { |
There was a problem hiding this comment.
🟡 Medium native_bot_feed/view.rs:1162
Right-clicking a link in cron output opens its URL in the external browser, so inspecting link context unexpectedly navigates to untrusted output. The handler ignores the mouse event; check event.standard_click() before calling gpui_open_external_http_url.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/desktop/src/app/native_bot_feed/view.rs around line 1162:
Right-clicking a link in cron output opens its URL in the external browser, so inspecting link context unexpectedly navigates to untrusted output. The handler ignores the mouse event; check `event.standard_click()` before calling `gpui_open_external_http_url`.
| /// and tilde are symbols Hermes keeps, and for the Latin-1, General Punctuation, CJK and fullwidth | ||
| /// blocks a model's punctuation around a marker comes from; the rest of the Unicode table is left | ||
| /// out. | ||
| fn is_edge_punctuation(character: char) -> bool { |
There was a problem hiding this comment.
🟡 Medium src/bot_feed.rs:293
The feed exposes runs such as ⸀SILENT⸀ even though Hermes suppresses them, so the feed does not match delivered Discord output. is_edge_punctuation omits Unicode punctuation outside its listed blocks, causing is_marker to return false; expand it to recognize the full Unicode P category, including U+2E00 Supplemental Punctuation.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @server/src/bot_feed.rs around line 293:
The feed exposes runs such as `⸀SILENT⸀` even though Hermes suppresses them, so the feed does not match delivered Discord output. `is_edge_punctuation` omits Unicode punctuation outside its listed blocks, causing `is_marker` to return false; expand it to recognize the full Unicode `P` category, including U+2E00 Supplemental Punctuation.
What
Bot automations: a second Official extension beside Bots that adds a Discord-style feed of every Hermes cron run. It is off by default, offered only where the Hermes CLI is installed, and its switch is disabled until Bots is on. With it off, nothing changes.
botAutomationsHidden(defaulttrue), a Built-in card under Planning and automation. A newrequiresExtensionon the Official descriptor shows the card with a disabled switch and "Turn on Bots first" while Bots is off; the desktop view gate, gx-core and gxserver apply the same "needs Bots" rule.TitlebarMode::BotFeed, app-wide, drawn by GPUI and cached like Automate), and clicking it again closes it.#channelper cron job across every profile, named after the job (never renamed, since the name is Hermes config), plus#allfor every run at once. Each run is a Markdown message with the bot, the time and the job's output, grouped under day separators with the newest at the bottom. The channel header shows the bot and the schedule. The feed is read-only and picks up new runs every 30 s while it is showing.#all). A bot filter narrows the list to one bot. Channels sort by newest activity or by hand with drag handles, and manual order moves a channel only within its section.#allcan be hidden (right-click it) and shown again (right-click the Feeds title).#alllive in client storage (botFeed, keyghostex.botFeed.v1, bounded at 256 KiB). A refused or failed save leaves a line in the support log./api/listBotFeed(desktop-local only, since runs hold whole agent reports) reads each profile'scron/jobs.jsonandcron/output/<job>/*.mdread-only. It follows Hermes' three run-file shapes, leaves out runs Hermes held back as silent by its own delivery rule, and hides a channel with no delivered run. A run Hermes marked failed ([CRON_FAILURE], a failed script status, or a crash) is badged "failed". Each bot's runs today ride its project row (botRunsToday) and refresh on the existing 60 s bot pass, which now shares one per-bot cache with the gateway dot.features.mdand a generatedbotAutomationsHiddenrow (bun run help:generateoutput included).Why
Hermes writes every cron run to disk, but the only way to read them was Discord, where they are split across channels and mixed with chat. The feed puts them beside the bots that produced them.
Scope
sven/bots): this PR's base is that branch, so the diff shows only the feed. It carries none of feat(sidebar): Bots, a sidebar mode for your Hermes agents #167's or feat(chat): Hermes chats name the bot, show its context, and switch models #163's hunks.packages/gx-core/src/bot_feed.rs, tested) and the Automations row flag and count. gxserver owns reading Hermes and the today count. The desktop view only draws and routes.Verification
On this branch, on top of
sven/bots(b14d24c):bun run typecheck(help references up to date) andbun run desktop:typecheckpass.bunx vitest run: 1020 of 1021 pass. The one failure reads.dependencies/zmx/build.zig.zon, which the build worktree did not populate.cargo test --libinserver/: 834 passed, 2 failed. Both failing tests also fail onsven/botsand touch nothing here:select_remote_code_server_never_falls_through_to_monaco_or_machine_editoranddecodes_real_codex_transcript_when_present. New tests cover channels across profiles, run parsing, every silence shape Hermes holds back (including full-width punctuation), failed runs, hidden empty channels, the settings gate and today's count.cargo testinpackages/gx-core: 19 passed (groups, moves, unread, collapse, filter, both orders, selection, an older saved value).cargo checkinapps/desktoppasses againstmain's current submodule pins.bun run web:buildpasses, so gx-core builds for wasm32.Tried by hand in the running desktop app, over several rounds:
#all, hiding and showing#all;The last round's server tidy-up was checked by running the old and new feed code against a real Hermes home: the output was byte-identical.
🤖 Generated with Claude Code
https://claude.ai/code/session_012zbDZWJZLMu96hXMsxiE6a
Note
Add native Bot Feed view showing every Hermes cron run
BotFeedtitlebar mode labeled Automations, rendered as a native desktop view that loads channels and runs from a newGET /api/listBotFeedendpoint and refreshes every 30 seconds.botRunsTodayon project presentations.botFeednamespace.botAutomationsHiddensetting (defaulttrue) plus extension prerequisites: the Bot automations extension requires the Bots extension and Hermes agent CLI, and blocked extensions show a dependency explanation in the settings card.TitlebarModeprimary ordering shifts BotFeed to position 6 and moves Extension to 7 and Agents to 8; default-profile gateway state now resolves fromHERMES_HOMEviabot_profile_homein bot_projects.rs; the browser host shows a "Not available in the browser" toast for Automations.Macroscope summarized d46002d.