fabro/lib/crates
David Julia e4f7b8028b
feat(slack): render context_display and run link in interview messages
Slack interview messages previously showed only the hexagon node's label
(e.g. "Approve Plan") with no preview of the upstream stage's output and
no link back to the run. A reviewer who only sees the Slack message has
nothing to act on; they have to open the web UI to find the plan, the
artifacts, or any other context. That defeats the point of routing the
gate through Slack.

The data needed to fix this is already on the wire. InterviewStartedProps
carries `context_display` (populated by fabro-workflow with the previous
stage's response, e.g. plan summary + Dossier URLs), and AppState exposes
`run_web_url` for the deep link. This change wires both into
question_to_blocks so the Slack message is self-sufficient.

Outbound (blocks.rs):
- question_to_blocks gains a `run_web_url: Option<&str>` argument.
- A new header_section renders bold question text, an optional stage hint
  (`stage \`plan\``), and an "Open in Fabro" link when the URL is known.
- A new context_section renders question.context_display below the header,
  truncated to fit Slack's documented 3000-character section text limit
  with an explicit "(truncated; open the run in Fabro for the full
  context)" suffix. Empty context_display is skipped.
- A divider separates context from the action buttons.

Slack control characters:
- New escape_slack_controls applies HTML-entity escapes to `&`, `<`, `>`
  in untrusted strings (question text, stage, context_display, and the
  answered_blocks question/answer texts). This neutralises LLM-produced
  payloads like `<!here>`, `<@U…>`, or `<#C…>` so a stage's response
  cannot ping people or surface channels by accident.
- Markdown formatting (`*bold*`, `_italic_`, `` `code` ``, `~strike~`)
  is intentionally NOT escaped so legitimate formatting in plan summaries
  still renders.
- Per https://docs.slack.dev/messaging/formatting-message-text/#escaping.

Defensive length capping:
- truncate_to_limit clamps each section's final text against
  SLACK_SECTION_TEXT_LIMIT (3000 chars), including the truncation suffix
  in the budget so the result is guaranteed under the limit. Applies to
  both the header text and the context block, so a pathological question
  or LLM response cannot produce `invalid_blocks` from Slack.

Server plumbing (server.rs):
- start_optional_slack_service's event subscriber calls
  state.run_web_url(&envelope.event.run_id) per event and forwards the
  result to SlackService::handle_event, which threads it into
  question_to_blocks. Returns None (and the link is omitted) when the
  web UI is disabled or `server.web.url` is unset.

Tests (+10 in blocks.rs):
- header_includes_run_link_when_url_provided
- header_omits_link_when_url_missing
- header_shows_stage_when_present
- header_truncates_when_inputs_exceed_section_limit
- context_display_renders_between_header_and_actions
- context_display_truncates_oversized_text_to_fit_slack_budget
- empty_context_display_is_skipped
- slack_control_chars_in_question_text_are_escaped
- slack_control_chars_in_context_display_are_escaped
- answered_blocks_escape_slack_control_chars

84/84 fabro-slack tests pass (was 74 after the action_id fix in
fix/slack-action-id-uniqueness).
`cargo +nightly-2026-04-14 fmt --check --all` and `cargo
+nightly-2026-04-14 clippy -p fabro-slack -p fabro-server --all-targets
-- -D warnings` both clean.

Verified end-to-end against a real Slack workspace: a multiple_choice
"Approve Plan" gate now renders with bold header, stage hint, "Open in
Fabro" link, the upstream plan summary (Dossier canonical + version
URLs, artifact paths, plan-summary bullets), a divider, and [A]/[R]
buttons. A reviewer can act on the gate from Slack without opening the
web UI.

