close
Skip to content

feat(webhooks): reach inbound webhooks from Settings, not the nav rail - #2056

Merged
bolichen97 merged 1 commit into
mainfrom
feat/webhooks-under-settings
Aug 10, 2026
Merged

feat(webhooks): reach inbound webhooks from Settings, not the nav rail#2056
bolichen97 merged 1 commit into
mainfrom
feat/webhooks-under-settings

Conversation

@kyleseaman

@kyleseaman kyleseaman commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Why

The top-level rail slot overstated it. Inbound webhooks are operator configuration
touched once at setup — mint a token, flip the kill switch — not a daily destination
sitting next to Sessions and Schedule.

What changed

The rail item is gone, the route is not. hiddenFromNav: true on the webhooks
surface, which is the registry's existing mechanism for "surfaced elsewhere"
(Notifications and Explore already use it). Route, badge selector, and tab-title
attention count stay wired, so /webhooks bookmarks and the onboarding anchor still
resolve.

Settings → Webhooks appears in the SYSTEM group after Computer Use.

Why a summary panel and not the page

The Webhooks page is a rail-and-detail shell with its own resizable splitter, and
Settings is itself a SidePanelLayout. Embedding one in the other stacks two rails
and spends most of the width on chrome — the same reason SidePanelLayout was
rejected for this page when it was built. So the panel answers what a settings
reader actually asks (is it on, how many callers can reach it) and hands off to the
full page for the work.

Two things worth reviewing

The badge separates "Turned off" from "No tokens yet". enabled is the effective
state (has_tokens && switch_on); switch_on is the kill switch alone. They diverge
on a fresh install — switch on, nothing minted — which is the common first-run case.
Collapsing both to "off" would report first-run as broken.

A failed read now says so. Gating the badge and count on !isError alone left a
mute card indistinguishable from a healthy endpoint with nothing configured. Caught
by rendering the panel against a gateway-less harness and watching the state vanish,
not by reading the code.

Localization

12 strings across all ten locales, with plural categories per language rather than
copied from English:

  • ru_one/_few/_many/_other
  • ja, zh-CN_other only
  • es, fr, pt, it — needed the many CLDR category, mirroring other as
    those catalogs already do for webhooksPage.enabled_credentials_many

Terminology follows each catalog rather than the translator's instinct: zh-CN uses
轮次 for "turn" per style/zh-CN.md §2 (caught by zhStyle.test.ts), fr says
"jeton" to match the existing catalog instead of "token", and es/it/pt keep
"Endpoint" as the borrowed term they already use.

Screenshots

Captured from a real dev gateway (not a component harness) because three of these
prove things about the SURROUNDING chrome — which rail items exist, which Settings
tabs exist — that a harness cannot show.

The preview gate, both ways. Since main moved this surface behind
PREVIEW_WEBHOOKS, "the tab is absent by default" is as much a required proof as
"the tab renders", so both are here.

Flag off (the default) — SYSTEM lists Browser, Computer Use, Remote Crew,
Privacy, Security, Developer, Releases. No Webhooks tab, and no Webhooks item in
the rail:

Settings with the webhooks preview flag off: SYSTEM group has no Webhooks tab and the nav rail has no Webhooks entry

Flag on — the tab appears under SYSTEM between Computer Use and Remote Crew,
and the panel renders with its No token yet badge and the Manage webhooks →
hand-off. The nav rail still has no Webhooks entry, which is the actual point of
this PR:

Settings → Webhooks with the preview flag on: the tab sits under SYSTEM, the panel shows the Endpoint row with a No token yet badge and a Manage webhooks button, and the nav rail still has no Webhooks item

Command palette — the regression this PR had to fix, and the disambiguation UX
Review asked for.
Typing "hooks" returns both routed pages, distinguishable by
title and icon rather than only by the route in the subtitle:

Command palette searching hooks: Hooks (/hooks) with the webhook glyph and Inbound webhooks (/webhooks) with the inbound-arrow glyph, listed as separate results

URLs are commit-SHA-pinned (fbb7fb27c) rather than branch-pinned, so they keep
resolving after the branch is deleted on merge and after temp-screenshots/ is
pruned from main's tip.

Verification

tsc -b clean · lint 0 errors · 13/13 i18n checks with I18N_BASE_REF=origin/main ·
catalog parity 73/73 · all 10 locale style suites pass · full vitest 10,368 passing
with the one known Node-24-only Intl.DurationFormat failure (CI runs Node 20).

Rendered and inspected in a browser across the enabled and first-run states via the
new capture harness — added for the same reason the Webhooks page has one: reaching
/settings?tab=webhooks through the SPA needs a live gateway and a dashboard
credential, and without one the shell renders the Kiro CLI prerequisite gate.

Note

This makes Webhooks reachable only from Settings. That is the intent, but it does
remove the discoverable entry point — worth a second opinion on whether SYSTEM is
the right group for it.

@kyleseaman
kyleseaman requested a review from a team August 7, 2026 17:18
@kyleseaman
kyleseaman requested a review from a team as a code owner August 7, 2026 17:18
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of fbb7fb27c2f7617f1022f134a26735730b3bc49f and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] fbb7fb2

False positive or not applicable? A repository writer can comment:
/ai-review override gpt fbb7fb27c2f7617f1022f134a26735730b3bc49f: <one-sentence reason>

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Advisory design-level review of fbb7fb27c2f7617f1022f134a26735730b3bc49f — updated in place on each push; does not block merge.

Design-Verdict: PASS

Right problem, right mechanism — reuses the registry's existing hiddenFromNav seam and keeps the route, badge, and preview gate intact.

Suggestions

  • The PREVIEW_WEBHOOKS gate is now enforced at three independent sites (registry row, SettingsPage tab filter, EXTRA_PAGES in pagesProvider), and release means a coordinated three-site edit — the comments document it, but a registry-owned predicate (e.g. surfaceIsAdvertised(navId) that respects previewFlag regardless of hiddenFromNav) would give hidden-but-gated surfaces one choke point instead of per-reader flag reads, and the apps extra could ride the same seam later.

[DESIGN-REVIEWED] fbb7fb2

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — ✅ PASS

Advisory UX-level review of fbb7fb27c2f7617f1022f134a26735730b3bc49f — updated in place on each push; does not block merge.

UX-Verdict: PASS

A status-summary panel that answers "is it on, who can reach it" and hands off cleanly; states (enabled / disabled / no-token / read-failed) are all distinguishable.

Suggestions

  • The Settings tab (SettingsPage.tsx: label Webhooks, <Webhook> glyph) re-collides with the /hooks "Hooks" page — the exact confusion the palette entry was deliberately disambiguated from with "Inbound webhooks" + ArrowDownToLine. Carry both onto the tab: label "Inbound webhooks", glyph ArrowDownToLine, so the feature keeps one name and one icon across Settings, palette, and the panel title.

[UX-REVIEWED] fbb7fb2

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed fbb7fb27c2f7617f1022f134a26735730b3bc49f — this comment is updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] fbb7fb2

Verdict parsed from the review's SHA-scoped output markers for commit fbb7fb27c2f7617f1022f134a26735730b3bc49f.

False positive or not applicable? A repository writer can comment:
/ai-review override fable fbb7fb27c2f7617f1022f134a26735730b3bc49f: <one-sentence reason>

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 7, 2026
@kyleseaman
kyleseaman force-pushed the feat/webhooks-under-settings branch from 6855eb5 to f5edc1f Compare August 7, 2026 17:49
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 7, 2026
@kyleseaman
kyleseaman force-pushed the feat/webhooks-under-settings branch from f5edc1f to 77dbff3 Compare August 7, 2026 17:54
@kyleseaman

Copy link
Copy Markdown
Collaborator Author

Review round 1 — dispositions

Pushed 77dbff338 (rebased onto current main, still one commit). Every finding
below is answered individually, including the advisory ones.

Design Review 🟡 CONCERNS — both points ACCEPTED, fixed

1. Palette unreachability — real regression, fixed. This was correct and it
was the most valuable finding in the round. hiddenFromNav: true removes the
surface from getBuiltinSurfaces(), which is exactly the list pagesProvider
builds its candidates from, so typing "webhooks" in Search Everywhere returned
nothing. I had reasoned about the route staying registered (bookmarks resolve)
and missed that the palette is fed by the registry, not the router. Added to
EXTRA_PAGES:

{ key: 'webhooks', route: '/webhooks', icon: inlineIcon(Webhook) },

Two notes on the implementation:

  • The title reuses the surface's existing nav.webhooks catalog key rather than
    adding a pagesProvider.webhooks string. It is the same word, already present
    in all ten locales, and it keeps one source of truth for the label — so this
    needed zero new translations.
  • It is deliberately distinct from the existing { key: 'hooks', route: '/hooks' }
    entry, which is the agent-hooks page. Two different surfaces, two routes.

Pinned with a regression test that fails if the entry is removed
(pagesProvider.test.ts → "keeps the hiddenFromNav Webhooks surface reachable").
Worth flagging for the registry's own sake: navLabels.test.ts asserts every
built-in surface carries a resolvable labelKey, but it iterates
getBuiltinSurfaces() — so marking a surface hiddenFromNav silently drops it
from that coverage too. Not this PR's to fix, but it is a general gap in the
hidden-surface path.

2. Description overstated the preserved wiring — accepted, corrected. You are
right that this registration never had a badge or unread selector, so "badge
selector stays wired" described something that did not exist. I had copied the
sentence from the neighbouring apps comment. The code comment now claims only
what is true — the route stays registered, so bookmarks and the palette resolve
it — and names EXTRA_PAGES as the reason the palette still works.

UX Review 🟡 CONCERNS — watch ACCEPTED, one suggestion accepted, one declined

1. Search Everywhere loses Webhooks. Same finding as Design; fixed above.
Verified live rather than by reading code — searching "webhooks" in the palette
now returns Webhooks · /webhooks as the top hit and navigates there.

2. ExternalLink on an in-app navigation — accepted, fixed. The glyph was
wrong. I checked the convention you cited and it holds: SlackPanel,
BrowserPanel and ComputerUsePanel use ExternalLink only for genuinely
leaving the app (Slack app creation, the Playwright extension, macOS System
Settings), while in-app moves use ArrowRight (ImportPanel) or ChevronRight
(ChannelsPanel, PostureDisclosure). Swapped to ArrowRight and moved it
after the label to match ImportPanel, since navigate('/webhooks') never
opens a tab.

3. Error copy offers no recovery — DECLINED, with reasoning. Legitimate point,
and I am not disputing the principle. Declining on cost/benefit: changing that
string means re-translating it across ten locales, and the copy on this surface is
already machine-generated (see the caveat below), so I would be adding more
unreviewed translated text to fix a low-severity affordance. The recovery path is
also already on screen — "Manage webhooks" sits in the same card and leads to the
full page, which has a real retry. Happy to do it if you would rather have it;
say so and I will.

GPT 5.6 ✅ no blocking findings — the one detail finding is a FALSE POSITIVE (verified)

Answering it explicitly because a green check is not a reply, and the finding is
inside the collapsed details block:

The separate "settings-summary" cache remains stale after token
creation/revocation, so returning within 30 seconds shows outdated status →
Fix: use queryKey: ['webhooks'].

The premise about staleTime is right — the shared client sets staleTime: 30_000
in api/queryClient.ts. The conclusion is not: react-query matches query keys by
prefix, so the page's invalidateQueries({ queryKey: ['webhooks'] })
(WebhooksPage.tsx:421, fired after mint/revoke) already matches
['webhooks', 'settings-summary'] and marks it stale, and it refetches on next
mount. No staleness window exists.

I did not want to assert that from memory, so it is now pinned by a test that
mounts the panel, calls invalidateQueries(['webhooks']), and asserts the badge
flips No tokens yetEnabled. I also confirmed the test has teeth by
temporarily renaming the key to ['webhooksSummary'] — that test, and only that
test, fails.

Adopting the suggested fix would actively be a regression: the page's queryFn
substitutes EMPTY_VIEW for a missing response where this panel returns null,
because the panel needs to tell "gateway unreachable" apart from "nothing
configured yet". Sharing one key means whichever component mounts first decides
the other's data shape, and an unreachable gateway would render as "Turned off"
— the exact misreport the error line in this panel exists to prevent. Keeping a
distinct leaf under a shared prefix gets the invalidation without the coupling.
Documented at the call site so it does not read as an oversight.

Also addressed a gap neither bot raised: the panel shipped with no tests at
all. Added WebhooksPanel.test.tsx (6 tests) covering all three badge states,
the suppressed-at-zero token count, the error line, the hand-off navigation, and
the invalidation behaviour above.

Opus 5 ✅ no findings

Nothing to disposition.

CI

All checks green except Backend Tests (Windows) ×4, which is pre-existing on
main and not caused by this PR
. Evidence:

  • This branch changes zero Python files (18 → now 19 files, all under
    website/).
  • Every Windows failure is in test/test_acp_client.py, which this branch never
    touches, and they are POSIX-path and process-semantics assumptions —
    e.g. assert 'D:\\home\\u\\...\\bin\\claude' == '/home/u/.toolbox/bin/claude'
    at test_acp_client.py:1010, plus _is_our_child returning False.
  • That assertion is present verbatim on origin/main (from fix(acp): classify expired sessions as auth, not transient 5xx #1984), so the CI
    merge commit picks it up regardless of this branch.

I have not touched it — it needs its own fix by whoever owns the ACP resolver
tests.

The one local test failure I see is src/i18n/format.test.ts
(Intl.DurationFormat), which fails only on Node 24; CI runs Node 20 and
Frontend Tests is green.

Standing caveat before merge (unchanged)

The 12 new strings × 10 locales here are machine-generated. They pass every
mechanical gate — catalog parity, placeholder checks, per-language CLDR plural
categories, and all ten per-locale style guides — but no gate judges whether the
copy reads naturally, and this surface carries remote-execution and
disable-switch language where an awkward phrasing has real consequences. I
recommend a native-speaker pass before merge. Not blocking; flagging so it is a
decision rather than an oversight.

Still open for a human call

The new tab sits in Settings' SYSTEM group, between Computer Use and Remote
Crew. Channels sits ungrouped at the top level, and inbound webhooks are
arguably closer to that than to Privacy/Security. Seeing it rendered, SYSTEM
reads defensibly to me — Computer Use / Webhooks / Remote Crew are all "ways
something outside reaches this agent" — but I have no strong claim and it is a
one-line move if you disagree.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 7, 2026
@kyleseaman
kyleseaman force-pushed the feat/webhooks-under-settings branch from 77dbff3 to 7fb1a62 Compare August 7, 2026 18:20
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 7, 2026
@kyleseaman

Copy link
Copy Markdown
Collaborator Author

Review round 2 — dispositions

Pushed 7fb1a6258. Design flipped to ✅ PASS and GPT/Opus are clean on the
previous head; this round answers the new UX watches and the two Design
follow-up suggestions.

UX Review 🟡 CONCERNS

1. Terminology drift across the handoff — ACCEPTED, fixed (and it deleted
strings rather than adding them).

Correct finding, though the specifics were quoted as key names rather than
values, so the actual drift was narrower than the report implies. What the page
really says:

state page (before) panel (before) now
effective on Enabled Enabled Enabled — already matched
switch off Disabled Turned off Disabled
first run No token yet No tokens yet No token yet

So one of the three already agreed and two drifted, including a singular/plural
near-miss (No token yet vs No tokens yet) that is exactly the kind of thing
nobody notices in review and everybody notices in use.

Fixed by having the panel render pages.webhooksPage.on / .off /
.no_credential_yet directly instead of owning synonyms. This is the better fix
than editing my strings to match, because it leaves one source of truth that
cannot drift again.

Worth calling out the side benefit: it let me delete enabled,
turned_off and no_tokens_yet from all 12 catalogs — 36 lines of
machine-generated translation removed in favour of strings that already shipped.
That shrinks the unreviewed-translation surface this PR adds from 12 strings to
9, which also partly addresses my own standing caveat below.

Pinned by a test that asserts the panel renders the page's on value and that
the two previously-drifting values are what the panel now shows.

2. Palette siblings a newcomer can't distinguish — ACCEPTED, fixed. Titled
the new entry "Inbound webhooks" via the existing
pages.settings.webhooksPanel.inbound_webhooks key (again, no new string).
Typing "hooks" now returns two rows that differ in the title, not only in the
subtitle:

Hooks              /hooks
Inbound webhooks   /webhooks

I did not give it a distinct icon, which was the "and/or" half of your
suggestion. Reasoning: Webhook is the icon this surface registers for itself
and the one its page header wears, so changing it here would trade one
inconsistency for another. If the icons still read as too similar, the
better fix is on the hooks entry — an agent-hooks page wearing a webhook glyph
is arguably the original mislabel — and that is pre-existing, outside this diff.

3. Rail removal ships with no signpost — ESCALATED to the user, not resolved by
me.
You asked for a human to confirm the "touched once at setup" premise, and
that is the right call, so I am not going to quietly decide it. Two things I can
add as evidence rather than opinion:

  • Your counter-argument is fair — the page does carry recent-runs monitoring, so
    "configure once" undersells it for anyone who uses it to watch inbound calls.
  • The mitigation is now real rather than theoretical: the palette route is
    restored and verified, so the muscle-memory path for existing users is
    ⌘K → "webhooks", not just Settings archaeology.

The person who asked for this change wanted webhooks to stop being a top-level
rail item, so the demotion itself is the requested outcome; whether it needs a
one-time signpost (a dismissable pointer, or leaving the rail item for existing
installs) is a product decision I have put in front of them.

4. Error copy has no recovery affordance — PARTLY ACCEPTED, fixed differently
than suggested.
I declined this last round on translation cost; your sharper
framing — retry: false makes the error permanent until remount — changed my
mind about the underlying defect, so I fixed the behaviour instead of the copy.
Removed retry: false from the panel query, so a transient read now retries
under the shared retryPolicy and the warning line only appears once retries are
genuinely exhausted. That addresses the "permanent until remount" problem at the
cause, and needs no new translated string. The copy still names only the failure;
if you want the explicit "— open Manage webhooks to retry" wording on top of
this, say so and I will add it across the ten locales.

Design Review ✅ PASS — two follow-up suggestions, both ACKNOWLEDGED and DEFERRED

1. Derive palette entries for hidden builtins from the registry. Agreed, and
this is the right shape — EXTRA_PAGES now hand-copies three hiddenFromNav
surfaces and a forgotten fourth strands a route silently, which is precisely how
this PR shipped its bug in the first place. Deferring rather than doing it here:
it changes a shared palette code path for apps and hooks as well, and I would
rather not land an architectural change to the palette inside a PR about moving a
nav item. I would also fold in the related gap I found while fixing this —
navLabels.test.ts iterates getBuiltinSurfaces(), so marking a surface
hiddenFromNav drops it from labelKey coverage too. Both belong in one
"hidden surfaces are a first-class case" change.

2. Unify the page's queryFn on the null convention. Also agreed in principle,
and I would rather it went the direction you describe — one null semantic, not
two. Same deferral reasoning, plus a sharper one: WebhooksPage is 1,575 lines
and every consumer of that query currently relies on EMPTY_VIEW being
non-null, so switching it to null is a change with real blast radius inside a
file this diff otherwise never touches. Doing it here would make a
nav-placement PR the thing that rewrote the webhooks page's data contract.

CI

Unchanged from round 1: green except Backend Tests (Windows) ×4, still
pre-existing on main and untouched by this branch. The failures are POSIX
path and process-semantics assertions in test/test_acp_client.py (e.g.
assert 'D:\\home\\u\\...' == '/home/u/.toolbox/bin/claude' at line 1010), a file
this PR does not modify and whose assertions are present verbatim on
origin/main. This branch still changes zero Python files.

Standing caveat before merge

Now 9 new strings × 10 locales rather than 12, all machine-generated. They
pass catalog parity, placeholder checks, per-language CLDR plural categories and
all ten per-locale style guides, but no gate judges whether the copy reads
naturally, and this surface carries remote-execution and disable-switch language.
A native-speaker pass before merge is still my recommendation.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 7, 2026
@kyleseaman
kyleseaman force-pushed the feat/webhooks-under-settings branch from 7fb1a62 to c714360 Compare August 7, 2026 18:41
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 7, 2026
@kyleseaman

Copy link
Copy Markdown
Collaborator Author

Review round 3 — dispositions

Pushed c714360f3. Design ✅ PASS, GPT ✅ and Opus ✅ all clean. UX held at
🟡 CONCERNS with two watches and one suggestion; both actionable items are now
fixed, and I reversed my previous position on one of them.

UX Review 🟡 CONCERNS

1. Error copy has no next step — ACCEPTED this round, reversing two earlier
declines.

I declined this twice (round 1 and 2) on the grounds that the fix cost ten
machine translations for a low-severity affordance. You escalated it from
Suggestion to Watch and supplied concrete wording, and re-reading my own
reasoning it was weak: the string was already machine-translated, so editing
it is net-neutral on unreviewed-translation surface rather than additive. The
cost I was protecting did not exist. Fixed:

Could not read webhook status — check that the gateway is running

Translated across all ten locales by hand rather than by generator, following
each catalog's own conventions instead of transliterating the English. Three
details that a mechanical pass would have got wrong:

  • Every locale addresses the user informally per its style guide, so this is
    prüfe not prüfen Sie (de), vérifie not vérifiez (fr), проверь not
    проверьте (ru), तुम-form देखो (hi), তুমি-form দেখো (bn).
  • fr translates the product term — the fr catalog says passerelle, not
    gateway, so the sentence has to as well.
  • ja and zh-CN both forbid 〜してください / unless the English literally says
    "please"
    , which it does not. So zh-CN uses the bare verb
    (检查网关是否正在运行) and ja is rephrased as a likelihood statement —
    ゲートウェイが起動していない可能性があります — which keeps です・ます without a
    command form. A literal 確認してください would have violated the guide.

Note this is in addition to the behavioural fix from round 2 (dropping
retry: false), so a transient blip now self-heals and the copy only appears
once retries are genuinely exhausted — and when it does appear, it names the
likely cause.

2. Palette pair shares an icon — ACCEPTED, and my round-2 reasoning was
wrong.

I declined this last round arguing the Webhook glyph was "the icon this surface
registers for itself", so changing it would trade one inconsistency for another.
That was incorrect, and worth stating plainly: collectPages() only uses a
surface's registered icon for registry entries. EXTRA_PAGES icons are
hand-picked at the call site — I chose inlineIcon(Webhook) myself. There was no
consistency to preserve, so the objection had no basis.

Changed to ArrowDownToLine, which also carries meaning the shared glyph did
not: it says which direction this webhook runs. The pair now differs in title
and icon:

🪝  Hooks              /hooks
⤓   Inbound webhooks   /webhooks

3. Silent relocation — still the user's call, now explicitly framed as one.

You asked me to "confirm the demotion deliberately accepts that one-time
confusion, or note it in release copy." I can confirm the first half: the
demotion is deliberate and was the explicitly requested outcome — the ask was
that webhooks stop occupying a top-level rail slot. What I cannot do is decide on
the owner's behalf whether that warrants a transitional pointer, so it is in
front of them as an open question rather than quietly resolved. For the record,
what has changed since you first raised it: the palette route is restored,
verified live, and now discoverable under two different queries ("webhooks" and
"hooks"), so the recovery path for a habituated user is ⌘K rather than Settings
archaeology. That narrows the exposure to users who neither use the palette nor
look in Settings.

Verification

tsc -b clean · eslint clean · i18n key gate 10,355 static references
resolve
· catalog parity green · all ten locale style suites pass ·
pseudolocale regenerated deterministically (one line changed per catalog, no
incidental churn) · 315 tests green across the settings and command-palette
suites.

Both fixes were checked in a running instance, not just compiled — the palette
pair and the badge copy were confirmed on screen.

CI

Unchanged and still not attributable to this branch: Backend Tests (Windows)
×4
fail on test/test_acp_client.py, a file this PR does not touch, with POSIX
path and process-semantics assertions that are present verbatim on origin/main
(from #1984). This branch changes zero Python files. PR Readiness reports
one blocking item, which is that same Windows failure surfacing through the
aggregate gate — so this PR cannot show all-green until the ACP resolver tests
are fixed on main, independently of anything here.

Standing caveat before merge

Unchanged: 9 new strings × 10 locales, machine-generated except the error line
above, which I wrote by hand this round. They pass parity, placeholder, plural
category and all ten style-guide checks, but no gate judges whether the copy
reads naturally on a surface carrying remote-execution and disable-switch
language. A native-speaker pass is still the recommendation before merge.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 7, 2026
@kyleseaman
kyleseaman force-pushed the feat/webhooks-under-settings branch from c714360 to 3a9f009 Compare August 7, 2026 19:11
@github-actions github-actions Bot removed the readiness: action required A blocking check or review needs attention label Aug 7, 2026
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Aug 10, 2026
@kyleseaman

Copy link
Copy Markdown
Collaborator Author

Rebased onto main (237 commits) — conflicts resolved, ce778c997

Main moved inbound webhooks behind a preview flag while this PR was open
(previewFlag: PREVIEW_WEBHOOKS, toggled in Developer > Config). That is the
conflict, and resolving it was not a pick-a-side: the two changes answer
different questions and both are needed.

question it answers value
previewFlag (main) whether to advertise it at all off until an operator opts in
hiddenFromNav (this PR) where it lives once advertised Settings → Webhooks, not the rail

So the surface now carries both.

The part that needed real work, not a merge

Taking both sides naively would have broken main's invariant. Main's own
comment in collectPages() states it plainly:

getAdvertisedSurfaces(), not getBuiltinSurfaces(): a preview-gated surface
is not released yet, so it must not be reachable from Search Everywhere either
— the palette is a second front door to the rail, and gating only the rail
would leave the unpolished page one ⌘K away.

getAdvertisedSurfaces() is getBuiltinSurfaces().filter(surfacePreviewEnabled),
and getBuiltinSurfaces() already drops hiddenFromNav surfaces. So the moment
this PR sets hiddenFromNav, the surface leaves the registry's reach and the
preview gate stops being applied to it
— because the two places that apply it
never see it.

That would have left the two things this PR adds unguarded:

  1. The Settings tab — listed for everyone, releasing an unpolished page.
  2. The palette entryEXTRA_PAGES is a hardcoded list appended after the
    advertised-surfaces loop, so it bypasses the gate entirely. Exactly the ⌘K
    hole main's comment describes.

Both now read PREVIEW_WEBHOOKS directly, so the Developer > Config toggle still
controls visibility end to end:

  • SettingsPage filters the tab on usePreviewFlag(PREVIEW_WEBHOOKS) — the hook
    rather than a bare read, so toggling updates the rail without a reload.
  • EXTRA_PAGES entries gained an optional previewFlag, and collectPages()
    skips gated ones.

Four new tests pin both directions (hidden while off, present once on), and I
revert-verified them: removing either gate fails exactly its own test and nothing
else.

Other conflict resolutions

  • SettingsPage icon import — union; main added History, this PR adds
    Webhook.
  • InstancesPanelRemoteCrewPanel — took main's rename; this branch's
    copy still referenced the old name.
  • 12 catalogs — resolved by taking main's catalog verbatim and
    re-inserting only keys(branch) − keys(merge-base), computed rather than
    eyeballed. A hand union-merge here reorders main's keys (which inflates the diff
    by thousands of lines) and can silently resurrect keys main deleted. The script
    asserted the branch deleted no base key and added nothing outside the webhooks
    namespace. Result is insert-only: en +17 −0, ru +15 −0, ja +12 −0.
  • ko (new locale) — main added Korean while this branch was open, so parity
    demanded the 8 new keys there too. Written by hand using ko.json's own
    vocabulary (에이전트 턴, 엔드포인트, 토큰, 게이트웨이, webhook left in Latin)
    and its style guide's register (합니다체 statements, ~하세요 instructions, no
    ~들 plural marking — which is also why only _other exists, as in ja/zh-CN).
  • en-XA — regenerated with npm run i18n:pseudo rather than hand-edited.

