Skip to content

feat(agent): add hidden rootly agent AI chat mode with terminal UX - #74

Open
kwent wants to merge 50 commits into
masterfrom
devin/1793275858-agent-typed-errors
Open

kwent wants to merge 50 commits into
masterfrom
devin/1793275858-agent-typed-errors

Conversation

@kwent

@kwent kwent commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds the opt-in rootly agent (alias chat) AI chat mode, hidden for now, plus the terminal UX on top of it. This now includes the phase-1 work from #73 (closed as superseded), so phases 1 and 2 land together.

Core (phase 1)

  • Streams /v1/ai/chat/stream through rootly-go StreamAiChatEvents, rendering into a Bubble Tea chat with Glamour Markdown and a status spinner.
  • confirmation_required pauses for y/N and resumes with checkpoint_id + decision. An expired card is re-checked before resuming.
  • Ctrl+C stops the stream locally; pressing it twice exits. -p "q" or piped stdin runs one turn and prints to stdout.
  • Bare rootly and all existing subcommands are unchanged.

Builds on rootly-go v0.15.0 (typed stream errors from rootlyhq/rootly-go#206, session title).

Errors

  • *rootly.AiChatStreamStatusError is mapped to friendly messages for 401, OAuth scope 403 and 429. A 429 says retry in Ns, using Retry-After.
  • A 403 AI chat is not enabled for this team, or /users/me returning feature_flags["ai-chat"] == false, gives: Rootly AI chat isn't enabled for your organization. Contact your Rootly account team or Rootly support… This check runs before the first turn. If the key is missing, the CLI proceeds as before.

Layout (full screen, fixed footer)

  • Interactive TTY runs in the alt screen (tea.View.AltScreen, no mouse capture so native select/Cmd+C work; scroll with PgUp/PgDn, Ctrl+Home/End). Print mode, piped stdin and non-TTY output are unchanged.
  • Startup first wipes the screen and scrollback the way Cmd+K does (ESC[2J ESC[3J ESC[H). Turn it off with --no-clear.
  • Body = a bubbles/viewport over an in-model transcript (m.println appends a block; the wrapped text is cached per width) plus the live tail. Footer = rule, ❯ composer, rule, hint; it's always on the last rows. The / menu and the delete prompt are overlays between body and footer. /resume//sessions and /skills take over the whole screen; Esc clears the filter, then returns to the chat unchanged.
  • Scrolling: wheel (3 lines), PgUp/PgDn, Ctrl+Home/End, or Home/End when the composer is empty. At the bottom, the view follows output; when scrolled up, the hint shows ↓ new output · ctrl+end to jump. While an overlay is open, it gets the paging keys.
  • On exit (exit, /exit, double Ctrl+C, errors), Transcript() is printed to the normal screen so the chat stays in terminal scrollback, followed by Resume with: ….
  • Piped/redirected stdout (rootly agent | tee chat.log): the UI draws on stderr, and stdout gets only the plain ANSI-free transcript at exit; the terminal gets the coloured copy too. If neither stream is a TTY, it errors with a use -p hint.

Header

  • It isn't fixed: it's the first transcript block of each session, so it shows on launch, /new, /clear and /resume, and scrolls away. Each new header replays the ECG animation (draw-in, then beat, about 1.6s; static with --no-color).
  • 14×4 purple ECG pulse logo (welcome.go, #7C5CE6) with 4 lines beside it: ✦ Rootly AI vX, user · team, API host, then the session (INC-8 · title, alert, the resumed title, or New session). Below 50 columns it drops the logo.
  • The pulse line draws in left to right, then one bright beat sweeps along it (~1.6s). Submitting early settles it. It's static for --no-color or low-colour terminals.

Transcript

  • Your turn shows as a faint ❯ plus plain text, with no background band. The answer starts with ● and has no Assistant: label. One blank line separates the prompt, the answer and the turn footer.
  • The composer is one row and grows to about a third of the terminal height.
  • Finished tool tasks stay in scrollback, e.g. ✓ Searched incidents (1.2s).
  • While a turn runs, the spinner shows one playful verb for that turn (Triaging…, Paging the hamsters…). The footer reuses it: ✻ Triaged for 2s · done 10:04 AM (the session id is in /status and Resume with:).
  • On quit, the CLI prints Resume with: rootly agent --resume <uuid>.
  • Confirmations render as a readable card: fields, inferred and missing markers, a destructive tag and expiry. The composer is hidden while the card is up. Print mode shows the same card on stderr and keeps Approve? [y/N].
  • Ctrl+C prints Stopped locally. The server may still finish this turn…
  • Markdown links show as OSC-8 hyperlinks in colour TTYs only.
  • Bracketed paste (tea.PasteMsg) goes to the composer (idle or streaming) or the open picker filter, never to a y/n prompt.
  • Metric answers: the metric-chart fence from Ai::SurfaceRenderers::MetricResult (api surface) is drawn natively — pie/bar as eighth-block bars (13 · 52%), line/step via asciigraph (plotted at width (n-1)·k+1 so every point and peak is exact; step as real steps; nulls as gaps) — instead of raw JSON; the Markdown table still follows. [Add to dashboard](#ai-add-to-dashboard:…) becomes Add to dashboard: open in web → <web>/account?chat=<session id>. JSON/stream-JSON output stays raw.
  • Assistant blocks keep their Markdown source and re-render at the new width on resize (cached per block), so shrinking the terminal reflows cleanly.
  • Long answers: each finished Markdown block (a blank-line boundary outside code fences) is committed to the transcript as it streams; only the block in progress is re-rendered each frame.
  • --incident/--alert binding shows once, on the header's 4th line. /status shows it as binding INC-8: ….

Auth

  • rootly logout also clears the cached OAuth dynamic client registration (client_id/scopes), so the next login re-registers with the current scopes, ai.* included. A stale registration was getting "requested scope is invalid" from prod.

Flags

  • -r/--resume <uuid> and -c/--continue. --continue picks the latest non-archived session. History is loaded from the server session APIs, with no local storage.
  • -r <uuid> shows the session title on the header's 4th line, from the title field of GET /v1/ai/chat/sessions/:id/messages (rootlyhq/rootly#25957); /status shows it too. Empty or Untitled falls back to the short id.
  • --incident <id> / --alert <id> bind the first turn. These four flags are mutually exclusive.
  • --output-format text|json|stream-json works in print/stdin mode only.

Slash commands (/help lists them in this order)

  • /exit, /quit, or plain exit/quit.
  • /new and /clear.
  • /resume (alias /sessions): a bubbles/list picker with filtering. Load more pages through before=next_before, so it handles 1,000+ sessions.
  • /rename [title], /pin, /unpin and /archive call PATCH on the session.
  • /delete asks y/n first.
  • /copy uses OSC-52 via tea.SetClipboard.
  • /export [file] writes a Markdown transcript and never overwrites an existing file.
  • /skills [query]: a picker over GET /v1/ai_skills (25 per page with page[number]/page[size], sorted by name, Load more; the query goes to filter[search]). Rows look like ✔ /incident-retro with name · v3 · you + model. Enter only inserts /<slug> into the composer and never toggles a skill. A disabled or model-only skill says why. With AI Skills off: AI Skills isn't enabled for this organization.
  • /incident <id>, /alert <id>, /status and /help.
  • Typing / opens a filtered command menu: Tab completes, Esc closes. After the built-ins, it suggests enabled user-invocable skills, fetched with a 150ms debounce and cached per prefix.
  • Skills are plain /<slug>, the form the server's LoadSkill#user_named_skill? matches, so the text is sent unchanged. Built-ins win: a skill whose slug equals a built-in (status, new, …) is hidden and can't be typed. Any other /token is checked with filter[slug]. A matching enabled user-invocable skill is sent as a normal prompt; otherwise you get Unknown command /foo.

Existing commands (rootly incidents list, etc.) and bare rootly are unchanged.

Risk: low. agent is still a hidden command. Rollback: revert this PR.

Depends on (all merged): rootlyhq/rootly-go#206, rootlyhq/rootly#25887 (ai-chat in /users/me), rootlyhq/rootly#25957 (session title).

Link to Devin session: https://rootly.devinenterprise.com/sessions/28ad93d126254ea18116565e125bb089
Open in Devin Desktop: https://rootly.devinenterprise.com/desktop/session/28ad93d126254ea18116565e125bb089?variant=devin
Requested by: @kwent

devin-ai-integration Bot and others added 5 commits October 29, 2026 11:42
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@greptile-apps

greptile-apps Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

[Low impact] The change affects stream-error messages in the hidden agent command.

The PR appears safe to merge, with a non-blocking fix recommended for the time-dependent test.

Findings

  1. P2 Countdown test can fail randomly ▶
Fix with agent prompt
### Issue 1
internal/agent/stream_test.go:67
The HTTP-date test uses real time but expects exactly 17 seconds. `http.TimeFormat` drops fractions of a second, so crossing the next second before `describeError` runs can reduce the countdown to 16 seconds. A busy test runner can then fail even when the code is correct. Use a fixed clock, such as `testing/synctest`, for this case.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Replaces error-string parsing with AiChatStreamStatusError and adds a rounded countdown for Retry-After.

  • Agent stream errors show when to retry after rate limits.

Reviews (1) · Last reviewed commit: "feat(agent): use typed rootly-go stream ..." · Reviewed by Greptile

Comment thread internal/agent/stream_test.go Outdated
}

func TestDescribeError(t *testing.T) {
retryDate := time.Now().Add(17 * time.Second).UTC().Format(http.TimeFormat)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Countdown test can fail randomly

The HTTP-date test uses real time but expects exactly 17 seconds. http.TimeFormat drops fractions of a second, so crossing the next second before describeError runs can reduce the countdown to 16 seconds. A busy test runner can then fail even when the code is correct. Use a fixed clock, such as testing/synctest, for this case.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/agent/stream_test.go
Line: 67

Comment:
**Countdown test can fail randomly**

The HTTP-date test uses real time but expects exactly 17 seconds. `http.TimeFormat` drops fractions of a second, so crossing the next second before `describeError` runs can reduce the countdown to 16 seconds. A busy test runner can then fail even when the code is correct. Use a fixed clock, such as `testing/synctest`, for this case.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
devin-ai-integration Bot and others added 2 commits October 29, 2026 12:29
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
@devin-ai-integration devin-ai-integration Bot changed the title feat(agent): use typed rootly-go stream errors and show Retry-After feat(agent): typed stream errors, Retry-After, and terminal chat UX Oct 29, 2026
devin-ai-integration Bot and others added 4 commits October 29, 2026 13:15
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
@devin-ai-integration
devin-ai-integration Bot changed the base branch from devin/1793273673-rootly-agent to master October 29, 2026 14:25
@devin-ai-integration
devin-ai-integration Bot requested a review from a team October 29, 2026 14:25
@devin-ai-integration devin-ai-integration Bot changed the title feat(agent): typed stream errors, Retry-After, and terminal chat UX feat(agent): add hidden rootly agent AI chat mode with terminal UX Oct 29, 2026
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
@devin-ai-integration

devin-ai-integration Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Real-terminal E2E: local Rails + real LLM

I ran these tests on frozen binaries built from 2f35b87, 23f7072 and 6b7d25f. The earlier full run was on 13e7f64. The head is 21de752, which only changes long-series bucketing in fitLineSeries and has unit tests. CI is 7/7 and Devin Review is clean on it.

Metric question → native bars → resize 139→70→139

Native metric charts

  • Severity bars, real data: 2 · 50%, 1 · 25%, 1 · 25%. The table stays, no raw {"type" JSON shows, the chart lines up with the answer text, and it re-fits at 70 columns and back.
  • Trend line (32 days, four days with 1 incident): the y max reads 1.00 at 139 and at 70 columns on 6b7d25f. On 23f7072, asciigraph's resampling showed the peak as 0.90 and 0.79; fitLineSeries fixes that.
  • -p output: TTY and piped output draw the bars too, with no ai-add-to-dashboard. --output-format json keeps the raw metric-chart markdown.
🔴 23f7072 trend (max 0.90) 🟢 6b7d25f trend (max 1.00)
23f7072 6b7d25f
Bars, 139 cols Bars, 70 cols
bars 139 bars 70

Selection, scroll, paste

  • Native selection (2f35b87): drag-select copies the exact text to the clipboard. PgUp/PgDn scroll, /help no longer mentions the wheel or Option-select, and ❯ is visible.
  • Paste (6b7d25f): Ctrl+Shift+V and menu Paste insert a two-line question, which submits to a real answer. Pasting while an answer streams keeps the draft. Pasting into the /sessions filter filters, and Esc, Esc restores the chat pixel-identical.
Paste while streaming Paste into picker filter
streaming paste picker paste
Native selection held Esc restores chat
selection restored

Earlier, on 13e7f64: resize reflow, prompt/answer/footer spacing, full-screen /sessions · /resume · /skills with Esc, prompt restore after a failed skill lookup, user skill loading (Ai::SkillLoad.source == "user") and /status precedence all passed.

Not verified

  • Dashboard link: it renders as Add to dashboard: open in web with target http://localhost:3000/account?chat=<session id>, but Ctrl+click in the local Konsole 21.12.3 didn't open it.
  • ◌ drawing chart…: the streaming placeholder never showed live. It's covered by unit tests only.

All QA sessions are archived and the local Rails state is restored.

devin-ai-integration Bot and others added 5 commits October 30, 2026 08:49
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>

@devin-ai-integration devin-ai-integration Bot left a comment •

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.

✅ Devin resolved all 13 findings on 21de752

Fixed by Devin (13)

  • Line chart loses its final drop
  • Step charts omit later measurements
  • Indented code examples become charts
  • Step charts show sloped transitions
  • Bar charts omit numeric values
  • Line charts conceal missing samples
  • Skill lookup failures discard prompts
  • Chat transcript misses the terminal
  • Redirected chats lose their transcript
  • Approved activities appear as failures
  • Missing approval fields disappear without preview fields
  • Truncated chat streams report success
  • Debug logs expose chat prompts

View all findings in Devin Review

Devin Review

devin-ai-integration Bot and others added 27 commits October 30, 2026 11:50
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
…pacing

Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
…rected

Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
…aced turns, full-screen pickers

Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
… and null gaps in metric charts

Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Co-Authored-By: Quentin Rousseau <quentin@rootly.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant