mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-23 00:41:13 +00:00
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. |
||
|---|---|---|
| .. | ||
| crates | ||
| packages/fabro-api-client | ||