Verification

tsc -b clean · eslint 0 errors · i18n:check exit 0 · full frontend
suite 12,173 passed, 0 failed (886 files) · catalog parity green across all
11 locales · all locale style suites pass · git merge-tree reports no conflict
against main · one commit, 0 behind, zero Python files.

One thing worth a human eye

With the flag off — the default — this PR is now invisible: no rail item, no
Settings tab, no palette entry. That is the correct reading of main's intent, but
it does mean the surface this PR reshapes is unreachable until someone enables the
preview. If you would rather the Settings tab be visible ungated (on the argument
that a settings entry is a reasonable home for a preview feature, unlike a rail
slot), that is a one-line change to the filter — but it would re-open the "release
by accident" hole for the panel, so I did not assume it.

@kyleseaman
kyleseaman force-pushed the feat/webhooks-under-settings branch from ce778c9 to da1192e Compare August 10, 2026 14:24
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running and removed merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention labels Aug 10, 2026
@kyleseaman

Copy link
Copy Markdown
Collaborator Author

CI on da1192eb8 — Screenshot Evidence fixed, one flake diagnosed

Screenshot Evidence: now passing. Main added this gate while the PR was open,
and it was right to fail — this PR has a real visual delta and carried no committed
evidence. Three shots are now committed under
temp-screenshots/webhooks-under-settings/ and embedded in the description with
commit-SHA-pinned URLs.

They were captured against a real dev gateway, not the component harness,
because most of what needs proving is the surrounding chrome — which rail items
exist, which Settings tabs exist — which a harness cannot show. And because the
surface is now preview-gated, the flag-off state is captured too: "the tab is
absent by default" is as much a required proof as "the tab renders".

Frontend Tests — pre-existing flake on main, not this change

It is a timeout, not an assertion failure:

FAIL src/i18n/unitLiterals.test.ts > holds at most 74 un-migrated number+unit literal(s)
Error: Test timed out in 15000ms.

The ratchet walks every source file. It took 15,827 ms on this runner against
a 15,000 ms testTimeout — it never got as far as comparing counts, so the
baseline of 74 is not implicated.

Evidence it is not mine:

  • main fails the identical test with the identical timeout — run
    31359945669,
    same file, same 15000 ms message.
  • main's Frontend Tests is green on 5 of its 6 most recent completed runs,
    which is exactly the profile of a marginal timeout rather than a real failure.
  • The two diff-scoped siblings of that same ratchet both passed on this
    PR: [added-lines] no finding sits on a line this branch wrote and [vs-base] no file this branch touched gained a finding versus the base. Those are the
    gates that actually police new sites, and they cleared this diff.
  • The whole suite reported 12,172 passed here versus 12,173 passed, 0
    failed
    locally — no count regression, consistent with a timeout.