Stacks on fix/slack-action-id-uniqueness.
2026-05-12 22:23:25 -06:00
..
build-support fix(build): refresh embedded git sha on branch commits 2026-05-09 14:10:04 -04:00
fabro-acp fix(acp): tolerate clean stdio exit after final response 2026-05-12 09:03:53 -04:00
fabro-agent feat(model): support open provider catalog data (#245) 2026-05-12 15:42:49 -04:00
fabro-api feat(model): support open provider catalog data (#245) 2026-05-12 15:42:49 -04:00
fabro-auth feat(model): support open provider catalog data (#245) 2026-05-12 15:42:49 -04:00
fabro-checkpoint Make git metadata sandbox-native 2026-04-27 21:43:15 -07:00
fabro-cli feat(model): support open provider catalog data (#245) 2026-05-12 15:42:49 -04:00
fabro-client feat(cli): add Fabro MCP server (#236) 2026-05-11 18:20:50 -04:00
fabro-config feat(server): inject settings-backed model catalog (#247) 2026-05-12 17:55:28 -04:00
fabro-core Cancel in-flight agent stages with CancellationToken (#211) 2026-05-05 09:54:22 -04:00
fabro-dev feat(model): support open provider catalog data (#245) 2026-05-12 15:42:49 -04:00
fabro-devcontainer refactor(static): centralize env var names 2026-04-24 12:29:51 -04:00
fabro-dump feat(api): unify public run shape 2026-05-10 20:48:55 -04:00
fabro-github fix(github): refresh installation tokens during workflows 2026-05-06 07:15:18 -04:00
fabro-graphviz refactor: simplify Rust review cleanup 2026-05-02 15:41:17 -04:00
fabro-hooks fix(github): refresh installation tokens during workflows 2026-05-06 07:15:18 -04:00
fabro-http refactor(static): centralize env var names 2026-04-24 12:29:51 -04:00
fabro-install fix(server): default foreground logs to stdout 2026-05-06 12:41:52 -04:00
fabro-interview Wire end-to-end steering for running agents (#209) 2026-05-05 15:34:16 -04:00
fabro-llm feat(model): support open provider catalog data (#245) 2026-05-12 15:42:49 -04:00
fabro-macros refactor(dev): simplify generated docs tooling 2026-04-24 18:41:00 -04:00
fabro-manifest fix(validate): pick up sibling workflow.toml inputs for bare .fabro path (#242) 2026-05-12 12:05:21 -04:00
fabro-mcp feat(cli): add Fabro MCP server (#236) 2026-05-11 18:20:50 -04:00
fabro-mcp-server fix(validate): pick up sibling workflow.toml inputs for bare .fabro path (#242) 2026-05-12 12:05:21 -04:00
fabro-model feat(server): inject settings-backed model catalog (#247) 2026-05-12 17:55:28 -04:00
fabro-oauth fix(error): preserve remaining error context 2026-05-02 10:51:15 -04:00
fabro-options-metadata refactor(dev): simplify generated docs tooling 2026-04-24 18:41:00 -04:00
fabro-proc refactor(static): centralize env var names 2026-04-24 12:29:51 -04:00
fabro-redact refactor(integrations): make chat integrations Slack-only 2026-05-09 11:43:16 -04:00
fabro-sandbox fix(sandbox): resolve bash via PATH instead of /bin/bash (#241) 2026-05-12 10:31:12 -04:00
fabro-server feat(slack): render context_display and run link in interview messages 2026-05-12 22:23:25 -06:00
fabro-slack feat(slack): render context_display and run link in interview messages 2026-05-12 22:23:25 -06:00
fabro-spa feat(dev): gitignore embedded spa assets 2026-04-26 21:31:11 -04:00
fabro-static fix(server): preserve worker terminal color env 2026-05-06 15:54:00 -04:00
fabro-store Add ACP backend support (#237) 2026-05-11 23:39:43 -04:00
fabro-telemetry refactor(workflow): remove retro stage (#230) 2026-05-09 10:18:20 -04:00
fabro-template fix(validate): pick up sibling workflow.toml inputs for bare .fabro path (#242) 2026-05-12 12:05:21 -04:00
fabro-test feat(cli): add Fabro MCP server (#236) 2026-05-11 18:20:50 -04:00
fabro-tracker fix(github): refresh installation tokens during workflows 2026-05-06 07:15:18 -04:00
fabro-types feat(run): add managed branch controls (#243) 2026-05-12 12:00:30 -04:00
fabro-util refactor(error): drop String error shims and DisplayContains test traits 2026-05-01 19:35:22 -04:00
fabro-validate Add ACP backend support (#237) 2026-05-11 23:39:43 -04:00
fabro-vault refactor(api): unify secret metadata types 2026-04-29 20:26:13 -04:00
fabro-workflow feat(model): support open provider catalog data (#245) 2026-05-12 15:42:49 -04:00