feat(command-bar): let an installed app contribute command rows - #7423
Conversation
UX Review (Fable 5) — ✅ PASSUX-level review of UX-Verdict: PASS The two-step argument flow, resolved-prompt consent preview, and clip warning make an app-authored auto-send comprehensible and controllable on first encounter. Suggestions
[UX-REVIEWED] b866e11 |
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS Sound contribution-point design with a disclosed one-way door: the first signed Watch
[DESIGN-REVIEWED] b866e11 |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All mechanisms verified. I have what I need for the verdict — the author's claims about pre-existing opt-outs check out, the repo conventions (capture scripts, temp-screenshots) are established, and the consumer counts are done. Final review: First-Principles-Verdict: CONCERNS The seam earns its place, but its signed vocabulary freezes on first use with exactly one external client ever having exercised it — confirm the vocabulary now. What this change shipsIntent: let a person add launcher commands from their own app instead of patching the product — an ADDITION.
Watch
[FIRST-PRINCIPLES-REVIEWED] b866e11 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
|
Round 1 disposition -- all three GPT findings were real and self-introduced. Fixed in BLOCKING, Fixed one level deeper than the suggested
The command still works: its prompt lands in the composer, where the text is visible and one keystroke sends it. BLOCKING, A pattern that quantifies a group whose body is itself quantified is now refused, by the same syntactic check in FINDING, Tests added: 6 catastrophic patterns refused and 9 safe ones still accepted on each side -- including this feature's own shipped app pattern and the two shapes a naive check gets wrong ( Local gates on |
|
Round 2 disposition -- all three findings real, fixed in
So the primitive is gone. A manifest no longer supplies a regex at all -- it NAMES one of a fixed set of matchers the host implements: "argument": { "kind": "url", "hosts": ["github.com"] }
Two details worth naming: the host allowlist is exact unless an entry carries a leading dot, so The cost, stated plainly: apps lose fine-grained matching. The old pattern demanded the link be a PR list or a single pull request;
The mechanism, verified in I did not take the suggested fix (navigate to
Gates on |
bfc3c80 to
f0bd58c
Compare
|
Round 3 disposition -- First Principles' blocker was correct and is fixed in Blocker: the docs described a schema the code had dropped. Correct, and the harm analysis is exactly right -- this was worse than stale prose. An author copying the reference example would write Both halves fixed:
Watch: Watch: undeclared riders / I tried the suggested rebase onto main and did not keep it: the push was refused by a local content gate that scans the push range, because rebasing made 75 of main's commits newly reachable from this branch ref and their messages carry non-ASCII punctuation and PR Hygiene: 3 commits, needs 1 or 2. Squashed to one. The description is also rewritten -- it still described the retired Gates on |
f0bd58c to
8cb6b7e
Compare
|
Round 16 disposition -- Design PASS, First Principles CONCERNS. No code change this round: one item is answered by the description as it already stands, the other I am rebutting with the spec that named the key. GPT and Opus are still running on "Items 7-8 ride along undeclared" -- already declared, and this is the second time. The description carries a section titled "Two pre-existing bugs this fixes, which are not about contributions", added five pushes ago, and it names both: the I raised the same rebuttal in an earlier round and the finding has returned unchanged, so the more useful thing to say is what I think is happening rather than repeating myself: this lane's comment is rewritten in place on each push, so it has no memory of what it previously asked for or what was answered. That is worth a maintainer's eye because the failure mode is not "the author ignored it" -- it is a lane that cannot see its own history re-reporting a closed item, and there is no way for me to close it from this side except to keep pointing at the body. "Collapse the
This same lane cited that entry in an earlier round as the reason the feature is justified at all ("the spec's own 'Deliberately not here' named this exact key"). Implementing the recorded decision and then renaming the key it recorded would leave the spec describing a contract the code does not have -- the exact defect this lane has twice made me fix elsewhere in this PR. Two supporting reasons, though the spec is the load-bearing one. The point about Both verdicts are advisory and neither blocks. Lane state on |
|
Round 17 -- no code change. Two reds, and neither is mine to fix from here. Both Backend Tests reds are MAIN-OWNED, and it is a NEW instance of a ratchet that keeps drifting. Attributed rather than assumed: I checked out This branch is 125 commits behind main, so the fix reaches it only through a new merge ref -- a re-run replays the original one. A rebase is the mechanism, and it is worth flagging that when I tried it earlier this PR the push was refused by a local content gate that scans the push range, because rebasing made 125 of main's own commit messages newly reachable from this branch ref. So: main needs to correct its own census, or someone with the standing to override that gate rebases this branch. GPT's blocking finding is the fourth appearance of one theme, with the remedy I have already rebutted twice. The four: a stale command snapshot at submit (fixed), a disable landing mid-await during session creation (fixed), a stale What I established, and stand on:
The question underneath is whether "app enabled" is a client-enforced control or a server-enforced one. If it is a security boundary it belongs server-side, where the skill registry already is. If it is a UX courtesy -- do not run what the reader just switched off -- the in-tab guard already added is the right size, and the spec should describe it that way rather than as a control. That is a maintainer's call, not mine, and it is the reason I am stopping here rather than pushing a sixteenth head. Everything else on |
|
/ai-review override gpt 9540184: Premise verified, consequence is not privileged execution: both disable callers revoke skills (CLI deregister_app, dashboard teardown_app_runtime), so a prompt slipping the cache window names an unregistered skill and the agent says so. The prescribed re-fetch adds a round-trip to every activation, fails closed on a gateway blip, and cannot close the window because a cache is not the authority; enforcement belongs server-side. |
Human judgment recorded@chenmingwei23 marked the gpt AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
State on GPT 5.6 Review now reads success -- the human override was recorded ( All four remaining reds are one main-owned failure plus its aggregate.
Attributed rather than assumed: The main-side fix is #7554 ("route pip stderr logs through the context redactor"), which touches exactly Sequencing note for whoever picks this up. This branch is 125 commits behind main, so once #7554 lands the fix reaches this PR only through a new merge ref -- a re-run replays the original one. That means a rebase, and a rebase is a push, which VOIDS the SHA-pinned GPT override and re-arms that gate. So the order is: #7554 merges, then rebase, then re-post the override on the new head. Worth knowing before anyone pushes here, because a casual rebase silently loses the override. Two open product questions remain, both advisory and neither blocking: whether |
|
Round 18 -- rebased onto main, and GPT's post-rebase finding is FIXED, not overridden. The rebase cleared the four backend reds. Main's GPT raised a NEW finding on the rebased head, so the standing override authorization did not cover it. It was real:
Verified before implementing. It is also worse than a silent erasure, which the finding did not say: the frontend already distinguishes "no argument" ( Fix, following the shape already used four times in this file ( A second divergence found while fixing the first, on the other side. Both fixes are mutation-verified, and the first attempt at the fixture case was wrong. My initial conformance case used a prompt containing the token, so with the refusal stubbed out it still passed -- the older "interpolates but declares no argument" error was carrying it. The case now uses a non-interpolating prompt, which nothing else rejects: stubbing the guard fails it with "expected a refusal, got none". The array case could not isolate on both sides at once (each validator has a different neighbouring rule that also fires), so rather than keep a fixture case that passes for a different reason on each side, it is pinned by a frontend unit test under a prompt that does interpolate -- where the array check is the only path that can refuse it. Gates on |
|
Round 19 -- one GPT finding fixed, both First Principles items taken. GPT: serialization erased a restriction the refusal flags could not carry. Real, and the deepest instance of a shape this PR has now hit six times. Verified the chain rather than the line: I did not take the prescribed remedy as written. "Serialize rejection markers so the frontend drops the command" works only for a dashboard that understands the new marker; one that does not would read the permissive matcher, which is the failure being fixed. So Scope is deliberately narrow, and the reasoning is in the code: a malformed Pinned by a parametrized test over all three shapes plus a converse case asserting a legitimate First Principles item 8 -- the riding fix was declared, but not where anyone could read it. The claim was that the description never mentions the Ask row. It did, in "Two pre-existing bugs this fixes"; the section just started at byte 9,393 of a 16.5KB body, past the 8,000-byte cut the review sees. So the declaration existed and was invisible, which for a reviewer is the same thing. Moved that section to the top of section 3: the sentence saying the 404 fix changes the Ask row too now sits at byte 2,316. The 5-section order is unchanged. This was a presentation defect, not a missing disclosure, and the reviewer was right that a human should be able to see it. First Principles subtraction -- Gates on |
|
Round 20 -- GPT is green (no blocking, no override). Its two advisory findings are both fixed; one First Principles subtraction is taken and one is rebutted. GPT reached "no blocking findings" without an override. Worth stating plainly because an override was authorized and available: six rounds of real fixes got there instead, and the override posted earlier was voided by the rebase and never re-posted. Both remaining GPT items were advisory, and I fixed them anyway, because each is another instance of the exact shape this PR argues against -- a malformed declaration silently becoming a valid-looking one. A non-object ENTRY was filtered out before validation. This one changed an existing test that asserted The mirrored caps were measured in the wrong unit. The numbers matched -- 120, 4000, 60 on both sides -- but First Principles:
Gates on |
|
Round 21 -- GPT, Design and First Principles are all clean on Where the lanes stand. GPT: no blocking findings and, this round, no advisory findings either -- both of last round's are fixed. Design: PASS. First Principles: PASS, upgraded from CONCERNS, and it withdrew both of its own earlier items: UX was right about the preview screenshot, and the cause is worse than a stale frame. I opened The cause is in the capture harness. Every other step waits for a selector before shooting; the preview step went straight from The deeper hole: this cue had no test, and could not have had one. jsdom performs no layout, so Provenance in the argument state. Not done, and why. The row-meta half of the same finding -- that contributed rows carry the same "Command" tag as builtins in Re-capturing the four PNGs needs a running pod, which this host cannot provide ( Gates on |
|
Round 22 -- two new GPT blocking findings, both real, both fixed. Neither was the finding the override covers, so no override was used. An icon naming an INHERITED key crashed the whole overlay. I proved this rather than accepting it: reverting the fix and running the new test reproduces One correction to the finding's scope, in the interest of not overclaiming: of The subtitle and the app label were the last unbounded searchable strings. This one is worth naming as my own inconsistency rather than a new discovery: I bounded Both fixes mutation-checked. Removing the subtitle cap fails two tests including the fixture case; reverting the icon lookup fails the Gates on Still advisory and not blocking: the |
|
Round 23 -- two more GPT blocking findings, both real, both fixed. Both are consequences of my own two previous rounds, which is worth saying plainly rather than presenting them as fresh discoveries. A lone surrogate crashed validation -- a regression I introduced last round. Last round I changed the caps to count UTF-16 units so they match the launcher. Fixed with Resize could hide an auto-sent tail -- and I had already found this and left it. The measure effect's dependencies are the previewed CONTENT, so narrowing the viewport rewraps the text and a prompt that fitted starts clipping with the cue absent. With I owe a disclosure here: I identified exactly this gap last round while investigating UX's screenshot item, wrote it down as "no re-measure on container resize", and chose not to act on it. UX suggested a ResizeObserver in the same round. GPT has now made it blocking, and it was right to. Two reviewers and my own notes converged on the same instrument, so the honest reading is that I under-weighted it, not that it is new. Fixed with a ResizeObserver on the preview element rather than the prescribed "add the viewport value to the effect dependencies". Holding a viewport value in state requires a window listener anyway, and a window listener is strictly weaker: the box also changes when a font finishes loading or the dialog reflows, and neither raises a resize event. Observing the element covers every cause. Both mutation-checked. Reverting the encode reproduces the crash quoted above; removing the observer fails the new resize test while the content-driven cases stay green -- which also confirms the new test is measuring the observer rather than being carried by the existing measure. Gates on Still advisory and not blocking: the |
|
Round 24 -- Design PASS. First Principles CONCERNS with one item, and it is the same presentation failure as before, on my side. No code change this round. The head is unchanged at The one-way-door item was already disclosed, and again invisible. First Principles asks a human to confirm the contribution vocabulary before the first SIGNED app freezes it, and notes no manifest under The problem is where that section sits. Reviews read the first 8000 bytes; the section was at byte 10,968 of a 16.3KB body, and I measured that the visible window contained ZERO mentions of signing, freezing, or the vocabulary. So the reviewer was not missing the disclosure, it could not see it. This is the second time this exact mechanism has produced a finding, and the first fix caused the second one. Two rounds ago the riders declaration was past the cut, so I moved it to the top of section 3 -- and that block runs about 7.9KB, which then consumed the entire visible window and pushed everything after it out of view. I traded one invisible disclosure for another without noticing. Fixed structurally rather than by shuffling again: a short "Two things a reviewer should decide before this merges" block now sits at byte 1,542, immediately after section 2, naming both decisions and pointing at the full treatments below. The visible window now mentions the signing freeze, the vocabulary, and the Ask row; it mentioned none of them before. The detailed sections stay where they are, so nothing is duplicated as a claim -- one is the decision, the other is the explanation. On the substance, which is genuinely for a maintainer. Its second Watch note repeats the legacy palette's |
|
Round 25 -- Opus clean, Design PASS. UX's two items are both fixed, and one of them requires me to correct something I stated on this PR last round. Correction first: I was wrong that the screenshots could not be re-captured here. Last round I wrote that re-running the harness needs a running pod this host cannot provide, and left the frames stale on that basis. That was wrong, and the harness's own header says so:
It was built for exactly this constraint. I asserted an impossibility from a stale assumption instead of reading the file, and that turned a fixable problem into a declared limitation for a round. The root cause was also not what I said it was. I attributed the missing cue to the capture racing the measure effect. The real reason is that Rebuilt the SPA (the fresh bundle does contain the string), re-ran the harness, and read frame 4 myself before committing it: the amber "Scroll to read the rest -- this instruction continues below." now sits directly under the clipped box, and "PR Bulk Ops" renders as the attribution line under the subtitle. Both consent-critical elements UX said were missing are in the frame. All four frames are refreshed and byte-identical to what the harness produced. "Open View" was a false promise, and my own comment shows I guarded the wrong risk. Fixed with one new key, That test took three attempts and the first two passed or failed for the wrong reason -- worth recording, since the same trap recurs here. Selecting the row by typing a query breaks UX's third item stays a Suggestion and stays deferred: the Gates on |
|
Round 26 -- Design PASS. UX's two items fixed; First Principles has one new observation worth acting on that I am NOT acting on unilaterally, and one subtraction that is Raymond's call.
All three now carry the title cap on both sides, refused rather than trimmed, with a fixture case pinning it (33 cases). Chose Frame 1 was stale again, and again by my own hand. UX caught that
First Principles' sharpest point this round, which I am deliberately not acting on alone. It observes that I am not renaming it unilaterally, for two reasons. It changes the manifest contract, so the already-published external app breaks until its own repo is updated -- a cross-repo change I should not make silently inside a converged PR. And the review is explicitly asking a human to CONFIRM the vocabulary, not asking me to pick new names; choosing them myself is the opposite of what it requested. If the answer is to rename, Its Gates on |
|
Round 27 -- GPT and Opus both clean, and no lane carries a blocking finding on Design's Suggestion is the one thing here that was mine to act on, and it is a good one. It proposed pinning the frozen vocabulary as an explicit signature-compatibility test class "so a future rename fails a named gate rather than a reviewer's memory". Three reviewers have now asked a human to confirm this vocabulary before the first signed use because it cannot be changed afterwards -- and until now the only thing protecting it was that they had said so. Added I narrowed the suggestion where it overstated the consequence. It listed the caps among the frozen surface; they are not. The gate is mutation-checked in the two directions that matter: renaming the Everything else this round restates items already dispositioned and already Raymond's. Design's two Watch items -- the vocabulary freeze and the two shipped-behaviour riders needing an explicit yes -- are the same two First Principles raises, now stated by a second reviewer, and both are named in the description's "Two things a reviewer should decide before this merges" block near the top. Design adds one detail worth passing on: the Gates on |
|
Round 28 -- Opus reports "No findings". UX raised one new item that is a real accessibility defect, and I have stopped deferring a second one I was wrong to keep holding. The consent preview told a keyboard user to scroll a box they could not reach. The Fixed as a NAMED region rather than a bare focusable block: Getting there cost two wrong attempts worth recording. I was wrong to keep deferring the row provenance, and I am doing it. I called it a visual design decision twice. Re-reading the substance: a contributed row was visually identical to a builtin -- same badge, same shape -- while its prompt goes to an agent with tools, the subtitle cannot carry attribution because the app may write its own, and an argument-less command never reaches the argument state where the attribution line lives. So "which app put this in my launcher", which this code's own comment calls the reader's first question, could go unanswered end to end. That is a trust surface, not a styling preference, and three consecutive reviews named it. The meta column now reads "PR Bulk Ops - Command" for a contributed row, composed with the separator that column already uses for folder and timestamp rather than a new string -- the app's own name is data, not copy. It required one field on the row type, which I want to flag against myself: I deleted Frames re-captured LAST this time, after the final code change and a fresh build -- the rule I got wrong twice. Frame 1 now shows all three contributed rows carrying "PR Bulk Ops - Command" while the settings rows still read "Setting", confirming the change is scoped to contributed rows, and the footer reads "Continue". Both fixes mutation-checked: removing the tab stop fails the accessibility test, and dropping the composed meta fails the provenance test. UX's two Suggestions stay deferred and stay named: Gates on |
|
Round 29 -- UX is now PASS, Opus has no blocking issues, and its one advisory finding was a real defect I had introduced two rounds ago. Fixed. UX went to PASS. Its headline on this head reads that the consent chain -- attribution, argument step, resolved-prompt preview, clipped-tail warning -- "makes app-authored auto-send comprehensible and safe on a cold read". The accessibility fix and the row attribution closed both of its Watch items. Opus caught me creating a new instance of the exact failure this PR exists to prevent. Two rounds ago I capped Fixed the way Opus suggested, and its reasoning is the right one: none of the command's own declared fields is what is too long, so the proportionate response is to drop the LABEL, not the rows. An over-long label now becomes empty with a distinct console warning -- "renders without attribution", not "was skipped" -- and the row still works, it just cannot say who contributed it. That is strictly better than the row not existing. Worth being explicit that this does not undo the reason the cap was added. The cap existed because the label reaches the searched subtitle through I did NOT take the other available route, capping Mutation-checked: restoring the One red on this head is a runner flake, and I checked rather than assumed. Design and First Principles remain CONCERNS on the same two items, now stated by both: the signature-frozen vocabulary with one external client, and the rider that changes a shipped row. Both are named at the top of the description in the "Two things a reviewer should decide" block, both are dispositioned, and both are a maintainer's call rather than mine. Gates on |
|
Round 30 -- the only red on 63 checks green, 4 skipped, 1 failure. The failure is Both passes died on the same 401. So this is the reviewer's own backing credential, not this PR: there is nothing in the diff that could produce it, and no verdict exists to dispose of. Recording it here because the failure text on the check is just "did not complete", which does not name the cause -- whoever owns that workflow may want to know it was a token expiry rather than a model or prompt problem. Confirmed it is transient rather than repo-wide, before spending another re-run on it. Another open PR's GPT lane ran 21:12-21:24Z and concluded SUCCESS, which is after this failure at 20:44Z -- so the credential has been refreshed in the interval and the lane can complete again. An earlier re-run I dispatched at 18:50Z hit the same expiry, which is why this one waited on evidence that the window had closed instead of retrying blind. Re-run now dispatched. Everything else on this head is clean. UX PASS. Opus "No findings" -- its previous advisory was the one-sided
|
|
Round 31, GPT's BLOCKING finding -- FIXED at The finding. Why it was there. Round 29 refused the whole command when the label was too long, and Opus correctly called that a fresh instance of this PR's own bug class: The fix keeps both. The label degrades in steps and can no longer be empty: One correction to the prescribed remedy. The finding says to "fall back to a bounded One thing the finding did not cover, closed with it. The chain is only non-empty while Verification. 192 frontend tests pass (3 tests replace the 1 that asserted the old empty-label contract: the No divergence introduced. The Python validator has no appLabel notion and the shared conformance fixture has zero cases for it, because the fixture pins the command-ENTRY contract that both sides validate, while the label is composed from app-record fields on the frontend only. Nothing to mirror here. |
|
Round 32 -- What failed, precisely. Not eslint: that step ran The ellipsis is a rendered string literal, so the gate counted it as untranslated user-visible copy on a line this branch wrote, and the file regressed from 0 to 1. The gate's own message also closes the escape I would have reached for first: the strict config "looks INSIDE ALL-CAPS module constants", so hoisting the character to a constant would not have cleared it. Why this got past me, stated plainly. My local i18n step was The fix: the clip loses the ellipsis and takes the full bound. The alternatives were a catalog key in eleven locales for a punctuation mark, or a new shape exclusion in Verification. One number worth flagging for whoever owns the ceiling. CI measured 603 eslint warnings on the previous head where my tree measures 602 with the identical command, so the gate is sitting at exactly its ceiling with no slack in CI's environment even when it looks like it has one slot locally. That is not this PR's to fix, but the next branch to add a single warning will red on it. |
|
Round 33 -- The failure is a census assertion, not a behaviour test: Established by running it, not by reading the diff:
CI runs the PR's MERGE with current main, which is how a failure that lives only on main lands on a head whose own tree is clean. Every open PR on the repo inherits it the same way. Already being fixed, so nothing is needed here. #7758 ("fix(ci): use context redactor for Slack log") is exactly this site and is MERGEABLE awaiting review; #7761 covers the sibling heartbeat line. Once #7758 lands, a re-run of this shard clears without a push here. Two things I deliberately did NOT do. I did not rebase: my base is 73 commits behind, so rebasing would import main's failing state into a branch whose tree currently passes, and it would re-roll every review lane on a PR that has just converged -- paying a real cost to make a red genuinely mine. And I did not raise the census in this PR: it is main's number to correct, #7758 already corrects it, and a security-census bump has no business riding a command-bar change. The remaining backend shards are still in flight; if the Linux ones red on the same assertion, that is the same single cause and not a second finding. |
|
Round 34 -- main's census bug is FIXED upstream, but re-running the failed jobs cannot pick that up, and here is the proof. No code change; this is a record of why the red persists. Main is fixed. PR #7761 (commit The re-run still fails, and not because anything is wrong here. Attempt 2 of run 33568424211 re-ran the four failed jobs and hit the identical assertion. Its checkout line says what happened:
What that leaves. A fresh dispatch is the only thing that recomputes The cost is that it re-rolls all five review lanes, which have all judged this exact tree clean (GPT and Opus "no blocking findings", UX PASS, Design and First Principles CONCERNS on the advisory sign-off items only). GPT is non-deterministic, so a re-roll is a fresh sample rather than a replay of a known result. That is a real cost, and it is the maintainer's to spend rather than mine, so it is his call and not something I will do quietly -- the more so because there is no reading of the current state in which this PR merges without it. Everything else on the head is green. |
The Command Bar had no contribution point: an external app could appear as a "jump
to this app" row and nothing else, and the module's own spec listed
`contributes.commands` under "deliberately not here". A quick action belonging to an
app therefore had to be hard-coded into the launcher, in this repo, forever.
An app now declares rows in its manifest and the launcher renders them:
"contributes": { "commands": [{
"id": "approve-all", "title": "Approve all PRs",
"argument": { "kind": "url", "hosts": ["github.com"] },
"prompt": "Load the $my-skill skill and approve every PR behind {argument}",
"autoSend": true
}] }
A contribution is DATA, never code. There is no way to ship a function or an icon
URL: the launcher would be running app-authored JavaScript in the host's surface on
every keystroke, and the root page promises to issue no network request.
That principle is why the argument NAMES a matcher instead of supplying one. An
earlier revision of this branch accepted an app-supplied regex, which was wrong in
a way worth recording: a regex is a small program, and it ran against the field on
every keystroke on the thread that draws the launcher. `^(a+)+$` and `^(a|aa)+$`
are both under ten characters and both exponential, and neither runtime can
interrupt a synchronous match. Screening patterns syntactically was tried and
abandoned, because such a check only recognizes shapes and each version invites the
next one it does not cover. `kind` now selects a host matcher (`url` with an
optional host allowlist, or `text`), and `url` uses the runtime's own URL parser,
which is linear by construction.
The cost is precision, and it is real: `url` + `hosts` admits any URL on the host
and leaves what the link DENOTES to the agent. That is the right split, since the
host cannot safely encode another product's URL taxonomy. The allowlist is exact
unless an entry carries a leading dot, so `github.com` does not admit
`github.com.evil.test`, and only http/https parse.
An argument still carrying `pattern`, or naming an unknown `kind`, is REFUSED
rather than migrated: either would otherwise fall back to `text` and accept any
non-empty string while the app still declares autoSend and believes it is guarded.
`autoSend` sends app-authored text to an agent with tools as if the reader typed
it, so the argument field shows the RESOLVED prompt, their value already spliced
in, before Enter sends it. It therefore requires an argument: the preview is the
consent and it lives in that step, so a command collecting nothing cannot
auto-send. It is honoured only for the JSON boolean `true`, never `"false"`.
Validated on both sides, which is not redundancy: an unknown manifest key reaches
the dashboard through `extra` having passed no schema, so an older gateway can put
an arbitrary object on this path. The frontend re-checks everything, skips a bad
entry with a console warning rather than throwing, caps commands per app by
ATTEMPTED entries (a counter advancing only on success let a flood of malformed
ones run a validation and a warn each), and namespaces row ids `app:<app>:<id>` so
a contribution cannot impersonate a builtin.
Also fixes a pre-existing trap this feature made reachable: seeding a session with
`?autoSend=1` from a page where ChatPage was ALREADY mounted armed the send ref in
a render where none of the effect's deps changed, so the prompt was neither sent
nor left in the composer. The arming site now bumps `autoSendTick`, the remedy the
no-slot retry below it already uses.
Contributions are covered by the admission signature. A contributed prompt goes to
an agent with tools and `autoSend` fires it, which is the same surface class as a
cron's `command` -- already signed for exactly that reason. Left out, a signed
app's rows would be the one part an attacker could rewrite with the signature
still verifying, and the reader's trust in that signature is what would carry the
tampered prompt into a session. The matcher rides along too: widening `url` +
hosts to `text` changes no visible character of the row but decides whether the
value was checked at all. Emitted only when non-empty, so signatures issued before
contributions existed keep verifying.
Submitting an argument re-resolves the command from the CURRENT contributions
instead of the snapshot taken when the field opened. That field stays open while
the reader pastes a link, and the app can be disabled in another tab meanwhile --
the row vanishes at once but the captured object does not, so the prompt of a
just-disabled app would still run. Now it aborts and says so.
Verified: backend 566 passed, Command Bar vitest 131/131, tsc -b clean, eslint 0
errors, both i18n gates 0, mypy clean. Four frames captured against the real built
bundle: rows offered, argument state, a GitLab URL refused by the host allowlist,
and the resolved-prompt preview with the link spliced in verbatim.
|
Round 35 -- rebased onto current main at Why this is no longer a judgement call. I had been holding the branch still because the only way to refresh the pinned merge ref was a new head SHA, and that re-rolls five converged review lanes on unchanged code. The conflict, and how it was resolved. One hunk, the Main moved a gate while this sat. The eslint ceiling went from Full sweep on the rebased head, all green.
Two things worth stating plainly rather than burying. Recovering the toolchain cost a detour: I ran The 4 screenshot URLs in the description are repointed to the new SHA. CI has dispatched 55 checks; all five review lanes re-roll from scratch on this head, as they would have on any new SHA. |
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
1. What is the problem?
Every row in the Command Bar is hard-coded into this repository. An app can appear as
a destination ("go to this app's page"), but it cannot contribute a command, and
docs/system-specs/modules/command-bar.mdsaid so under Deliberately not here:The quicklinks group was designed and then removed for the same reason -- "a group
with no writer was removed rather than shipped empty".
So there is no way to add a launcher row without patching the product: a new row
means editing the bundle, translating its copy into twelve catalogs, and shipping it
to everyone who installs Kiro Crew. Work that belongs to one person's workflow has to
enter the shared surface to exist at all.
2. Why this issue matters to the user
A launcher is only as useful as the commands in it, and the commands worth having are
the ones specific to how a person works. The current shape makes exactly those the
most expensive to add.
It also puts the cost in the wrong place. Adding a fourth workflow row costs a code
change plus twelve translations plus a review cycle; it should cost one JSON object in
the reader's own app. And the constraints that belong to a command -- what shape of
input it accepts, whether it may send immediately -- have to be decided in product
code on behalf of everybody, rather than declared by whoever owns the command.
Two things a reviewer should decide before this merges
Both are disclosed in full further down, but they are the decisions rather than the
implementation, so they belong where they cannot be missed.
This freezes a vocabulary.
contributesis insidesigning_payload(), because acontributed prompt reaches a tool-enabled agent and is the same surface class as a
cron's
command. It is emitted only when non-empty, so signatures predating thisstill verify -- but once one SIGNED app declares a contribution, the canonical bytes
fix the whole vocabulary: the
kindnames, the leading-dot host rule, every cap.Widening it later stays compatible; changing what a name MEANS does not. No manifest
under
src/kiro_crew/apps/builtins/declarescontributestoday, so the only writeris one external app, and the design has been validated against that one client. Worth
confirming the vocabulary is the one you want before the first signed use fixes it.
Two pre-existing bugs ride along, and one changes an existing row. Both are wider
than contributions and are detailed at the top of section 3. The one to look at is the
switchSlot404 race: the fix is the existingkeepTargetOnMissingopt-out, andbecause the launcher's own Ask row goes through the same path, that row's behaviour on
that race changes too. That is intended, but it is a change to something already
shipped and should be an explicit yes rather than a rider nobody read.
3. How our fix solves it
Two pre-existing bugs this fixes, which are not about contributions
Both were found by review of this branch and both are wider than this feature, so they
should be read as riders rather than as part of the contribution point.
A new session's own 404 could seed the PREVIOUS conversation.
switchSlot.rejectedtreats a missing-slot error as "the target is gone" and restores the slot the caller
came from (#6309). A caller that just CREATED its target hits that path on a
create/fetch race, so the pending input then landed in whatever chat the reader had
open before -- and with
autoSend, fired there. The fix is the existingkeepTargetOnMissingopt-out, whichChatPage.tsxalready uses for the same reason.The launcher's own Ask row goes through this path too, so it is fixed as well.
?autoSend=1silently dropped the prompt when ChatPage was already mounted. Thesend effect's deps are
[send, connected, autoSendTick]. A cold navigation movesconnectedand fires; a caller already on/chatonly changes the search params, and aseeder that awaits between activating its slot and setting the pending input arms the
ref in a render where no dep changes. The text was then neither sent nor left in the
composer, because that branch has no composer fallback. The arming site now bumps
autoSendTick, the remedy the no-slot retry below it already uses.One sibling of the first cause is deliberately NOT fixed here: the legacy palette's
newSessionWithTokenleans on the same "create activates" assumption. The module specalready records deleting that palette as a separate change, so folding its rewrite into
this PR would mix a platform seam with a deprecation.
contributes.commandsis a new manifest contribution point the Command Bar reads. Anapp declares the row and what it does; the host renders and runs it.
Following the chain from symptom to root cause:
CommandBarOverlay.tsx.contributessits besideuirather than inside it, and that split is the point:uiis where an app declares surfaces of its OWN, while a contribution is a rowinside a surface the host owns. An app contributing commands needs no page, no
frontend bundle, no backend and no process.
A contribution is data, never code. There is no way to ship a function -- that
would be third-party JavaScript running inside the host's own surface, on every
keystroke, with the reader's session -- and no way to ship an icon URL, because the
root page promises to issue no network request. Icons name a glyph from a host
allowlist. This is the same trade the overlay registry already makes by resolving
idagainst components compiled into the bundle rather than loading one from the app.The argument names a matcher; it never supplies one. A command may collect one
value, and that value is spliced into an instruction handed to an agent with tools, so
"whatever the reader pasted" is not an acceptable domain -- but the check belongs to
the host.
kindselects one of a fixed set (url, with an optionalhostsallowlist, or
text) and the host implements each.This is the second design here, and the first is worth recording because review found
it twice. It accepted an app-supplied
pattern. A regex is a small program, and thatone ran against the field on every keystroke on the thread that draws the launcher:
^(a+)+$and^(a|aa)+$are both under ten characters and both exponential, andneither runtime can interrupt a synchronous match, so no timeout was available. The
first fix screened patterns syntactically for nested quantifiers; review then produced
the alternation form, which that check does not cover. It never could -- a syntactic
check recognizes shapes, so each version invites the next pattern it does not know. So
the primitive was deleted rather than fenced again.
urlnow parses with theruntime's own URL parser, which is linear by construction, and both nested-quantifier
detectors, the 200-character cap and the anchoring rule went with it.
The cost is precision, and it is real rather than free: a pattern could demand
/pull/<n>, whileurl+hosts: ["github.com"]admits any URL on that host andleaves what the link DENOTES to the agent reading it. That is the better split -- the
host is the wrong place to encode another product's URL taxonomy, and it cannot do so
safely. The allowlist is exact unless an entry carries a leading dot (
.github.comadmits subdomains;
github.comdoes not admitgithub.com.evil.test), and onlyhttp/httpsparse, becausejavascript:anddata:are valid URLs and this valueis shown back to the reader.
An argument still carrying
pattern, or naming an unknownkind, is REFUSED ratherthan migrated. Silence would be the dangerous outcome:
patternis an unknown keynow, so ignoring it leaves the argument on the default
textmatcher -- any non-emptystring -- while the app still declares
autoSendand still believes its patternguards the value.
Validated twice, deliberately.
AppManifestchecks it on every parse, andcontributedCommands.tsre-checks the same rules before rendering. The second pass isnot redundancy: an unknown top-level manifest key already reaches the dashboard
through the manifest's
extrabucket having passed no schema at all, so an appinstalled by an older gateway can put an arbitrary object on this path. A malformed
contribution is SKIPPED with a console warning, never thrown -- one bad app must not
take the Cmd+K gesture down for every other app on the instance. The per-app cap
counts ATTEMPTED entries rather than accepted ones, so a manifest of malformed
commands cannot run a validation and a console warning for each.
What the reader sees before an auto-sending command fires.
autoSendsendsapp-authored text to an agent as if the reader had typed it. They chose the row and
supplied the value, but nothing had shown them the instruction. So the argument state
renders the RESOLVED prompt -- the template with their value spliced in -- once the
matcher accepts the value, and Enter sends that. The preview is withheld until the
value validates, so it never advertises text that is not what would be sent.
autoSendtherefore REQUIRES an argument: the preview is the consent and it lives inthe argument step, so a command that collects nothing never reaches it and would
otherwise send with nothing shown at all. Such a command still runs -- its prompt
lands in the composer, visible, one keystroke from sending.
Three smaller decisions worth naming:
app:<app>:<id>, so a contribution is structurally unable toimpersonate a builtin row and inherit its frecency record.
an overlay claim: an overlay REPLACES a host surface so only a builtin may claim
one, while a command ADDS a row the host renders, which is exactly the capability an
external app should have.
idleDemote(new) sorts a row to the end of its group while the query is empty, andis DERIVED from "declares an argument" rather than declared in the manifest. A
command that needs a value cannot act on an empty query, so it has nothing to offer
a launcher that just opened -- and leaving this to app authors would mean asking each
of them to volunteer their own row out of the first page.
website/src/components/appstore/types.tsgains acontributesdeclaration for thesame reason
ui.overlayswas added there: the manifest serializes it, and a readeroutside the module that owns the shape could not otherwise see the field exists.
Evidence
The frames below come from
website/scripts/capture-command-bar-contributed.mjs,which runs the real built SPA behind
serveDistwith/api/**answered fromfixtures. The contributing app in the fixture is a NON-BUILTIN app whose
contributesblock is copied from a real external app'sapp.json.Three rows contributed by an installed app:
The argument state. The chip names the command; the placeholder and the hint below
are the app's own:
A value the host matcher refuses -- a GitLab link where the app allowlisted github.com. The red line is the app's
patternError, nothost copy, and no session has been created:
The resolved prompt, shown before an auto-sending command fires:
If the images do not load, they are in the Files changed tab under
temp-screenshots/command-bar-contributed/.On this shipping with no in-repo writer
Nothing in
src/kiro_crew/apps/builtinsdeclarescontributes, and that is the pointrather than an oversight: the whole reason for a contribution point is that a quick
action belonging to an app should not have to live in this repository. The writer that
exercised it is an external app, and the capture harness quotes its manifest verbatim so
the shape under test is a real one.
That said, the module has a precedent worth naming out loud, because it cuts the other
way: a quicklinks group with no writer was removed rather than shipped empty. The
difference is that this is a contribution POINT with a consumer outside the repo, not a
group waiting for one inside it -- but a reviewer who wants an in-repo writer before
merging is applying the module's own standard, and that is a fair thing to ask for.
4. What tests we did
New backend tests (
test/test_app_manifest.py::TestContributedCommands, 32cases): a well-formed contribution validates and round-trips through
to_dict(whichis what
/api/appssends);contributesis a KNOWN field so it is not swallowed byextra; and each refusal is pinned separately -- non-kebab id, missing id/title/prompt,over-cap prompt, prompt interpolating with no argument declared, an argument the prompt
never uses, a retired
patternkey, an unknownkind, hosts on a kind with no notion ofthem, an over-cap host list, a non-hostname entry, duplicate ids, non-dict entries, and a hostile
contributesblockthat must produce errors rather than an exception.
New frontend unit tests (
contributedCommands.test.ts, 28 cases): the samerefusals from the untrusted-input side, plus the caps (20 commands per app), that a
good entry survives in an array that also holds bad ones, that the module never throws
on a hostile declaration, and that
resolvePromptinserts a value containing$&or$1verbatim.New component tests (
CommandBarOverlay.test.tsx, 15 cases): a contributedcommand renders with the app's subtitle; a disabled app contributes nothing; malformed
entries are skipped while builtin rows stay intact; a contribution cannot claim a
builtin id; the first Enter collects the argument and creates nothing; a command with
no argument runs immediately; a value the matcher refuses is held back with the
app's message and creates no session; the resolved prompt appears before an
auto-sending command fires and is withheld until the value validates; the seeded text
is the resolved prompt and navigation is
?autoSend=1withoutnewSession=1; Escapeand Backspace leave the command before closing the bar; a second Enter during a create
is refused; and a bar dismissed mid-create seeds nothing.
Gates, all green:
tsc -b+ vite build clean; Command Bar vitest 114/114 across4 files; eslint 0 errors;
i18n:checkexit 0 run withI18N_BASE_REFset to themerge-base as CI does (
added-lines0,vs-base0,changed-passthrough0,source-strings3 new English keys, 0 badly shaped);lint:i18nexit 0; jscpd clean;mypy clean on the two changed Python modules; backend pytest 575 passed / 2 skipped
across manifest, discovery, manager, bridges, assets and optional-enable.
test/test_app_bridges.py's dataclass-driven ratchet correctly caught the new fieldbeing absent from its probe manifest, which is what that test exists to do; the probe
now declares a contribution.
Verified from outside the repo: an external app was registered through
register_external_app+enable_appandlist_apps()shows all three of itscommand ids reaching
/api/appswith prompt, matcher and autoSend intact, and nouior
backendkeys in its manifest.Not verified in a pod. The pod came up healthy but this host has a restricted
/proc, solsofcannot prove which process holds the port and the pod withholds itscredential by design. The screenshot harness above was used instead; the pod's own
.local_secretwas not read.5. Any other suggestions on the work
autoSendis worth considering later. The prompt preview isconsent at the moment of action, which is the strongest thing an argument-taking
command can offer -- the reader is already looking at the field. A grant given once
at install time is weaker for this case, because it is not re-read when a write
actually fires. The two compose rather than compete, so a grant is an addition
rather than a correction.
argument is another thing to get right before a command that writes somewhere fires.
A second argument is an additive change to
CommandArgument, not a rewrite.cost, paid to keep app-authored markup and network requests out of the launcher.
external app, which is the point of the change; the tests exercise the contract in
both directions.
One follow-up named by review and deliberately not folded in here:
autoSendgrant that composes with the resolved-prompt preview ratherthan replacing it. The preview is consent at the moment of action, which a grant
given once at install cannot be; the two are complementary.