Skip to content

feat(bots): Bot automations, a feed of every Hermes cron run - #169

Merged
maddada merged 1 commit into
mainfrom
sven/bot-automations
Sep 27, 2026
Merged

maddada merged 1 commit into
mainfrom
sven/bot-automations

Conversation

@banozz0

@banozz0 banozz0 commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

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.

  • Switch: botAutomationsHidden (default true), a Built-in card under Planning and automation. A new requiresExtension on 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.
  • Entry: an Automations row pinned to the top of the Bots sidebar shows the runs delivered today. Clicking it opens the feed as a view tab (TitlebarMode::BotFeed, app-wide, drawn by GPUI and cached like Automate), and clicking it again closes it.
  • Feed: one #channel per cron job across every profile, named after the job (never renamed, since the name is Hermes config), plus #all for 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.
  • Groups: + New group makes a named group, and right-clicking a job's channel moves it into one group at a time or out again. A group shows its jobs' runs merged by time, collapses, and can be renamed or deleted (its jobs go back below). The grouping is Ghostex's own and never touches Hermes or Discord.
  • Unread, filter, order: groups and channels show unread runs (only opening a group or a job's channel reads them, never #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. #all can be hidden (right-click it) and shown again (right-click the Feeds title).
  • Saved state: the sort, order, filter, groups, collapsed groups, read markers and hidden #all live in client storage (botFeed, key ghostex.botFeed.v1, bounded at 256 KiB). A refused or failed save leaves a line in the support log.
  • gxserver: /api/listBotFeed (desktop-local only, since runs hold whole agent reports) reads each profile's cron/jobs.json and cron/output/<job>/*.md read-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.
  • Help: a Bot automations paragraph in features.md and a generated botAutomationsHidden row (bun run help:generate output 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

Verification

On this branch, on top of sven/bots (b14d24c):

  • bun run typecheck (help references up to date) and bun run desktop:typecheck pass.
  • 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 --lib in server/: 834 passed, 2 failed. Both failing tests also fail on sven/bots and touch nothing here: select_remote_code_server_never_falls_through_to_monaco_or_machine_editor and decodes_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 test in packages/gx-core: 19 passed (groups, moves, unread, collapse, filter, both orders, selection, an older saved value).
  • cargo check in apps/desktop passes against main's current submodule pins.
  • bun run web:build passes, so gx-core builds for wasm32.

Tried by hand in the running desktop app, over several rounds:

  • the switch and its Bots dependency;
  • the Automations row, its today count, and opening and closing the feed;
  • channels, #all, hiding and showing #all;
  • the bot filter, both sort orders, drag, and restart persistence;
  • a new run arriving while the feed is open;
    • New group, moving jobs in and out, rename, delete, collapse, and unread counts.

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

  • Adds a BotFeed titlebar mode labeled Automations, rendered as a native desktop view that loads channels and runs from a new GET /api/listBotFeed endpoint and refreshes every 30 seconds.
  • The server collects Hermes cron runs per bot profile into feed channels, orders them by newest activity, and exposes today's run count via botRunsToday on project presentations.
  • Adds an Automations row to the native Bots sidebar with today's run count, and feed preferences (sort, filter, groups, read state) persist to client storage under the botFeed namespace.
  • Adds a botAutomationsHidden setting (default true) 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.
  • Risk: TitlebarMode primary ordering shifts BotFeed to position 6 and moves Extension to 7 and Agents to 8; default-profile gateway state now resolves from HERMES_HOME via bot_profile_home in bot_projects.rs; the browser host shows a "Not available in the browser" toast for Automations.

Macroscope summarized d46002d.

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
@coderabbitai

coderabbitai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 8816bee6-da92-46c2-a705-bcc179097ab0

📥 Commits

Reviewing files that changed from the base of the PR and between a6ac548 and d46002d.

📒 Files selected for processing (70)
  • apps/desktop/src/app/app_new.rs
  • apps/desktop/src/app/browser_pane.rs
  • apps/desktop/src/app/consts.rs
  • apps/desktop/src/app/consts/workarea.rs
  • apps/desktop/src/app/core.rs
  • apps/desktop/src/app/gx_store/sidebar_list.rs
  • apps/desktop/src/app/gx_store/sidebar_scratch_compare.rs
  • apps/desktop/src/app/gx_store/sidebar_snapshot.rs
  • apps/desktop/src/app/helpers/project/contract_and_paths.rs
  • apps/desktop/src/app/helpers/telemetry.rs
  • apps/desktop/src/app/helpers/titlebar.rs
  • apps/desktop/src/app/mod.rs
  • apps/desktop/src/app/modals.rs
  • apps/desktop/src/app/model/focus_close_targets.rs
  • apps/desktop/src/app/model/project_editor_placeholders.rs
  • apps/desktop/src/app/model/project_editor_shell.rs
  • apps/desktop/src/app/model/runtime_state.rs
  • apps/desktop/src/app/model/shell_focus_state.rs
  • apps/desktop/src/app/model/titlebar_mode.rs
  • apps/desktop/src/app/native_automate/mod.rs
  • apps/desktop/src/app/native_bot_feed/host.rs
  • apps/desktop/src/app/native_bot_feed/mod.rs
  • apps/desktop/src/app/native_bot_feed/model.rs
  • apps/desktop/src/app/native_bot_feed/storage.rs
  • apps/desktop/src/app/native_bot_feed/view.rs
  • apps/desktop/src/app/native_chat/markdown_style.rs
  • apps/desktop/src/app/native_chat/mod.rs
  • apps/desktop/src/app/native_chat/queue.rs
  • apps/desktop/src/app/native_sidebar/automations_row.rs
  • apps/desktop/src/app/native_sidebar/mod.rs
  • apps/desktop/src/app/native_sidebar/model.rs
  • apps/desktop/src/app/native_sidebar/render.rs
  • apps/desktop/src/app/render/project_editor_surface_and_workarea.rs
  • apps/desktop/src/app/render/session_chat_and_drop_feedback.rs
  • apps/desktop/src/app/render/view_picker.rs
  • apps/desktop/src/app/tab_actions.rs
  • apps/desktop/src/app/view_scopes.rs
  • apps/desktop/src/app/view_skeletons.rs
  • apps/desktop/src/app/workspace_reconcile.rs
  • apps/gpui-web/src/app/native_sidebar/automations_row.rs
  • apps/gpui-web/src/app/web_host/bot_feed.rs
  • apps/gpui-web/src/app/web_host/mod.rs
  • packages/client-storage-native/src/storage_catalog.rs
  • packages/client-storage/catalog.ts
  • packages/core-ui/settings-modal/tabs/extensions/built-in-cards.tsx
  • packages/gx-core/src/bot_feed.rs
  • packages/gx-core/src/lib.rs
  • packages/gx-core/src/sidebar_view/assemble.rs
  • packages/gx-core/src/sidebar_view/groups.rs
  • packages/gx-core/src/sidebar_view/inputs.rs
  • packages/gx-core/src/sidebar_view/model.rs
  • packages/gx-core/src/sidebar_view/view.rs
  • packages/gx-protocol/src/presentation.rs
  • packages/shared/ghostex-official-extensions.ts
  • packages/shared/ghostex-settings/defaults.ts
  • packages/shared/ghostex-settings/normalize.ts
  • packages/shared/ghostex-settings/types.ts
  • packages/shared/gxserver-protocol.ts
  • server/src/bot_feed.rs
  • server/src/bot_projects.rs
  • server/src/lib.rs
  • server/src/presentation/session_projection.rs
  • server/src/protocol.rs
  • server/src/server/background_tasks.rs
  • server/src/server/bot_sync.rs
  • server/src/server/mod.rs
  • skills/ghostex-help/references/features.md
  • skills/ghostex-help/references/settings-catalog.json
  • skills/ghostex-help/references/settings.md
  • tooling/ghostex-help/generate.ts
 _________________________________________________
< Untangling your code like a pair of headphones. >
 -------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

| TitlebarMode::Kanban
| TitlebarMode::Automate
| TitlebarMode::Manage
| TitlebarMode::BotFeed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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, _, _, _| {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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`.

Comment thread server/src/bot_feed.rs
/// 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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

@maddada
maddada changed the base branch from sven/bots to main September 27, 2026 19:17
@maddada
maddada merged commit 07e7ca2 into main Sep 27, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants