Compare commits

...

11 Commits

Author SHA1 Message Date
core-uiux 786c9f5c1a fix(canvas/mobile): wrap decorative plus icon in aria-hidden on floating spawn button
CI / Shellcheck (E2E scripts) (pull_request) Successful in 18s
E2E API Smoke Test / detect-changes (pull_request) Successful in 6s
E2E Chat / detect-changes (pull_request) Successful in 5s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 5s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 2s
Harness Replays / detect-changes (pull_request) Successful in 2s
CI / Platform (Go) (pull_request) Successful in 6m22s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 6s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 7s
sop-checklist / all-items-acked (pull_request) [info tier:low] acked: 5/7 — missing: root-cause, no-backwards-compat — body-unfilled: comprehensive-testing, local-postgres-e2e, staging-sm
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 55s
sop-checklist / na-declarations (pull_request) N/A: (none)
gate-check-v3 / gate-check (pull_request) Successful in 4s
qa-review / approved (pull_request) Failing after 4s
security-review / approved (pull_request) Failing after 4s
CI / Canvas (Next.js) (pull_request) Successful in 7m3s
sop-tier-check / tier-check (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 3s
CI / Python Lint & Test (pull_request) Successful in 6m39s
CI / Detect changes (pull_request) Successful in 5s
CI / all-required (pull_request) Successful in 6m41s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 28s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
Harness Replays / Harness Replays (pull_request) Successful in 38s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 42s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 44s
E2E Chat / E2E Chat (pull_request) Failing after 10m33s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 10m31s
Matches pattern applied to MobileSpawn and MobileChat. The plus icon inside
the aria-labeled "Spawn new agent" button is decorative.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 21:08:38 +00:00
core-uiux e5f6c31eac fix(canvas/mobile): wrap decorative icons in aria-hidden in MobileChat/MobileDetail/MobileSpawn
Add aria-hidden to decorative back/more icons inside aria-labeled buttons.
Fixes: attach icon in MobileChat composer was missing aria-hidden.
Matches the aria-hidden pattern already applied to MobileSpawn's check icon.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 21:08:38 +00:00
core-uiux 376f62a4e9 fix(canvas/mobile): add aria-hidden to decorative check icon in MobileSpawn
The check-mark span inside the selected-tier button is purely decorative
visual feedback — screen readers already get the tier name from the button
text. Mark it aria-hidden to avoid redundant announcements.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 21:08:38 +00:00
core-uiux ed778fa668 fix(canvas/mobile): add ARIA tab roles + keyboard nav to MobileDetail tabs
Add role="tablist", role="tab", aria-selected, tabIndex roving, and
arrow/Home/End keyboard navigation to the Overview/Activity/Config/Memory
tab buttons in MobileDetail.tsx. Matches the pattern already applied to
the bottom TabBar and the MobileChat sub-tabs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 21:08:38 +00:00
core-uiux ce697360dc fix(canvas/mobile): add ARIA tab pattern + keyboard nav to MobileChat sub-tabs
Add role="tab", aria-selected, tabIndex roving, and arrow/Home/End
keyboard navigation to the "My Chat / Agent Comms" tab buttons in
MobileChat.tsx — matching the WCAG 2.1.1 pattern already used in the
bottom TabBar. Without ARIA roles, screen readers treat these as plain
buttons with no tab-group semantics.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 21:08:38 +00:00
core-uiux 70d3ed690f fix(canvas/mobile): WCAG 2.4.7 focus-visible + aria-label on MobileCanvas buttons
Adds focus-visible ring + aria-label to the three inline-styled buttons in
MobileCanvas.tsx:

- Reset zoom: focus-visible ring
- Agent node: aria-label="Open {name}" + focus-visible ring (was missing both)
- Spawn FAB: focus-visible ring

Also adds MobileCanvas to the PR scope; refs #1395.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 21:08:38 +00:00
core-uiux 56dc04e724 fix(canvas/mobile): WCAG 2.4.7 focus-visible rings on all mobile canvas buttons
Adds keyboard focus indicators (focus-visible:ring-2 focus-visible:ring-accent
focus-visible:ring-offset) to every keyboard-navigable button across the mobile
canvas layer. Buttons using inline style props now carry a Tailwind className
pair that shows the ring on keyboard focus without affecting mouse/touch users.

Components fixed:
- MobileChat: Back, More, tab-switch, Retry, Remove file, Attach, Send
- MobileHome: Spawn FAB
- MobileSpawn: Close, template select, tier select, Spawn agent
- MobileMe: Accent swatches, Theme/Density segmented controls
- MobileDetail: Back, More, tab-switch, Open chat CTA
- MobileComms: Filter chips (All, Errors)
- components.tsx: AgentCard, FilterChips

Refs #1384

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 21:08:38 +00:00
hongming-pc2 4c0cd6b705 Merge pull request 'fix(queue): correct status deduplication for combined+all_statuses sort order' (#1428) from fix/queue-status-sort into main
CI / Shellcheck (E2E scripts) (push) Successful in 10s
CI / Platform (Go) (push) Successful in 5m36s
E2E API Smoke Test / detect-changes (push) Successful in 6s
E2E Chat / detect-changes (push) Successful in 5s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 5s
Handlers Postgres Integration / detect-changes (push) Successful in 2s
Runtime PR-Built Compatibility / detect-changes (push) Successful in 5s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 2s
Ops Scripts Tests / Ops scripts (unittest) (push) Successful in 1m1s
CI / Canvas (Next.js) (push) Successful in 5m55s
CI / Python Lint & Test (push) Successful in 6m34s
CI / all-required (push) Successful in 5m12s
Block internal-flavored paths / Block forbidden paths (pull_request) Waiting to run
CI / all-required (pull_request) Waiting to run
CI / Python Lint & Test (pull_request) Waiting to run
CI / Detect changes (pull_request) Waiting to run
CI / Platform (Go) (pull_request) Waiting to run
CI / Canvas (Next.js) (pull_request) Waiting to run
CI / Shellcheck (E2E scripts) (pull_request) Waiting to run
E2E API Smoke Test / detect-changes (pull_request) Waiting to run
E2E Chat / detect-changes (pull_request) Waiting to run
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Waiting to run
Handlers Postgres Integration / detect-changes (pull_request) Waiting to run
lint-required-no-paths / lint-required-no-paths (pull_request) Waiting to run
Runtime PR-Built Compatibility / detect-changes (pull_request) Waiting to run
Secret scan / Scan diff for credential-shaped strings (pull_request) Waiting to run
gate-check-v3 / gate-check (pull_request) Waiting to run
qa-review / approved (pull_request) Waiting to run
E2E Chat / E2E Chat (push) Successful in 1s
security-review / approved (pull_request) Waiting to run
sop-checklist / all-items-acked (pull_request) Waiting to run
sop-tier-check / tier-check (pull_request) Waiting to run
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 2s
audit-force-merge / audit (pull_request) Waiting to run
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 2s
CI / Canvas Deploy Reminder (push) Successful in 1s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 1m11s
publish-workspace-server-image / Production auto-deploy (push) Successful in 31m25s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (push) Successful in 2m7s
publish-workspace-server-image / build-and-push (push) Successful in 6m42s
Block internal-flavored paths / Block forbidden paths (push) Successful in 4s
CI / Detect changes (push) Successful in 7s
lint-bp-context-emit-match / lint-bp-context-emit-match (push) Compensated by status-reaper (workflow has no push: trigger; Gitea 1.22.6 hardcoded-suffix bug — see .gitea/scripts/status-reaper.py)
MCP Stdio Transport Regression / MCP stdio with regular-file stdout (push) Successful in 1m24s
main-red-watchdog / watchdog (push) Successful in 32s
gate-check-v3 / gate-check (push) Successful in 1m28s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Successful in 12s
ci-required-drift / drift (push) Successful in 37s
Weekly Platform-Go Surface / Weekly Platform-Go Surface (push) Successful in 5m49s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 4m40s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 3s
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 6s
gitea-merge-queue / queue (push) Successful in 5s
status-reaper / reap (push) Successful in 1m4s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 8m12s
CI / Canvas Deploy Reminder (pull_request) Has been cancelled
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Has been cancelled
E2E API Smoke Test / E2E API Smoke Test (pull_request) Has been cancelled
E2E Chat / E2E Chat (pull_request) Has been cancelled
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Has been cancelled
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Has been cancelled
2026-05-17 20:56:57 +00:00
core-devops af7afc6112 Merge PR #1417 via gitea-merge-queue
E2E Chat / E2E Chat (push) Successful in 7s
Ops Scripts Tests / Ops scripts (unittest) (push) Successful in 1m5s
CI / Platform (Go) (push) Successful in 7m26s
CI / Python Lint & Test (push) Successful in 7m10s
Block internal-flavored paths / Block forbidden paths (push) Successful in 13s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 11s
CI / Detect changes (push) Successful in 9s
CI / Shellcheck (E2E scripts) (push) Successful in 13s
Sweep stale Cloudflare DNS records / Sweep CF orphans (push) Successful in 11s
CI / Canvas (Next.js) (push) Successful in 10m7s
CI / all-required (push) Successful in 8m1s
publish-workspace-server-image / Production auto-deploy (push) Successful in 14m25s
ci-required-drift / drift (push) Successful in 1m5s
E2E API Smoke Test / detect-changes (push) Successful in 11s
E2E Chat / detect-changes (push) Successful in 11s
CI / Canvas Deploy Reminder (push) Blocked by required conditions
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Blocked by required conditions
Handlers Postgres Integration / Handlers Postgres Integration (push) Blocked by required conditions
Runtime PR-Built Compatibility / PR-built wheel + import smoke (push) Blocked by required conditions
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 4s
publish-workspace-server-image / build-and-push (push) Successful in 7m54s
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 6s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 11s
Handlers Postgres Integration / detect-changes (push) Successful in 4s
Runtime PR-Built Compatibility / detect-changes (push) Successful in 7s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 4s
Serialized merge by gitea-merge-queue after current-main, SOP, and required CI checks were green.
2026-05-17 20:07:54 +00:00
core-devops 4f5d683f4b chore: re-trigger Gitea Actions workflows (core-devops agent)
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 5s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
audit-force-merge / audit (pull_request) Successful in 6s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 4s
CI / Detect changes (pull_request) Successful in 5s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 14s
E2E API Smoke Test / detect-changes (pull_request) Successful in 7s
E2E Chat / detect-changes (pull_request) Successful in 7s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 7s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 6s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m1s
CI / Canvas (Next.js) (pull_request) Successful in 7m54s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 9s
CI / all-required (pull_request) Successful in 7m48s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 5s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 1m2s
qa-review / approved (pull_request) Failing after 3s
security-review / approved (pull_request) Failing after 4s
CI / Platform (Go) (pull_request) Successful in 6m2s
CI / Python Lint & Test (pull_request) Successful in 6m49s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 4s
E2E Chat / E2E Chat (pull_request) Successful in 5s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 4s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 3s
sop-checklist / all-items-acked (pull_request) [info tier:low] acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4
gate-check-v3 / gate-check (pull_request) Successful in 3s
sop-tier-check / tier-check (pull_request) Successful in 4s
2026-05-17 14:37:35 +00:00
core-devops df4a0e3f9d fix(queue): skip PRs with HTTP 403/404/405 merge errors instead of looping
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 2s
CI / Detect changes (pull_request) Successful in 4s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 12s
E2E API Smoke Test / detect-changes (pull_request) Successful in 5s
E2E Chat / detect-changes (pull_request) Successful in 5s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 3s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 5s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 5s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 4s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 55s
qa-review / approved (pull_request) Failing after 2s
sop-checklist / na-declarations (pull_request) N/A: (none)
security-review / approved (pull_request) Failing after 3s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 56s
CI / Platform (Go) (pull_request) Successful in 4m25s
gate-check-v3 / gate-check (pull_request) Successful in 3s
sop-checklist / all-items-acked (pull_request) Failing after 2s
sop-tier-check / tier-check (pull_request) Successful in 3s
CI / Canvas (Next.js) (pull_request) Successful in 6m54s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 3s
CI / Python Lint & Test (pull_request) Successful in 6m28s
E2E Chat / E2E Chat (pull_request) Successful in 4s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 2s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 1s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 2s
CI / all-required (pull_request) Successful in 5m54s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
The queue was retrying the same PR forever when merge returned HTTP 405
("User not allowed to merge PR"). ApiError was caught by main() and returned
0, so the next tick tried the same PR again — infinite loop.

Changes:
- Add MergePermissionError(ApiError) for permanent merge failures
- merge_pull() catches ApiError and re-raises MergePermissionError for
  HTTP 403/404/405
- process_once() catches MergePermissionError, posts a comment on the PR
  explaining the permission issue, and returns 0

The PR stays in the merge-queue label so future ticks can retry after
the permission issue is resolved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 13:55:46 +00:00
10 changed files with 159 additions and 41 deletions
+34 -2
View File
@@ -65,6 +65,11 @@ class ApiError(RuntimeError):
pass
class MergePermissionError(ApiError):
"""Merge failed with a permanent permission error (403/404/405).
The queue should skip this PR and move to the next one."""
@dataclasses.dataclass(frozen=True)
class MergeDecision:
ready: bool
@@ -367,7 +372,16 @@ def merge_pull(pr_number: int, *, dry_run: bool) -> None:
print(f"::notice::merging PR #{pr_number}")
if dry_run:
return
api("POST", f"/repos/{OWNER}/{NAME}/pulls/{pr_number}/merge", body=payload, expect_json=False)
try:
api("POST", f"/repos/{OWNER}/{NAME}/pulls/{pr_number}/merge", body=payload, expect_json=False)
except ApiError as exc:
# Re-raise permission-like errors so process_once can skip this PR.
# 403 = no push access, 404 = repo/pr not found, 405 = not allowed.
msg = str(exc)
for code in ("403", "404", "405"):
if code in msg:
raise MergePermissionError(msg) from exc
raise # re-raise other ApiErrors unchanged
def process_once(*, dry_run: bool = False) -> int:
@@ -438,7 +452,25 @@ def process_once(*, dry_run: bool = False) -> int:
"deferring to next tick"
)
return 0
merge_pull(pr_number, dry_run=dry_run)
try:
merge_pull(pr_number, dry_run=dry_run)
except MergePermissionError as exc:
# Permanent merge failure (HTTP 403/404/405). Post a comment so
# maintainers know why, then return 0 so this tick is done.
# The PR stays in the queue; future ticks can retry after the
# permission issue is resolved.
sys.stderr.write(f"::error::merge permission error for PR #{pr_number}: {exc}\n")
post_comment(
pr_number,
(
"merge-queue: merge failed with HTTP 405 'User not allowed to merge PR'. "
"No available token has Can-merge permission on this repo. "
"Fix: grant Can-merge to a token, or add a maintain/admin collaborator. "
"Skipping to next queued PR on next tick."
),
dry_run=dry_run,
)
return 0
return 0
return 0
@@ -118,3 +118,13 @@ def test_merge_decision_updates_stale_pr_before_merge():
assert decision.ready is False
assert decision.action == "update"
def test_MergePermissionError_inherits_from_ApiError():
assert issubclass(mq.MergePermissionError, mq.ApiError)
def test_MergePermissionError_message_preserved():
exc = mq.MergePermissionError("POST /merge -> HTTP 405: User not allowed")
assert "405" in str(exc)
assert "User not allowed" in str(exc)
@@ -205,6 +205,7 @@ export function MobileCanvas({
type="button"
onClick={resetView}
aria-label="Reset zoom"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
style={{
position: "absolute",
right: 14,
@@ -272,6 +273,8 @@ export function MobileCanvas({
key={l.agent.id}
type="button"
onClick={() => onOpen(l.agent.id)}
aria-label={`Open ${l.agent.name}`}
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
style={{
position: "absolute",
left: `${l.x}%`,
@@ -376,6 +379,7 @@ export function MobileCanvas({
type="button"
onClick={onSpawn}
aria-label="Spawn new agent"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2"
style={{
position: "absolute",
right: 24,
@@ -394,7 +398,7 @@ export function MobileCanvas({
boxShadow: "0 8px 24px rgba(40,30,20,0.25)",
}}
>
{Icons.plus({ size: 22 })}
<span aria-hidden="true">{Icons.plus({ size: 22 })}</span>
</button>
</div>
);
+65 -32
View File
@@ -339,6 +339,7 @@ export function MobileChat({
type="button"
onClick={onBack}
aria-label="Back"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
style={{
width: 36,
height: 36,
@@ -352,7 +353,7 @@ export function MobileChat({
justifyContent: "center",
}}
>
{Icons.back({ size: 18 })}
<span aria-hidden="true">{Icons.back({ size: 18 })}</span>
</button>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
@@ -385,6 +386,7 @@ export function MobileChat({
<button
type="button"
aria-label="More"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
style={{
width: 36,
height: 36,
@@ -398,39 +400,66 @@ export function MobileChat({
justifyContent: "center",
}}
>
{Icons.more({ size: 18 })}
<span aria-hidden="true">{Icons.more({ size: 18 })}</span>
</button>
</div>
{/* Sub-tabs */}
<div style={{ display: "flex", gap: 18, marginTop: 12, paddingLeft: 4 }}>
{(
[
{ id: "my", label: "My Chat" },
{ id: "a2a", label: "Agent Comms" },
] as const
).map((t) => {
const on = tab === t.id;
return (
<button
key={t.id}
type="button"
onClick={() => setTab(t.id)}
style={{
padding: "4px 0 8px",
border: "none",
background: "transparent",
fontSize: 13.5,
cursor: "pointer",
color: on ? p.text : p.text3,
fontWeight: on ? 600 : 500,
borderBottom: on ? `2px solid ${p.accent}` : "2px solid transparent",
}}
>
{t.label}
</button>
);
})}
</div>
{(
[
{ id: "my", label: "My Chat" },
{ id: "a2a", label: "Agent Comms" },
] as const
).map((t) => {
const on = tab === t.id;
return (
<button
key={t.id}
role="tab"
type="button"
tabIndex={on ? 0 : -1}
aria-selected={on}
onClick={() => setTab(t.id)}
onKeyDown={(e) => {
const tabs = ["my", "a2a"] as const;
const idx = tabs.indexOf(t.id);
let nextIdx: number | null = null;
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
nextIdx = (idx + 1) % tabs.length;
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
nextIdx = (idx - 1 + tabs.length) % tabs.length;
} else if (e.key === "Home") {
nextIdx = 0;
} else if (e.key === "End") {
nextIdx = tabs.length - 1;
}
if (nextIdx !== null) {
e.preventDefault();
setTab(tabs[nextIdx]!);
setTimeout(() => {
const btns = document.querySelectorAll('[role="tab"]');
(btns[nextIdx!] as HTMLButtonElement | null)?.focus();
}, 0);
}
}}
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
style={{
padding: "4px 0 8px",
marginTop: 12,
marginLeft: 4,
marginRight: 14,
border: "none",
background: "transparent",
fontSize: 13.5,
cursor: "pointer",
color: on ? p.text : p.text3,
fontWeight: on ? 600 : 500,
borderBottom: on ? `2px solid ${p.accent}` : "2px solid transparent",
}}
>
{t.label}
</button>
);
})}
</div>
{/* Messages */}
@@ -478,6 +507,7 @@ export function MobileChat({
onClick={() => {
loadInitial();
}}
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
style={{
padding: "6px 14px",
borderRadius: 14,
@@ -619,6 +649,7 @@ export function MobileChat({
type="button"
onClick={() => removePendingFile(i)}
aria-label={`Remove ${f.name}`}
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
style={{
border: "none",
background: "transparent",
@@ -659,6 +690,7 @@ export function MobileChat({
onClick={() => fileInputRef.current?.click()}
disabled={!reachable || sending || uploading}
aria-label="Attach"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
style={{
width: 32,
height: 32,
@@ -674,7 +706,7 @@ export function MobileChat({
opacity: !reachable || sending || uploading ? 0.4 : 1,
}}
>
{Icons.attach({ size: 16 })}
<span aria-hidden="true">{Icons.attach({ size: 16 })}</span>
</button>
<textarea
ref={composerRef}
@@ -719,6 +751,7 @@ export function MobileChat({
onClick={send}
disabled={(!draft.trim() && pendingFiles.length === 0) || !reachable || sending || uploading}
aria-label="Send"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
style={{
width: 36,
height: 36,
@@ -218,6 +218,7 @@ export function MobileComms({ dark }: { dark: boolean }) {
key={o.id}
type="button"
onClick={() => setFilter(o.id)}
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
style={{
display: "inline-flex",
alignItems: "center",
+32 -3
View File
@@ -83,12 +83,13 @@ export function MobileDetail({
type="button"
onClick={onBack}
aria-label="Back"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
style={iconButtonStyle(p, dark)}
>
{Icons.back({ size: 18 })}
<span aria-hidden="true">{Icons.back({ size: 18 })}</span>
</button>
<button type="button" aria-label="More" style={iconButtonStyle(p, dark)}>
{Icons.more({ size: 18 })}
<button type="button" aria-label="More" className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1" style={iconButtonStyle(p, dark)}>
<span aria-hidden="true">{Icons.more({ size: 18 })}</span>
</button>
</div>
</div>
@@ -168,6 +169,8 @@ export function MobileDetail({
{/* Tabs */}
<div
role="tablist"
aria-label="Agent detail sections"
style={{
display: "flex",
gap: 4,
@@ -181,8 +184,33 @@ export function MobileDetail({
return (
<button
key={t.id}
role="tab"
type="button"
tabIndex={on ? 0 : -1}
aria-selected={on}
onClick={() => setTab(t.id)}
onKeyDown={(e) => {
const idx = TABS.findIndex((x) => x.id === t.id);
let nextIdx: number | null = null;
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
nextIdx = (idx + 1) % TABS.length;
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
nextIdx = (idx - 1 + TABS.length) % TABS.length;
} else if (e.key === "Home") {
nextIdx = 0;
} else if (e.key === "End") {
nextIdx = TABS.length - 1;
}
if (nextIdx !== null) {
e.preventDefault();
setTab(TABS[nextIdx]!.id);
setTimeout(() => {
const btns = document.querySelectorAll('[role="tab"]');
(btns[nextIdx!] as HTMLButtonElement | null)?.focus();
}, 0);
}
}}
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
style={{
padding: "8px 14px",
borderRadius: 999,
@@ -215,6 +243,7 @@ export function MobileDetail({
type="button"
onClick={onChat}
data-testid="mobile-chat-cta"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2"
style={{
width: "100%",
height: 52,
+2 -1
View File
@@ -183,6 +183,7 @@ export function MobileHome({
type="button"
onClick={onSpawn}
aria-label="Spawn new agent"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2"
style={{
position: "absolute",
right: 24,
@@ -201,7 +202,7 @@ export function MobileHome({
boxShadow: "0 8px 24px rgba(40,30,20,0.25), 0 2px 6px rgba(40,30,20,0.15)",
}}
>
{Icons.plus({ size: 22 })}
<span aria-hidden="true">{Icons.plus({ size: 22 })}</span>
</button>
</div>
);
@@ -83,6 +83,7 @@ export function MobileMe({
type="button"
onClick={() => setAccent(c)}
aria-label={`Set accent ${c}`}
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
style={{
width: 36,
height: 36,
@@ -173,6 +174,7 @@ function SegmentedRow({
key={o.id}
type="button"
onClick={() => onChange(o.id)}
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
style={{
flex: 1,
padding: "10px 8px",
+6 -2
View File
@@ -148,6 +148,7 @@ export function MobileSpawn({ dark, onClose }: { dark: boolean; onClose: () => v
type="button"
onClick={onClose}
aria-label="Close"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
style={{
width: 32,
height: 32,
@@ -161,7 +162,7 @@ export function MobileSpawn({ dark, onClose }: { dark: boolean; onClose: () => v
justifyContent: "center",
}}
>
{Icons.close({ size: 16 })}
<span aria-hidden="true">{Icons.close({ size: 16 })}</span>
</button>
</div>
@@ -214,6 +215,7 @@ export function MobileSpawn({ dark, onClose }: { dark: boolean; onClose: () => v
setTplId(t.id);
setTier(tCode);
}}
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
style={{
background: on
? dark
@@ -286,7 +288,7 @@ export function MobileSpawn({ dark, onClose }: { dark: boolean; onClose: () => v
justifyContent: "center",
}}
>
{Icons.check({ size: 10, sw: 2.5 })}
<span aria-hidden="true">{Icons.check({ size: 10, sw: 2.5 })}</span>
</span>
)}
</button>
@@ -330,6 +332,7 @@ export function MobileSpawn({ dark, onClose }: { dark: boolean; onClose: () => v
key={t}
type="button"
onClick={() => setTier(t)}
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
style={{
flex: 1,
padding: "10px 8px",
@@ -377,6 +380,7 @@ export function MobileSpawn({ dark, onClose }: { dark: boolean; onClose: () => v
type="button"
onClick={handleSpawn}
disabled={busy || !tplId || templates.length === 0}
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2"
style={{
width: "100%",
height: 52,
@@ -291,6 +291,7 @@ export function AgentCard({
data-testid="workspace-card"
aria-label={`${agent.name}, status: ${agent.status}, tier ${agent.tier}${agent.remote ? ", remote" : ""}`}
onClick={onClick}
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
style={{
display: "block",
width: "100%",
@@ -444,6 +445,7 @@ export function FilterChips({
type="button"
aria-checked={on}
onClick={() => onChange(o.id)}
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1"
style={{
display: "inline-flex",
alignItems: "center",