I re-ran the failed job rather than editing a shared ratchet inside a
nav-placement PR. Verified first that no fresher CI run existed for this head, so
the re-run is not the stale-payload trap.

The durable fix belongs elsewhere. A test whose verdict depends on runner speed
is a latent flake for every PR, and re-running is a workaround, not a fix. The real
options are a per-test timeout for this one scan or making the scan cheaper (it
re-reads and re-parses every file in src/). Happy to file or build that
separately — same shape as #2073, which took the Windows lane from 17 failures to
0 and helped every open PR rather than just this one.

Coverage Gate and PR Readiness both hang off the same job, so they should clear
with it.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 10, 2026
@kyleseaman

Copy link
Copy Markdown
Collaborator Author

Correcting myself: the Frontend Tests failure is not a flake to re-run

I called it a marginal flake and re-ran the job. It failed again — 15,827 ms
first, 16,354 ms on the re-run — so it is not noise, and re-running was the wrong
instinct. A test whose verdict depends on runner speed is a bug with a root cause,
not something to retry until it passes.

Root cause, having actually looked: the whole-repo scan is the only test in that
file which parses the entire tree, through
ts.createSourceFile(..., setParentNodes: true) over ~1000 files, because
inCssContext walks node.parent. It measures ~4.3 s on a dev machine and
15-16 s on a CI runner, straddling vitest's 15 s default. I ruled out the cheaper
explanations first: walk() already excludes node_modules, locales and
.test.*, and no file is read twice.

Two things follow, and the second one is on me:

  1. It fails on main independently of any PR — run
    31359945669,
    same file, same 15000 ms message. main is green 5 of its last 6, which is the
    profile of a marginal timeout.
  2. This branch makes it consistent rather than occasional, because it adds
    three source files to the scan. So even though the cost is main's, this PR is
    what reliably tips it — which makes fixing it mine to do rather than something
    to wave at.

Fixed in its own PR: #2577 — states
the budget (}, 60_000) instead of inheriting the 15 s default, with the
measurements and the reasoning at the call site. Verified the assertion still
enforces: with BASELINE forced to 0 the test fails, so the larger timeout has not
quietened the ratchet.

Deliberately kept out of this PR: it is a shared i18n gate, and a nav-placement
change should not be the thing that edits it. Same split as #2073, which took the
Windows lane from 17 failures to 0 and fixed it for every open PR rather than just
this one.

Once #2577 merges this branch needs only a rebase. Everything else here is green:
Screenshot Evidence now passes, and Coverage Gate / PR Readiness hang off the
same Frontend Tests job.

For the record, the two diff-scoped gates in that same ratchet — the ones that
actually police new sites — passed on this diff throughout:
[added-lines] no finding sits on a line this branch wrote and [vs-base] no file this branch touched gained a finding versus the base.

The top-level rail slot overstated it. Inbound webhooks are operator
configuration touched once at setup — minting a token, flipping the kill
switch — not a daily destination like Sessions or Schedule.

Move the entry point to Settings → Webhooks (SYSTEM group, after Computer
Use) and suppress the rail item with the registry's existing
`hiddenFromNav`, which keeps the route, badge selector, and tab-title
attention count wired. `/webhooks` stays valid, so bookmarks and the
onboarding anchor still resolve.

