code-coordinator 0.5.46__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. code_coordinator-0.5.46.dist-info/METADATA +625 -0
  2. code_coordinator-0.5.46.dist-info/RECORD +295 -0
  3. code_coordinator-0.5.46.dist-info/WHEEL +5 -0
  4. code_coordinator-0.5.46.dist-info/entry_points.txt +2 -0
  5. code_coordinator-0.5.46.dist-info/licenses/LICENSE +110 -0
  6. code_coordinator-0.5.46.dist-info/top_level.txt +1 -0
  7. coord/__init__.py +176 -0
  8. coord/_board_mapping.py +229 -0
  9. coord/acceptance.py +468 -0
  10. coord/acceptance_drivers.py +632 -0
  11. coord/agent.py +7517 -0
  12. coord/agent_app.py +1555 -0
  13. coord/agent_update.py +417 -0
  14. coord/agents/opencode/.gitignore +13 -0
  15. coord/agents/opencode/agents/work.md +129 -0
  16. coord/agents/opencode/routing.jsonc +49 -0
  17. coord/audit.py +301 -0
  18. coord/auto_loop.py +1440 -0
  19. coord/board_bool_guard.py +72 -0
  20. coord/board_service.py +141 -0
  21. coord/board_wire.py +309 -0
  22. coord/brain.py +581 -0
  23. coord/branch_model.py +214 -0
  24. coord/cargo_cache.py +258 -0
  25. coord/ci_github.py +386 -0
  26. coord/ci_store.py +560 -0
  27. coord/claim.py +353 -0
  28. coord/cli.py +454 -0
  29. coord/client.py +610 -0
  30. coord/commands/__init__.py +1 -0
  31. coord/commands/_common.py +329 -0
  32. coord/commands/acceptance.py +916 -0
  33. coord/commands/agent_ops.py +1339 -0
  34. coord/commands/audit.py +131 -0
  35. coord/commands/chat.py +320 -0
  36. coord/commands/dispatch.py +1780 -0
  37. coord/commands/dispatch_workers.py +4894 -0
  38. coord/commands/drive.py +616 -0
  39. coord/commands/drive_queue.py +1203 -0
  40. coord/commands/gate_a.py +217 -0
  41. coord/commands/gates.py +89 -0
  42. coord/commands/issues.py +681 -0
  43. coord/commands/lifecycle.py +513 -0
  44. coord/commands/merge.py +1900 -0
  45. coord/commands/milestone.py +2081 -0
  46. coord/commands/plan_followup.py +1243 -0
  47. coord/commands/plans.py +156 -0
  48. coord/commands/release.py +2232 -0
  49. coord/commands/report.py +341 -0
  50. coord/commands/review.py +1523 -0
  51. coord/commands/scorecard.py +252 -0
  52. coord/commands/sessions.py +1930 -0
  53. coord/commands/setup.py +576 -0
  54. coord/commands/status.py +2089 -0
  55. coord/commands/terminal.py +385 -0
  56. coord/commands/test_gate.py +775 -0
  57. coord/commands/tui.py +288 -0
  58. coord/comments.py +718 -0
  59. coord/config.py +3032 -0
  60. coord/conflict_fix.py +633 -0
  61. coord/dao.py +483 -0
  62. coord/dashboard/__init__.py +0 -0
  63. coord/dashboard/fixture.py +376 -0
  64. coord/dashboard/index.html +658 -0
  65. coord/dashboard/server.py +1894 -0
  66. coord/dashboard/terminal.py +382 -0
  67. coord/dashboard/webapp/.gitignore +9 -0
  68. coord/dashboard/webapp/components.json +17 -0
  69. coord/dashboard/webapp/dist/assets/Gallery-da3qNiIw.js +71 -0
  70. coord/dashboard/webapp/dist/assets/Terminal-9CEnUXvW.css +32 -0
  71. coord/dashboard/webapp/dist/assets/Terminal-skVFCxPU.js +63 -0
  72. coord/dashboard/webapp/dist/assets/index-DltfZR5f.js +184 -0
  73. coord/dashboard/webapp/dist/assets/index-Dq4kwTdw.css +1 -0
  74. coord/dashboard/webapp/dist/assets/workbox-window.prod.es5-BqEJf4Xk.js +2 -0
  75. coord/dashboard/webapp/dist/icons/icon-192.png +0 -0
  76. coord/dashboard/webapp/dist/icons/icon-512.png +0 -0
  77. coord/dashboard/webapp/dist/icons/icon.svg +5 -0
  78. coord/dashboard/webapp/dist/index.html +38 -0
  79. coord/dashboard/webapp/dist/manifest.webmanifest +1 -0
  80. coord/dashboard/webapp/dist/sw.js +1 -0
  81. coord/dashboard/webapp/dist/workbox-e4022e15.js +1 -0
  82. coord/dashboard/webapp/e2e/available-gates-terminal.spec.ts +75 -0
  83. coord/dashboard/webapp/e2e/deep-link.spec.ts +172 -0
  84. coord/dashboard/webapp/e2e/fixtureServer.ts +155 -0
  85. coord/dashboard/webapp/e2e/live-update-fixture.spec.ts +113 -0
  86. coord/dashboard/webapp/e2e/realtime.spec.ts +238 -0
  87. coord/dashboard/webapp/e2e/shell.spec.ts +309 -0
  88. coord/dashboard/webapp/e2e/smoke.spec.ts +191 -0
  89. coord/dashboard/webapp/e2e/terminal.spec.ts +420 -0
  90. coord/dashboard/webapp/e2e/theme.spec.ts +138 -0
  91. coord/dashboard/webapp/eslint.config.js +20 -0
  92. coord/dashboard/webapp/index.html +37 -0
  93. coord/dashboard/webapp/node_modules/flatted/python/flatted.py +144 -0
  94. coord/dashboard/webapp/package-lock.json +10584 -0
  95. coord/dashboard/webapp/package.json +63 -0
  96. coord/dashboard/webapp/playwright.acceptance.config.ts +166 -0
  97. coord/dashboard/webapp/playwright.config.ts +93 -0
  98. coord/dashboard/webapp/postcss.config.js +6 -0
  99. coord/dashboard/webapp/public/icons/icon-192.png +0 -0
  100. coord/dashboard/webapp/public/icons/icon-512.png +0 -0
  101. coord/dashboard/webapp/public/icons/icon.svg +5 -0
  102. coord/dashboard/webapp/src/App.tsx +140 -0
  103. coord/dashboard/webapp/src/api/client.ts +199 -0
  104. coord/dashboard/webapp/src/api/generated.ts +176 -0
  105. coord/dashboard/webapp/src/components/ConnectionBadge.tsx +52 -0
  106. coord/dashboard/webapp/src/components/Detail.tsx +800 -0
  107. coord/dashboard/webapp/src/components/Gallery.tsx +341 -0
  108. coord/dashboard/webapp/src/components/Home.tsx +435 -0
  109. coord/dashboard/webapp/src/components/MobileKeyBar.tsx +280 -0
  110. coord/dashboard/webapp/src/components/PanelHeader.tsx +59 -0
  111. coord/dashboard/webapp/src/components/PipelineCard.tsx +168 -0
  112. coord/dashboard/webapp/src/components/SessionCard.tsx +99 -0
  113. coord/dashboard/webapp/src/components/SessionDetail.tsx +140 -0
  114. coord/dashboard/webapp/src/components/SessionsList.tsx +81 -0
  115. coord/dashboard/webapp/src/components/Terminal.tsx +376 -0
  116. coord/dashboard/webapp/src/components/__tests__/ConnectionBadge.test.tsx +81 -0
  117. coord/dashboard/webapp/src/components/__tests__/Detail.test.tsx +680 -0
  118. coord/dashboard/webapp/src/components/__tests__/Gallery.test.tsx +83 -0
  119. coord/dashboard/webapp/src/components/__tests__/Home.test.tsx +271 -0
  120. coord/dashboard/webapp/src/components/__tests__/MobileKeyBar.test.tsx +197 -0
  121. coord/dashboard/webapp/src/components/__tests__/PipelineCard.test.tsx +143 -0
  122. coord/dashboard/webapp/src/components/__tests__/SessionCard.test.tsx +106 -0
  123. coord/dashboard/webapp/src/components/__tests__/Terminal.test.tsx +504 -0
  124. coord/dashboard/webapp/src/components/ui/badge.tsx +41 -0
  125. coord/dashboard/webapp/src/components/ui/button.tsx +54 -0
  126. coord/dashboard/webapp/src/components/ui/card.tsx +55 -0
  127. coord/dashboard/webapp/src/components/ui/dialog.tsx +99 -0
  128. coord/dashboard/webapp/src/components/ui/dropdown-menu.tsx +189 -0
  129. coord/dashboard/webapp/src/components/ui/empty-state.tsx +35 -0
  130. coord/dashboard/webapp/src/components/ui/sheet.tsx +123 -0
  131. coord/dashboard/webapp/src/components/ui/skeleton.tsx +9 -0
  132. coord/dashboard/webapp/src/components/ui/tabs.tsx +55 -0
  133. coord/dashboard/webapp/src/components/ui/theme-provider.tsx +78 -0
  134. coord/dashboard/webapp/src/components/ui/theme-toggle.tsx +20 -0
  135. coord/dashboard/webapp/src/components/ui/toast.tsx +123 -0
  136. coord/dashboard/webapp/src/components/ui/toaster.tsx +30 -0
  137. coord/dashboard/webapp/src/components/ui/tooltip.tsx +26 -0
  138. coord/dashboard/webapp/src/components/ui/use-toast.ts +134 -0
  139. coord/dashboard/webapp/src/index.css +210 -0
  140. coord/dashboard/webapp/src/lib/pipeline.ts +29 -0
  141. coord/dashboard/webapp/src/lib/utils.ts +6 -0
  142. coord/dashboard/webapp/src/main.tsx +46 -0
  143. coord/dashboard/webapp/src/realtime/RealtimeProvider.tsx +112 -0
  144. coord/dashboard/webapp/src/realtime/__tests__/RealtimeProvider.test.tsx +189 -0
  145. coord/dashboard/webapp/src/realtime/__tests__/connection.test.ts +255 -0
  146. coord/dashboard/webapp/src/realtime/connection.ts +227 -0
  147. coord/dashboard/webapp/src/realtime/events.ts +100 -0
  148. coord/dashboard/webapp/src/routes/__tests__/paths.test.ts +92 -0
  149. coord/dashboard/webapp/src/routes/paths.ts +92 -0
  150. coord/dashboard/webapp/src/shell/ActivityRail.tsx +335 -0
  151. coord/dashboard/webapp/src/shell/AppShell.tsx +276 -0
  152. coord/dashboard/webapp/src/shell/ComingSoon.tsx +33 -0
  153. coord/dashboard/webapp/src/shell/EmptyDetail.tsx +26 -0
  154. coord/dashboard/webapp/src/shell/RouteNotFound.tsx +33 -0
  155. coord/dashboard/webapp/src/shell/ShellLayout.tsx +147 -0
  156. coord/dashboard/webapp/src/shell/StatusBar.tsx +46 -0
  157. coord/dashboard/webapp/src/shell/__tests__/ShellLayout.test.tsx +520 -0
  158. coord/dashboard/webapp/src/shell/__tests__/shellState.test.ts +95 -0
  159. coord/dashboard/webapp/src/shell/__tests__/stubViewport.ts +40 -0
  160. coord/dashboard/webapp/src/shell/breakpoints.ts +87 -0
  161. coord/dashboard/webapp/src/shell/railItems.ts +105 -0
  162. coord/dashboard/webapp/src/shell/shellState.ts +174 -0
  163. coord/dashboard/webapp/src/shell/useRegionFocus.ts +95 -0
  164. coord/dashboard/webapp/src/test-setup.ts +41 -0
  165. coord/dashboard/webapp/src/vite-env.d.ts +2 -0
  166. coord/dashboard/webapp/tailwind.config.js +140 -0
  167. coord/dashboard/webapp/tsconfig.json +25 -0
  168. coord/dashboard/webapp/tsconfig.node.json +11 -0
  169. coord/dashboard/webapp/vite.config.ts +71 -0
  170. coord/db.py +1076 -0
  171. coord/dead_end.py +332 -0
  172. coord/deploy/README.md +33 -0
  173. coord/deploy/coord-agent.service +89 -0
  174. coord/deploy/coord-db-backup.service +60 -0
  175. coord/deploy/coord-db-backup.sh +74 -0
  176. coord/deploy/coord-db-backup.timer +18 -0
  177. coord/deploy/coord-drive-queue.service +117 -0
  178. coord/deploy/coord-drive-queue.timer +39 -0
  179. coord/deploy/coord-notify.service +48 -0
  180. coord/deploy/coord-notify.timer +24 -0
  181. coord/deploy/coord-release-propagate.service +83 -0
  182. coord/deploy/coord-release-propagate.timer +38 -0
  183. coord/deploy/coord-release-window.service +119 -0
  184. coord/deploy/coord-release-window.timer +36 -0
  185. coord/deploy/coord-serve.service +82 -0
  186. coord/deploy/coord-web-dist-build.service +43 -0
  187. coord/deploy/coord-web-dist-build.timer +36 -0
  188. coord/deploy/coord-web.service +125 -0
  189. coord/deploy_manifest.py +80 -0
  190. coord/deploy_units.py +384 -0
  191. coord/deps.py +115 -0
  192. coord/diagnose.py +1623 -0
  193. coord/dispatch.py +1009 -0
  194. coord/dist_name.py +123 -0
  195. coord/drive.py +3101 -0
  196. coord/drive_queue.py +2298 -0
  197. coord/drive_state.py +870 -0
  198. coord/events.py +381 -0
  199. coord/failure_class.py +914 -0
  200. coord/filelock.py +168 -0
  201. coord/fleet_config_health.py +300 -0
  202. coord/freshness.py +206 -0
  203. coord/gate_a.py +469 -0
  204. coord/gate_b.py +411 -0
  205. coord/gate_snapshot.py +385 -0
  206. coord/gates.py +582 -0
  207. coord/github_ops.py +1954 -0
  208. coord/goal.py +125 -0
  209. coord/graph_health.py +348 -0
  210. coord/health/__init__.py +69 -0
  211. coord/health/aggregate.py +129 -0
  212. coord/health/checks/__init__.py +13 -0
  213. coord/health/checks/agent_install.py +280 -0
  214. coord/health/checks/cargo_targets.py +171 -0
  215. coord/health/checks/claude_binary.py +65 -0
  216. coord/health/checks/deploy_lane_facts.py +458 -0
  217. coord/health/checks/disk.py +99 -0
  218. coord/health/checks/fleet_board.py +89 -0
  219. coord/health/checks/fleet_deploy_lanes.py +469 -0
  220. coord/health/checks/fleet_phantom.py +69 -0
  221. coord/health/checks/fleet_unit_drift.py +151 -0
  222. coord/health/checks/graph.py +192 -0
  223. coord/health/checks/plan_usage.py +88 -0
  224. coord/health/checks/repo_state.py +161 -0
  225. coord/health/checks/spawned_coord.py +465 -0
  226. coord/health/checks/timer_active.py +254 -0
  227. coord/health/checks/toolchain.py +547 -0
  228. coord/health/checks/unit_drift.py +648 -0
  229. coord/health/checks/unit_enablement.py +171 -0
  230. coord/health/checks/worktrees.py +96 -0
  231. coord/health/cli.py +121 -0
  232. coord/health/context.py +106 -0
  233. coord/health/fleet_snapshot.py +477 -0
  234. coord/health/models.py +250 -0
  235. coord/health/pypi.py +231 -0
  236. coord/health/registry.py +240 -0
  237. coord/health/render.py +82 -0
  238. coord/health/units.py +60 -0
  239. coord/hooks.py +106 -0
  240. coord/housekeeping.py +204 -0
  241. coord/interactive.py +4286 -0
  242. coord/issue_store.py +1496 -0
  243. coord/liveness_auditor.py +293 -0
  244. coord/machine_pause.py +755 -0
  245. coord/merge_queue.py +4681 -0
  246. coord/milestone_chat.py +600 -0
  247. coord/milestone_dispatch.py +943 -0
  248. coord/milestone_gate.py +709 -0
  249. coord/milestone_order.py +840 -0
  250. coord/mock_author.py +334 -0
  251. coord/models.py +891 -0
  252. coord/network.py +269 -0
  253. coord/new_issue_chat.py +229 -0
  254. coord/notify.py +3226 -0
  255. coord/openapi.py +404 -0
  256. coord/overlap_fence.py +133 -0
  257. coord/parentage.py +200 -0
  258. coord/parentage_github.py +58 -0
  259. coord/pipeline.py +481 -0
  260. coord/plan_parser.py +266 -0
  261. coord/plans.py +543 -0
  262. coord/platform_paths.py +43 -0
  263. coord/pr_body_lint.py +67 -0
  264. coord/prereqs.py +533 -0
  265. coord/progress.py +425 -0
  266. coord/providers/__init__.py +683 -0
  267. coord/providers/base.py +218 -0
  268. coord/providers/claude.py +284 -0
  269. coord/providers/claude_pty.py +610 -0
  270. coord/providers/opencode.py +896 -0
  271. coord/reconcile.py +2233 -0
  272. coord/refine_chat.py +485 -0
  273. coord/release_cordon.py +525 -0
  274. coord/release_propagate.py +1176 -0
  275. coord/release_verify.py +777 -0
  276. coord/release_window.py +322 -0
  277. coord/reports.py +1643 -0
  278. coord/revalidate.py +1101 -0
  279. coord/review.py +3317 -0
  280. coord/scorecard.py +484 -0
  281. coord/serve_app.py +7192 -0
  282. coord/skills/update-issue/SKILL.md +93 -0
  283. coord/smoke.py +1030 -0
  284. coord/split_work.py +210 -0
  285. coord/stage_projection.py +650 -0
  286. coord/state.py +5720 -0
  287. coord/test_author.py +1064 -0
  288. coord/test_chat.py +352 -0
  289. coord/test_orchestrator.py +494 -0
  290. coord/test_report.py +178 -0
  291. coord/tui_release.py +271 -0
  292. coord/usage.py +753 -0
  293. coord/usage_limits.py +358 -0
  294. coord/usage_rollup.py +709 -0
  295. coord/worker_events.py +954 -0
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Whole-app 404 (#1548) — what a URL that matches no route in the tree at
3
+ * all renders, e.g. a typo'd path or a link to a feature that was removed.
4
+ *
5
+ * Distinct from `Detail`'s own not-found state (a syntactically valid
6
+ * `/pipeline/:repo/:issue` whose issue isn't in today's pipeline — a *stale*
7
+ * link, not an *unknown* one): that one already exists per-item and knows
8
+ * the repo/issue it failed to find. This one is the catch-all `*` route
9
+ * inside `ShellLayout`, so it still renders inside the shell (rail, status
10
+ * bar) rather than a blank document — "a real not-found state, not a blank
11
+ * panel or a crash" per the story's acceptance criteria.
12
+ */
13
+ import { Compass } from 'lucide-react'
14
+ import { Link } from 'react-router-dom'
15
+ import { paths } from '@/routes/paths'
16
+
17
+ export function RouteNotFound() {
18
+ return (
19
+ <div className="flex h-full flex-col items-center justify-center gap-3 px-6 py-12 text-center">
20
+ <Compass className="h-7 w-7 text-faint" aria-hidden="true" />
21
+ <p className="text-sm font-medium text-foreground">Page not found</p>
22
+ <p className="max-w-xs text-xs text-muted-foreground">
23
+ There's nothing at this address.
24
+ </p>
25
+ <Link
26
+ to={paths.pipeline()}
27
+ className="mt-1 rounded-full border border-border px-3 py-1 text-xs text-muted-foreground hover:text-foreground"
28
+ >
29
+ Go to Pipeline
30
+ </Link>
31
+ </div>
32
+ )
33
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * ShellLayout (#1547, routed by #1548) — the react-router *layout route*
3
+ * that composes the shell, and the single place that decides what goes in
4
+ * which slot.
5
+ *
6
+ * The composition is deliberately boring, because that is the acceptance
7
+ * criterion: there is no `isMobile ? <PhoneHome/> : <DesktopHome/>`. `list`
8
+ * and `detail` are computed once and handed to `AppShell` unchanged; the only
9
+ * thing `mode` decides is where they land and which of them is mounted.
10
+ *
11
+ * The detail slot is `<Outlet/>` — so the child route (`/pipeline` ->
12
+ * `EmptyDetail`, `/pipeline/:repo/:issue[/:tab]` -> `Detail`,
13
+ * `/sessions/:id` -> `SessionDetail`, an unbuilt panel's own path ->
14
+ * `ComingSoon`, anything else -> `RouteNotFound`) fills it.
15
+ *
16
+ * The rail selection is derived from the URL (`shellViewFromPath`), not from
17
+ * persisted state: that was the explicit deferral #1547 made ("routing and
18
+ * deep links are the next story"), and this *is* that story. A URL is now
19
+ * the single source of truth for "what view am I looking at" — reload,
20
+ * bookmark, paste into Slack, browser back — all of it falls out of normal
21
+ * router behaviour instead of a second, URL-independent state machine that
22
+ * could disagree with the address bar.
23
+ */
24
+ import { Outlet, useLocation, useMatch, useNavigate } from 'react-router-dom'
25
+ import { useCallback, type ReactNode } from 'react'
26
+ import { useQuery } from '@tanstack/react-query'
27
+
28
+ import Home from '@/components/Home'
29
+ import SessionsList from '@/components/SessionsList'
30
+ import { fetchPipeline, fetchSessions } from '@/api/client'
31
+ import { isActive, needsMe } from '@/lib/pipeline'
32
+ import { RAIL_VIEW_PATH, shellViewFromPath } from '@/routes/paths'
33
+ import { AppShell } from './AppShell'
34
+ import { ActivityRail } from './ActivityRail'
35
+ import { ComingSoon } from './ComingSoon'
36
+ import { RouteNotFound } from './RouteNotFound'
37
+ import { StatusBar } from './StatusBar'
38
+ import { useShellMode } from './breakpoints'
39
+ import { useShellState, type ShellView } from './shellState'
40
+ import { useRegionFocus, type ShellRegion } from './useRegionFocus'
41
+
42
+ const ATTENTION_VIEWS: ReadonlySet<ShellView> = new Set<ShellView>(['pipeline'])
43
+
44
+ export function ShellLayout() {
45
+ const mode = useShellMode()
46
+ const shell = useShellState()
47
+ const navigate = useNavigate()
48
+ const location = useLocation()
49
+
50
+ // Which rail entry the current URL belongs to — `null` for a path with no
51
+ // owning view (an unmatched route; root `/` never reaches here, see
52
+ // `App.tsx`'s redirect).
53
+ const currentView = shellViewFromPath(location.pathname)
54
+
55
+ // The detail slot is "active" (narrow: shown instead of the list; medium:
56
+ // overlaid on it) exactly when the route names a specific item — a
57
+ // pipeline issue (with or without a tab segment) or a session. Matched
58
+ // explicitly rather than inferred from `currentView`, because "there is an
59
+ // Outlet route that fills the detail slot" and "which rail item is lit up"
60
+ // are genuinely different questions once there's more than one detail-
61
+ // capable list.
62
+ const pipelineItemMatch = useMatch('/pipeline/:repo/:issue')
63
+ const pipelineItemTabMatch = useMatch('/pipeline/:repo/:issue/:tab')
64
+ const sessionItemMatch = useMatch('/sessions/:id')
65
+ const detailActive = !!(pipelineItemMatch || pipelineItemTabMatch || sessionItemMatch)
66
+
67
+ // Same query keys the panels use, so this is a cache read, not a second
68
+ // fetch (see main.tsx: staleTime Infinity + SSE-driven invalidation).
69
+ const { data: pipeline } = useQuery({ queryKey: ['pipeline'], queryFn: fetchPipeline })
70
+ const { data: sessions } = useQuery({ queryKey: ['sessions'], queryFn: fetchSessions })
71
+
72
+ const inFlight = pipeline?.filter(isActive).length
73
+ const attention = pipeline?.some(needsMe) ?? false
74
+ // `machine` is nullable on SessionInfo (a session on a host the roster
75
+ // hasn't resolved yet) — a `null` in the status bar would render as an empty
76
+ // segment between two separators.
77
+ const machines = sessions
78
+ ? Array.from(new Set(sessions.map((s) => s.machine).filter((m): m is string => !!m))).sort()
79
+ : []
80
+
81
+ // Kept in step with AppShell's own `showList` / `showDetail` — F6 must not
82
+ // offer a region that isn't mounted.
83
+ const listMounted = mode === 'narrow' ? !detailActive : !(mode === 'wide' && shell.listCollapsed)
84
+ const detailMounted = mode === 'wide' || detailActive
85
+
86
+ // F6 must skip regions that aren't on screen — cycling into an unmounted
87
+ // container would silently drop focus to <body>.
88
+ const visibleRegions: ShellRegion[] = [
89
+ 'rail' as const,
90
+ ...(listMounted ? (['list'] as const) : []),
91
+ ...(detailMounted ? (['detail'] as const) : []),
92
+ ]
93
+ const { focusedRegion, registerRegion } = useRegionFocus(visibleRegions)
94
+
95
+ const handleSelectView = useCallback(
96
+ (view: ShellView) => {
97
+ const path = RAIL_VIEW_PATH[view]
98
+ // Every 'ready' rail entry has a path; a 'soon' entry never reaches
99
+ // here (ActivityRail doesn't wire onClick for it). The guard is for
100
+ // type honesty (`RAIL_VIEW_PATH` is a `Partial`), not a real runtime case.
101
+ if (path) navigate(path)
102
+ },
103
+ [navigate],
104
+ )
105
+
106
+ let list: ReactNode
107
+ if (currentView === 'pipeline') {
108
+ list = <Home />
109
+ } else if (currentView === 'sessions') {
110
+ list = <SessionsList />
111
+ } else if (currentView === null) {
112
+ list = <RouteNotFound />
113
+ } else {
114
+ list = <ComingSoon view={currentView} />
115
+ }
116
+
117
+ return (
118
+ <AppShell
119
+ mode={mode}
120
+ railCollapsed={shell.railCollapsed}
121
+ listCollapsed={shell.listCollapsed}
122
+ listWidthPx={shell.listWidthPx}
123
+ onListWidthChange={shell.setListWidthPx}
124
+ detailActive={detailActive}
125
+ registerRegion={registerRegion}
126
+ focusedRegion={focusedRegion}
127
+ rail={
128
+ <ActivityRail
129
+ mode={mode}
130
+ collapsed={shell.railCollapsed}
131
+ view={currentView}
132
+ onSelect={handleSelectView}
133
+ onToggleCollapsed={shell.toggleRail}
134
+ listCollapsed={shell.listCollapsed}
135
+ onToggleList={shell.toggleList}
136
+ counts={{ pipeline: inFlight, sessions: sessions?.length }}
137
+ attentionViews={attention ? ATTENTION_VIEWS : undefined}
138
+ regionRef={registerRegion('rail')}
139
+ regionFocused={focusedRegion === 'rail'}
140
+ />
141
+ }
142
+ list={list}
143
+ detail={<Outlet />}
144
+ status={<StatusBar inFlight={inFlight} sessions={sessions?.length} machines={machines} />}
145
+ />
146
+ )
147
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shell status bar (#1547) — the mock's `.statusbar`, promoted from inside
3
+ * `.main` to a full-width foot row so it is one element at every breakpoint
4
+ * instead of appearing and disappearing with the detail column.
5
+ *
6
+ * It is also now the app's *only* `ConnectionBadge`. It used to be rendered
7
+ * once per screen (Home and Detail each had their own); with wide showing both
8
+ * at once that would be two live-region `role="status"` nodes saying the same
9
+ * thing, which is noise for a screen reader and an ambiguous locator for a
10
+ * test.
11
+ *
12
+ * Everything else here is mono, because everything else here is a value the
13
+ * machine owns. Nothing is invented: counts come from the same React-Query
14
+ * caches the panels read, and a figure with no data source (the mock's
15
+ * "$14.82 today") is simply absent until Spend lands.
16
+ */
17
+ import { ConnectionBadge } from '@/components/ConnectionBadge'
18
+
19
+ export interface StatusBarProps {
20
+ /** Pipeline items not yet merged. */
21
+ inFlight?: number
22
+ /** Live interactive sessions. */
23
+ sessions?: number
24
+ /** Distinct machines those sessions are on. */
25
+ machines?: string[]
26
+ }
27
+
28
+ export function StatusBar({ inFlight, sessions, machines }: StatusBarProps) {
29
+ return (
30
+ <footer
31
+ aria-label="Status"
32
+ className="flex items-center gap-4 overflow-x-auto whitespace-nowrap border-t border-border bg-surface px-3 py-1.5 font-mono text-[.69rem] text-faint md:px-5"
33
+ >
34
+ <ConnectionBadge />
35
+ {inFlight != null && <span>{inFlight} in flight</span>}
36
+ {sessions != null && (
37
+ <span>
38
+ {sessions} {sessions === 1 ? 'session' : 'sessions'}
39
+ </span>
40
+ )}
41
+ {machines && machines.length > 0 && (
42
+ <span className="hidden md:inline">{machines.join(' · ')}</span>
43
+ )}
44
+ </footer>
45
+ )
46
+ }