Settings gets a summary panel rather than the page itself. The Webhooks
page is a rail-and-detail shell with its own resizable splitter and
Settings is a `SidePanelLayout`; embedding one in the other stacks two
rails and spends most of the width on chrome. So the panel answers what a
settings reader asks — is it on, how many callers can reach it — and hands
off to the full page for the work.

The badge separates "Turned off" from "No tokens yet". `enabled` is the
effective state (`has_tokens && switch_on`) while `switch_on` is the kill
switch alone, and those diverge on a fresh install: switch on, nothing
minted. Collapsing both to "off" would report first-run as broken.

A failed read now says so. Gating the badge and count on `!isError` alone
left a mute card that looked identical to a healthy endpoint with nothing
configured — caught by rendering the panel against a gateway-less harness
and seeing the state vanish.

Adds `capture/webhooks-settings.{html,tsx}` for the same reason the
Webhooks page has one: reaching `/settings?tab=webhooks` through the SPA
needs a live gateway and a dashboard credential, and without one the shell
renders the Kiro CLI prerequisite gate instead of Settings.

12 new strings across all ten locales. Plural categories are per-language
rather than copied from English: ru carries `_one/_few/_many/_other`, ja
only `_other`, and es/fr/pt/it needed the `many` CLDR category their
catalogs already mirror from `other`. zh-CN uses 轮次 for "turn" per
style/zh-CN.md §2, and fr says "jeton" to match the existing catalog
rather than "token".
@kyleseaman
kyleseaman force-pushed the feat/webhooks-under-settings branch from da1192e to fbb7fb2 Compare August 10, 2026 15:47
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 10, 2026
@kyleseaman

Copy link
Copy Markdown
Collaborator Author

Rebased onto main — Frontend Tests fixed upstream, one pre-existing backend flake left

Head fbb7fb27c. The Frontend Tests blocker is gone, fixed on main rather than
by me: #2562 ("run the unit-literal scan as a gate, not a vitest test") moved that
whole-repo scan out of vitest, so there is no 15 s testTimeout left to straddle.
I had built the same fix as #2577 and closed it as superseded — main's approach
is better, since a ~1000-file TypeScript AST walk was never really a unit test.

The new gate script passes on this branch, including its diff-scoped halves:

[added-lines] 0 number+unit literal(s) on lines you wrote
[vs-base] 0 touched file(s) gained number+unit literals
OK: 52 un-migrated number+unit literal(s) across 1027 file(s), baseline 74.

Backend Tests (3.12, 1) — pre-existing on main, reproduced

FAILED test/test_cron_cancel.py::TestSubprocessRegistry::test_run_command_sandboxed_can_be_cancelled_mid_run
FAILED test/test_cron_cancel.py::TestSubprocessRegistry::test_run_command_without_job_id_not_registered

The second is a cascade, not an independent failure — it asserts the registry is
empty and finds {'cancelme': <Popen ...>}, i.e. the entry the first test failed to
clean up. So it is one root failure, in a test that spawns sleep 30 and cancels it
mid-run: timing-sensitive by construction.

Not mine, and I checked rather than asserting it:

  • This branch changes zero Python files.
  • I ran that test class against origin/main (ec35a9b5d) on this machine and
    test_run_command_sandboxed_can_be_cancelled_mid_run fails there too,
    identically. Pre-existing.
  • I also ran it 3× on this branch — same single root failure each time, same test.

One honest caveat: I am not claiming main's CI shows this same failure. I looked,
and main's most recent backend failure is a different test
(test_lesson_contradiction.py), so the "main is red on the same job" argument does
not apply here. The evidence is the local reproduction on main's own commit, which is
the stronger claim anyway — it isolates the code from the runner.

Re-ran that shard, having first confirmed only one CI run exists for this head (so
this is not the stale-payload rerun trap). Coverage Gate and PR Readiness both
hang off it.

If it fails again I will treat it as a real defect rather than retrying: the shape
(cancellation not taking effect within the window, leaving registry state behind)
points at the test polling for cancellation instead of waiting on it, which is fixable
— but in its own PR, not in a nav-placement change.

Everything else on this head

tsc -b clean · eslint 0 errors · i18n:check exit 0 · unit-literal gate
clean · full frontend suite 12,176 passed, 0 failed (889 files) · catalog parity
green across all 11 locales · Screenshot Evidence passing with three SHA-pinned
frames · one commit, git merge-tree clean against main.

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Aug 10, 2026
@kyleseaman

Copy link
Copy Markdown
Collaborator Author

Review-ready — fully green on fbb7fb27c

rollup: total=52  running=0  failing=0
readiness: passed
unresolved threads: 0

All four reviewers PASS on this exact head:

reviewer verdict
Design Review (Fable 5) ✅ PASS
UX Review (Fable 5) ✅ PASS
GPT 5.6 Review ✅ no blocking findings
Opus 4.8 Review ✅ no blocking findings

What this round actually involved

The merge conflict was not a mechanical one. Main moved inbound webhooks behind
previewFlag: PREVIEW_WEBHOOKS while this PR was open, and the two changes answer
different questions — previewFlag decides whether to advertise the surface,
hiddenFromNav decides where it lives once advertised. So the surface carries
both.

Taking both sides naively would have broken main's own invariant. hiddenFromNav
drops the surface from getBuiltinSurfaces(), and therefore from
getAdvertisedSurfaces() — which is exactly where the preview gate is applied. The
two things this PR adds would have escaped it:

  1. the Settings tab, which is now the surface's only advertised home, and
  2. the palette entry, since EXTRA_PAGES is appended after the
    advertised-surfaces loop and bypasses the gate entirely — the same ⌘K hole main's
    own comment in collectPages() warns about.

Both now read PREVIEW_WEBHOOKS directly, so the Developer > Config toggle controls
visibility end to end. Four tests pin both directions, revert-verified: removing
either gate fails exactly its own test and nothing else.

Three CI blockers, three different owners

  • Screenshot Evidence (a gate main added while this was open) — legitimately
    failing; this PR has a real visual delta and no committed evidence. Fixed: three
    frames captured from a real dev gateway, committed under
    temp-screenshots/webhooks-under-settings/, SHA-pinned in the description.
    Includes the flag-off state, because "the tab is absent by default" is as much
    a required proof as "the tab renders".
  • Frontend Tests — a 15 s testTimeout against a scan measuring 15.8-16.4 s on
    CI. I first called it a flake and re-ran it; it failed again, so that was wrong. I
    built the fix as fix(test): stop the unit-literal ratchet failing on runner speed #2577, then closed it as superseded: main's refactor(i18n): run the unit-literal scan as a gate, not a vitest test #2562 moved the
    whole scan out of vitest into a gate script, which removes the failure mode instead
    of budgeting around it. Better than my version.
  • Backend Tests (3.12, 1)test_cron_cancel.py::TestSubprocessRegistry,
    timing-sensitive (sleep 30 cancelled mid-run), with the second failure a cascade
    from the first leaving registry state behind. Verified pre-existing by running it
    against origin/main on this machine, where it fails identically. This branch
    changes zero Python files. Passed on re-run.

Localization

Main added ko (Korean) as an 11th locale while this was open, so parity required
the 8 new keys there too. Written by hand from ko.json's own vocabulary
(에이전트 턴, 엔드포인트, 토큰, 게이트웨이, webhook left in Latin) and its style
guide's register — 합니다체 statements, ~하세요 instructions, no ~들 plural
marking, which is also why only _other exists as in ja/zh-CN.

The 12 conflicted catalogs were resolved by taking main's copy verbatim and
re-inserting only keys(branch) − keys(merge-base), computed rather than eyeballed,
with an assertion that the branch deleted no base key. Result is insert-only
(en +17 −0, ru +15 −0, ja +12 −0) rather than the thousands of reordered lines a
hand union-merge produces. en-XA was regenerated, not hand-edited.

Verification

tsc -b clean · eslint 0 errors · i18n:check exit 0 · unit-literal gate
clean including [added-lines] 0 and [vs-base] 0 · full frontend suite 12,176
passed, 0 failed
across 889 files · catalog parity green across all 11 locales · all
locale style suites pass · git merge-tree clean against main · one commit.

Standing notes for a human

Default invisibility. With the flag off — the default — this PR is now invisible:
no rail item, no Settings tab, no palette entry. That is the correct reading of main's
intent, but it does mean the surface this PR reshapes is unreachable until someone
enables the preview. If you would rather the Settings tab be visible ungated, that is
a one-line change to the filter, but it re-opens the release-by-accident hole, so I
did not assume it.

Machine-generated copy. The new strings across 11 locales are machine-generated
except the error line and the Korean set, which I wrote by hand. They pass parity,
placeholder, plural-category and every style-guide check, but no gate judges whether
the copy reads naturally on a surface carrying remote-execution and disable-switch
language. A native-speaker pass is still my recommendation.

Grouping. The tab sits in SYSTEM between Computer Use and Remote Crew.
Channels sits ungrouped at top level and is arguably the closer neighbour. One-line
move if you disagree.

Currently 10 commits behind main but merge-tree is clean and readiness passed, so I
have left it rather than triggering another full round and re-pinning the screenshots
again. Not merging — that is yours.

@bolichen97
bolichen97 merged commit a8ab9d3 into main Aug 10, 2026
84 of 88 checks passed
@bolichen97
bolichen97 deleted the feat/webhooks-under-settings branch August 10, 2026 17:10
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Aug 10, 2026
encomjp pushed a commit to encomjp/kirocrew-customapi that referenced this pull request Aug 22, 2026
kirodotdev#2056)

The top-level rail slot overstated it. Inbound webhooks are operator
configuration touched once at setup — minting a token, flipping the kill
switch — not a daily destination like Sessions or Schedule.

Move the entry point to Settings → Webhooks (SYSTEM group, after Computer
Use) and suppress the rail item with the registry's existing
`hiddenFromNav`, which keeps the route, badge selector, and tab-title
attention count wired. `/webhooks` stays valid, so bookmarks and the
onboarding anchor still resolve.

Settings gets a summary panel rather than the page itself. The Webhooks
page is a rail-and-detail shell with its own resizable splitter and
Settings is a `SidePanelLayout`; embedding one in the other stacks two
rails and spends most of the width on chrome. So the panel answers what a
settings reader asks — is it on, how many callers can reach it — and hands
off to the full page for the work.

The badge separates "Turned off" from "No tokens yet". `enabled` is the
effective state (`has_tokens && switch_on`) while `switch_on` is the kill
switch alone, and those diverge on a fresh install: switch on, nothing
minted. Collapsing both to "off" would report first-run as broken.

A failed read now says so. Gating the badge and count on `!isError` alone
left a mute card that looked identical to a healthy endpoint with nothing
configured — caught by rendering the panel against a gateway-less harness
and seeing the state vanish.

Adds `capture/webhooks-settings.{html,tsx}` for the same reason the
Webhooks page has one: reaching `/settings?tab=webhooks` through the SPA
needs a live gateway and a dashboard credential, and without one the shell
renders the Kiro CLI prerequisite gate instead of Settings.

12 new strings across all ten locales. Plural categories are per-language
rather than copied from English: ru carries `_one/_few/_many/_other`, ja
only `_other`, and es/fr/pt/it needed the `many` CLDR category their
catalogs already mirror from `other`. zh-CN uses 轮次 for "turn" per
style/zh-CN.md §2, and fr says "jeton" to match the existing catalog
rather than "token".

Co-authored-by: Kyle Seaman <kseam@dev-dsk-kseam-1b-55230d27.us-east-1.amazon.com>
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