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,87 @@
1
+ /**
2
+ * Responsive breakpoints for the app shell (#1547).
3
+ *
4
+ * Three modes, not two — the story asks explicitly that mid widths "degrade
5
+ * sensibly rather than snapping between two extremes":
6
+ *
7
+ * narrow (< 768px) the phone app, preserved verbatim: one column,
8
+ * bottom nav, drill-in navigation.
9
+ * medium (768–1023px) rail collapses to its 60px icon strip, the list
10
+ * keeps the whole content column, and the detail
11
+ * arrives as an overlay above it (still a drill-in,
12
+ * just without losing the list underneath).
13
+ * wide (>= 1024px) the `SidebarView` twin: rail + list + detail, all
14
+ * three visible at once.
15
+ *
16
+ * The story's stated wide threshold is 1024px and the Gate-A mock
17
+ * (docs/mocks/web/pipeline-wide.html) switches to its bottom-row layout at
18
+ * 900px. Those aren't in conflict once there are three modes: 1024 is where
19
+ * the third column appears, and the phone layout is kept for genuinely
20
+ * phone-sized viewports (< 768) rather than for every tablet.
21
+ *
22
+ * Default when `matchMedia` is unavailable (jsdom, SSR) is `narrow` — the
23
+ * conservative choice, since that's the layout that is in daily use and the
24
+ * one every pre-existing component test was written against.
25
+ */
26
+ import { useEffect, useState } from 'react'
27
+
28
+ export const BREAKPOINT_MEDIUM_PX = 768
29
+ export const BREAKPOINT_WIDE_PX = 1024
30
+
31
+ export type ShellMode = 'narrow' | 'medium' | 'wide'
32
+
33
+ const WIDE_QUERY = `(min-width: ${BREAKPOINT_WIDE_PX}px)`
34
+ const MEDIUM_QUERY = `(min-width: ${BREAKPOINT_MEDIUM_PX}px)`
35
+
36
+ function readMode(): ShellMode {
37
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
38
+ return 'narrow'
39
+ }
40
+ if (window.matchMedia(WIDE_QUERY).matches) return 'wide'
41
+ if (window.matchMedia(MEDIUM_QUERY).matches) return 'medium'
42
+ return 'narrow'
43
+ }
44
+
45
+ /**
46
+ * Current shell mode, recomputed whenever either breakpoint is crossed.
47
+ *
48
+ * The initial value is computed synchronously in the `useState` initialiser
49
+ * (not in an effect) so a wide browser's very first paint is already the wide
50
+ * layout — no one-frame flash of the phone app on a 32" monitor.
51
+ */
52
+ export function useShellMode(): ShellMode {
53
+ const [mode, setMode] = useState<ShellMode>(readMode)
54
+
55
+ useEffect(() => {
56
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return
57
+
58
+ const wide = window.matchMedia(WIDE_QUERY)
59
+ const medium = window.matchMedia(MEDIUM_QUERY)
60
+ const sync = () => setMode(readMode())
61
+
62
+ // Re-sync once on mount: between the render-time read above and this
63
+ // effect, the viewport may have changed (or matchMedia may have been
64
+ // stubbed in late, as the test setup does).
65
+ sync()
66
+
67
+ // `addListener`/`removeListener` are the deprecated MediaQueryList API;
68
+ // some of the older WebKit builds this PWA is installed on (iOS < 14)
69
+ // never got `addEventListener` on MediaQueryList, so fall back.
70
+ const subscribe = (mql: MediaQueryList) => {
71
+ if (typeof mql.addEventListener === 'function') {
72
+ mql.addEventListener('change', sync)
73
+ return () => mql.removeEventListener('change', sync)
74
+ }
75
+ if (typeof mql.addListener === 'function') {
76
+ mql.addListener(sync)
77
+ return () => mql.removeListener(sync)
78
+ }
79
+ return () => {}
80
+ }
81
+
82
+ const unsubscribes = [subscribe(wide), subscribe(medium)]
83
+ return () => unsubscribes.forEach((fn) => fn())
84
+ }, [])
85
+
86
+ return mode
87
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The activity rail's contents (#1547).
3
+ *
4
+ * "Panels not yet built are visibly *coming*, not silently absent — the rail
5
+ * is the program's own progress bar." So this list is the whole planned M-W1
6
+ * surface, and each entry carries its own truth about whether it exists yet.
7
+ * A `'soon'` entry renders dimmed with a `soon` pill and is not activatable
8
+ * (the Gate-A mock, docs/mocks/web/pipeline-wide.html, gives them
9
+ * `opacity:.42` and `cursor:default`); flipping one to `'ready'` when its
10
+ * story lands is a one-line change here plus a case in `ShellLayout`.
11
+ *
12
+ * Grouping follows the mock exactly: an ungrouped top block, then Flow, then
13
+ * Insight, with Theme / Settings / Collapse pinned to the rail foot.
14
+ */
15
+ import {
16
+ BarChart3,
17
+ GitMerge,
18
+ LayoutDashboard,
19
+ Milestone,
20
+ ScrollText,
21
+ Server,
22
+ SquareTerminal,
23
+ Terminal,
24
+ Workflow,
25
+ type LucideIcon,
26
+ } from 'lucide-react'
27
+
28
+ import type { ShellView } from './shellState'
29
+
30
+ export type RailItemStatus = 'ready' | 'soon'
31
+
32
+ export interface RailItem {
33
+ id: ShellView
34
+ label: string
35
+ icon: LucideIcon
36
+ status: RailItemStatus
37
+ /** Group heading this item sits under; `undefined` = the top block. */
38
+ group?: string
39
+ /** Shown on hover / in the `soon` pill's tooltip. */
40
+ hint?: string
41
+ }
42
+
43
+ export const RAIL_ITEMS: readonly RailItem[] = [
44
+ { id: 'pipeline', label: 'Pipeline', icon: Workflow, status: 'ready' },
45
+ { id: 'board', label: 'Board', icon: LayoutDashboard, status: 'soon', hint: 'Board panel — M-W2' },
46
+ { id: 'sessions', label: 'Sessions', icon: SquareTerminal, status: 'ready' },
47
+ {
48
+ id: 'terminal',
49
+ label: 'Terminal',
50
+ icon: Terminal,
51
+ status: 'soon',
52
+ hint: 'Standalone terminal panel — open one from Sessions for now',
53
+ },
54
+ { id: 'machines', label: 'Machines', icon: Server, status: 'soon', hint: 'Machines panel — M-W2' },
55
+
56
+ {
57
+ id: 'merge-queue',
58
+ label: 'Merge queue',
59
+ icon: GitMerge,
60
+ status: 'soon',
61
+ group: 'Flow',
62
+ hint: 'Merge queue panel — M-W2',
63
+ },
64
+ {
65
+ id: 'milestones',
66
+ label: 'Milestones',
67
+ icon: Milestone,
68
+ status: 'soon',
69
+ group: 'Flow',
70
+ hint: 'Milestone DAG — M-W3',
71
+ },
72
+
73
+ {
74
+ id: 'audit',
75
+ label: 'Audit',
76
+ icon: ScrollText,
77
+ status: 'soon',
78
+ group: 'Insight',
79
+ hint: 'Audit trail — milestone #33',
80
+ },
81
+ {
82
+ id: 'spend',
83
+ label: 'Spend',
84
+ icon: BarChart3,
85
+ status: 'soon',
86
+ group: 'Insight',
87
+ hint: 'Spend & time observability — milestone #37',
88
+ },
89
+ ] as const
90
+
91
+ /**
92
+ * Rail items in render order, split into their heading groups. Computed once
93
+ * at module scope: `RAIL_ITEMS` is a constant, so recomputing per render would
94
+ * be pure waste and would break `React.memo` on any consumer.
95
+ */
96
+ export const RAIL_GROUPS: ReadonlyArray<{ heading?: string; items: RailItem[] }> =
97
+ RAIL_ITEMS.reduce<Array<{ heading?: string; items: RailItem[] }>>((groups, item) => {
98
+ const last = groups[groups.length - 1]
99
+ if (last && last.heading === item.group) {
100
+ last.items.push(item)
101
+ } else {
102
+ groups.push({ heading: item.group, items: [item] })
103
+ }
104
+ return groups
105
+ }, [])
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Persisted shell geometry (#1547, trimmed by #1548).
3
+ *
4
+ * "Panel sizes survive a reload" is an acceptance criterion, so geometry
5
+ * lives in one localStorage blob under a single key, written on every change
6
+ * and read once at mount. Reads and writes are individually guarded:
7
+ * `localStorage` throws outright in Safari private browsing and in lockdown
8
+ * modes, and a shell that refuses to render because it couldn't remember a
9
+ * panel width would be a worse bug than forgetting the width. Same posture
10
+ * (and same key prefix) as `ui/theme-provider.tsx`.
11
+ *
12
+ * The *selected view* used to live here too, decoupled from the URL on
13
+ * purpose because routing was explicitly the next story. #1548 is that
14
+ * story: the rail selection, the selected pipeline item and the selected
15
+ * session are now derived from the URL (see `src/routes/paths.ts`'s
16
+ * `shellViewFromPath`) so they are addressable, bookmarkable and survive a
17
+ * reload *because the URL does* — no separate persistence needed, and none
18
+ * left here to go stale against a bookmarked link. What's left is exactly
19
+ * the class of state the story says must *not* leak into the URL: panel
20
+ * geometry that has no meaning to anyone reading the link over your
21
+ * shoulder.
22
+ */
23
+ import { useCallback, useState } from 'react'
24
+
25
+ export const SHELL_STORAGE_KEY = 'coord-web-shell'
26
+
27
+ /** Every entry the activity rail can show, built or not. */
28
+ export type ShellView =
29
+ | 'pipeline'
30
+ | 'board'
31
+ | 'sessions'
32
+ | 'terminal'
33
+ | 'machines'
34
+ | 'merge-queue'
35
+ | 'milestones'
36
+ | 'audit'
37
+ | 'spend'
38
+ | 'settings'
39
+
40
+ export const LIST_WIDTH_MIN_PX = 260
41
+ export const LIST_WIDTH_MAX_PX = 640
42
+ export const LIST_WIDTH_DEFAULT_PX = 360
43
+
44
+ export const RAIL_WIDTH_PX = 216
45
+ export const RAIL_WIDTH_COLLAPSED_PX = 60
46
+
47
+ export interface ShellState {
48
+ railCollapsed: boolean
49
+ listCollapsed: boolean
50
+ listWidthPx: number
51
+ }
52
+
53
+ const DEFAULT_STATE: ShellState = {
54
+ railCollapsed: false,
55
+ listCollapsed: false,
56
+ listWidthPx: LIST_WIDTH_DEFAULT_PX,
57
+ }
58
+
59
+ export function clampListWidth(px: number): number {
60
+ if (!Number.isFinite(px)) return LIST_WIDTH_DEFAULT_PX
61
+ return Math.min(LIST_WIDTH_MAX_PX, Math.max(LIST_WIDTH_MIN_PX, Math.round(px)))
62
+ }
63
+
64
+ /**
65
+ * Parse a persisted blob field-by-field rather than trusting its shape.
66
+ * The key is user-writable (devtools) and, more practically, will be read by
67
+ * future builds after this schema grows — an unknown or half-written value
68
+ * must degrade to the default, never to `NaN` grid columns.
69
+ *
70
+ * A blob written by a pre-#1548 build still has a `view` field in it — it's
71
+ * simply ignored below rather than special-cased, the same way any other
72
+ * stale/unknown key would be.
73
+ */
74
+ export function parseShellState(raw: string | null): ShellState {
75
+ if (!raw) return DEFAULT_STATE
76
+ let parsed: unknown
77
+ try {
78
+ parsed = JSON.parse(raw)
79
+ } catch {
80
+ return DEFAULT_STATE
81
+ }
82
+ if (typeof parsed !== 'object' || parsed === null) return DEFAULT_STATE
83
+ const obj = parsed as Record<string, unknown>
84
+
85
+ return {
86
+ railCollapsed:
87
+ typeof obj.railCollapsed === 'boolean' ? obj.railCollapsed : DEFAULT_STATE.railCollapsed,
88
+ listCollapsed:
89
+ typeof obj.listCollapsed === 'boolean' ? obj.listCollapsed : DEFAULT_STATE.listCollapsed,
90
+ listWidthPx:
91
+ typeof obj.listWidthPx === 'number'
92
+ ? clampListWidth(obj.listWidthPx)
93
+ : DEFAULT_STATE.listWidthPx,
94
+ }
95
+ }
96
+
97
+ function readShellState(): ShellState {
98
+ if (typeof window === 'undefined') return DEFAULT_STATE
99
+ try {
100
+ return parseShellState(window.localStorage.getItem(SHELL_STORAGE_KEY))
101
+ } catch {
102
+ return DEFAULT_STATE
103
+ }
104
+ }
105
+
106
+ function writeShellState(state: ShellState): void {
107
+ if (typeof window === 'undefined') return
108
+ try {
109
+ window.localStorage.setItem(SHELL_STORAGE_KEY, JSON.stringify(state))
110
+ } catch {
111
+ // Persistence is a nice-to-have; the layout still applies this session.
112
+ }
113
+ }
114
+
115
+ export interface UseShellState extends ShellState {
116
+ setRailCollapsed: (collapsed: boolean) => void
117
+ toggleRail: () => void
118
+ setListCollapsed: (collapsed: boolean) => void
119
+ toggleList: () => void
120
+ setListWidthPx: (px: number) => void
121
+ }
122
+
123
+ export function useShellState(): UseShellState {
124
+ const [state, setState] = useState<ShellState>(readShellState)
125
+
126
+ const update = useCallback((patch: Partial<ShellState>) => {
127
+ setState((prev) => {
128
+ const next = { ...prev, ...patch }
129
+ // Cheap identity guard: a pointer-drag resize fires many times a second
130
+ // and would otherwise re-render (and re-serialise) on every no-op move.
131
+ if (
132
+ next.railCollapsed === prev.railCollapsed &&
133
+ next.listCollapsed === prev.listCollapsed &&
134
+ next.listWidthPx === prev.listWidthPx
135
+ ) {
136
+ return prev
137
+ }
138
+ writeShellState(next)
139
+ return next
140
+ })
141
+ }, [])
142
+
143
+ return {
144
+ ...state,
145
+ setRailCollapsed: useCallback(
146
+ (railCollapsed: boolean) => update({ railCollapsed }),
147
+ [update],
148
+ ),
149
+ toggleRail: useCallback(
150
+ () => setState((prev) => {
151
+ const next = { ...prev, railCollapsed: !prev.railCollapsed }
152
+ writeShellState(next)
153
+ return next
154
+ }),
155
+ [],
156
+ ),
157
+ setListCollapsed: useCallback(
158
+ (listCollapsed: boolean) => update({ listCollapsed }),
159
+ [update],
160
+ ),
161
+ toggleList: useCallback(
162
+ () => setState((prev) => {
163
+ const next = { ...prev, listCollapsed: !prev.listCollapsed }
164
+ writeShellState(next)
165
+ return next
166
+ }),
167
+ [],
168
+ ),
169
+ setListWidthPx: useCallback(
170
+ (px: number) => update({ listWidthPx: clampListWidth(px) }),
171
+ [update],
172
+ ),
173
+ }
174
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Keyboard region cycling for the shell (#1547) — the web-idiomatic answer to
3
+ * coord-tui's `FocusedRegion` / Ctrl-W.
4
+ *
5
+ * The binding is **F6 / Shift+F6**, which is the long-standing platform
6
+ * convention for "move to the next pane" (Firefox and Chrome both use it to
7
+ * cycle browser panes, VS Code uses it for editor groups, and Windows uses it
8
+ * app-wide). It is also, unusually for a function key, safe: no browser
9
+ * assigns F6 to anything that a web page stealing it would break, and it
10
+ * doesn't collide with a text field the way a Ctrl-letter chord does — so the
11
+ * handler doesn't need to special-case typing in the list panel's filter box.
12
+ *
13
+ * Focus lands on the *region container* (each is `tabIndex={-1}` with an
14
+ * accessible name), not on the first control inside it. That's the standard
15
+ * landmark-navigation behaviour: a screen reader announces the region you
16
+ * arrived in, and a plain Tab from there walks into its controls.
17
+ */
18
+ import { useCallback, useEffect, useRef, useState } from 'react'
19
+
20
+ export type ShellRegion = 'rail' | 'list' | 'detail'
21
+
22
+ export interface UseRegionFocus {
23
+ /** The region focus was last moved to via F6, for a visible ring. */
24
+ focusedRegion: ShellRegion | null
25
+ /** Ref callback factory: `ref={registerRegion('list')}`. */
26
+ registerRegion: (region: ShellRegion) => (el: HTMLElement | null) => void
27
+ /** Programmatic move (used by the resize handle's Escape, and by tests). */
28
+ focusRegion: (region: ShellRegion) => void
29
+ }
30
+
31
+ /**
32
+ * @param visibleRegions Regions currently on screen, in visual order. Regions
33
+ * omitted here are skipped by F6 — on narrow only one of list/detail exists
34
+ * at a time, and cycling into a `display:none` container would silently drop
35
+ * focus to `<body>`.
36
+ */
37
+ export function useRegionFocus(visibleRegions: ShellRegion[]): UseRegionFocus {
38
+ const elements = useRef<Partial<Record<ShellRegion, HTMLElement | null>>>({})
39
+ const [focusedRegion, setFocusedRegion] = useState<ShellRegion | null>(null)
40
+
41
+ // The keydown listener is attached once; reading the order through a ref
42
+ // keeps it from being torn down and rebuilt on every layout change.
43
+ const orderRef = useRef(visibleRegions)
44
+ orderRef.current = visibleRegions
45
+
46
+ const registerRegion = useCallback(
47
+ (region: ShellRegion) => (el: HTMLElement | null) => {
48
+ elements.current[region] = el
49
+ },
50
+ [],
51
+ )
52
+
53
+ const focusRegion = useCallback((region: ShellRegion) => {
54
+ const el = elements.current[region]
55
+ if (!el) return
56
+ el.focus()
57
+ setFocusedRegion(region)
58
+ }, [])
59
+
60
+ useEffect(() => {
61
+ const onKeyDown = (event: KeyboardEvent) => {
62
+ if (event.key !== 'F6') return
63
+ const order = orderRef.current
64
+ if (order.length === 0) return
65
+
66
+ // Where are we now? Prefer the region that actually contains
67
+ // `document.activeElement` over the last F6 target, so an F6 after a
68
+ // mouse click continues from where the user is rather than from where
69
+ // the keyboard last was.
70
+ const active = document.activeElement as HTMLElement | null
71
+ let currentIndex = order.findIndex((region) => {
72
+ const el = elements.current[region]
73
+ return !!el && !!active && el.contains(active)
74
+ })
75
+ if (currentIndex === -1) currentIndex = order.indexOf(focusedRegion as ShellRegion)
76
+
77
+ const step = event.shiftKey ? -1 : 1
78
+ // `currentIndex === -1` (focus is outside every region) + step 1 lands
79
+ // on index 0, which is the intended "enter the shell at the rail".
80
+ const nextIndex = (currentIndex + step + order.length) % order.length
81
+ const next = order[nextIndex]
82
+ const el = elements.current[next]
83
+ if (!el) return
84
+
85
+ event.preventDefault()
86
+ el.focus()
87
+ setFocusedRegion(next)
88
+ }
89
+
90
+ window.addEventListener('keydown', onKeyDown)
91
+ return () => window.removeEventListener('keydown', onKeyDown)
92
+ }, [focusedRegion])
93
+
94
+ return { focusedRegion, registerRegion, focusRegion }
95
+ }
@@ -0,0 +1,41 @@
1
+ import '@testing-library/jest-dom'
2
+
3
+ // jsdom doesn't implement matchMedia (https://github.com/jsdom/jsdom/issues/3522).
4
+ // xterm.js's CoreBrowserService queries it (devicePixelRatio change listener) as
5
+ // soon as a Terminal is opened, which crashes every Terminal-pane test (#1068)
6
+ // without this stub.
7
+ if (typeof window !== 'undefined' && !window.matchMedia) {
8
+ window.matchMedia = (query: string) => ({
9
+ matches: false,
10
+ media: query,
11
+ onchange: null,
12
+ addListener: () => {},
13
+ removeListener: () => {},
14
+ addEventListener: () => {},
15
+ removeEventListener: () => {},
16
+ dispatchEvent: () => false,
17
+ })
18
+ }
19
+
20
+ // jsdom's canvas is stubbed (no `canvas` npm package installed) -- getContext('2d')
21
+ // logs a noisy "Not implemented" jsdom error and returns undefined either way.
22
+ // xterm.js already handles a missing 2D context gracefully (its color helper
23
+ // falls back to manual CSS-color parsing), so short-circuit straight to that
24
+ // path and skip the console noise.
25
+ if (typeof HTMLCanvasElement !== 'undefined') {
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- matching the DOM lib's overload surface isn't worth it for a test-only stub
27
+ HTMLCanvasElement.prototype.getContext = (() => null) as any
28
+ }
29
+
30
+ // jsdom doesn't implement ResizeObserver (https://github.com/jsdom/jsdom/issues/3368).
31
+ // Terminal.tsx uses it to watch the pane container for layout shifts that
32
+ // window.resize misses (e.g. MobileKeyBar height changes, safe-area adjustments).
33
+ // A no-op stub prevents "ResizeObserver is not defined" crashes in every
34
+ // Terminal test while preserving real behaviour in a real browser.
35
+ if (typeof globalThis.ResizeObserver === 'undefined') {
36
+ globalThis.ResizeObserver = class ResizeObserver {
37
+ observe() {}
38
+ unobserve() {}
39
+ disconnect() {}
40
+ }
41
+ }
@@ -0,0 +1,2 @@
1
+ /// <reference types="vite/client" />
2
+ /// <reference types="vite-plugin-pwa/client" />
@@ -0,0 +1,140 @@
1
+ import animate from 'tailwindcss-animate'
2
+
3
+ /**
4
+ * Tailwind config for coord web's design-token layer (#1546).
5
+ *
6
+ * Colour values are plain CSS custom properties (hex / rgba), NOT the
7
+ * `hsl(var(--x))` triplets shadcn/ui's default template uses -- the token
8
+ * seed this builds on (docs/mocks/web/_tokens.css) is authored in hex, and
9
+ * re-deriving HSL triplets from it would be one more place for the real
10
+ * theme to drift from the settled mocks. `var(--x)` works as a Tailwind
11
+ * colour value exactly like `hsl(var(--x))` does; only the storage format
12
+ * differs.
13
+ *
14
+ * Two colour vocabularies coexist deliberately:
15
+ * - the shadcn semantic slots (background/foreground/card/primary/...)
16
+ * that ui/* primitives and existing components (PipelineCard,
17
+ * ConnectionBadge, MobileKeyBar) already consume, so migrating the token
18
+ * values underneath them is a one-file change;
19
+ * - the raw mock palette (ground/surface/line/brand/pass/attn/fail/idle/
20
+ * faint) exposed under its own names for new primitives and the gallery,
21
+ * matching docs/mocks/web/_tokens.css exactly.
22
+ *
23
+ * `accent` is kept pointed at the neutral hover/press surface (its existing
24
+ * meaning -- see MobileKeyBar's `active:bg-accent`) rather than repointed at
25
+ * the mock's cyan "work is happening" colour, which would have silently
26
+ * recoloured every existing pressed-state highlight. The cyan lives under
27
+ * `brand` instead (`--accent` the CSS custom property, to match the mock's
28
+ * own naming) and drives `ring`/`primary`, matching the mocks' focus and
29
+ * primary-button treatment.
30
+ */
31
+ export default {
32
+ darkMode: ['selector', '[data-theme="dark"]'],
33
+ content: ['./index.html', './src/**/*.{ts,tsx,js,jsx}'],
34
+ theme: {
35
+ container: {
36
+ center: true,
37
+ padding: '2rem',
38
+ },
39
+ extend: {
40
+ colors: {
41
+ border: 'var(--border)',
42
+ input: 'var(--input)',
43
+ ring: 'var(--ring)',
44
+ background: 'var(--background)',
45
+ foreground: 'var(--foreground)',
46
+ primary: {
47
+ DEFAULT: 'var(--primary)',
48
+ foreground: 'var(--primary-foreground)',
49
+ },
50
+ secondary: {
51
+ DEFAULT: 'var(--secondary)',
52
+ foreground: 'var(--secondary-foreground)',
53
+ },
54
+ destructive: {
55
+ DEFAULT: 'var(--destructive)',
56
+ foreground: 'var(--destructive-foreground)',
57
+ },
58
+ muted: {
59
+ DEFAULT: 'var(--muted)',
60
+ foreground: 'var(--muted-foreground)',
61
+ },
62
+ accent: {
63
+ DEFAULT: 'var(--accent-surface)',
64
+ foreground: 'var(--accent-surface-foreground)',
65
+ },
66
+ card: {
67
+ DEFAULT: 'var(--card)',
68
+ foreground: 'var(--card-foreground)',
69
+ },
70
+ popover: {
71
+ DEFAULT: 'var(--popover)',
72
+ foreground: 'var(--popover-foreground)',
73
+ },
74
+
75
+ // ── raw mock palette (docs/mocks/web/_tokens.css) ────────────────
76
+ ground: 'var(--ground)',
77
+ surface: {
78
+ DEFAULT: 'var(--surface)',
79
+ 2: 'var(--surface-2)',
80
+ 3: 'var(--surface-3)',
81
+ },
82
+ line: {
83
+ DEFAULT: 'var(--line)',
84
+ strong: 'var(--line-strong)',
85
+ },
86
+ faint: 'var(--text-faint)',
87
+ brand: {
88
+ DEFAULT: 'var(--accent)',
89
+ dim: 'var(--accent-dim)',
90
+ wash: 'var(--accent-wash)',
91
+ },
92
+ pass: {
93
+ DEFAULT: 'var(--pass)',
94
+ wash: 'var(--pass-wash)',
95
+ },
96
+ attn: {
97
+ DEFAULT: 'var(--attn)',
98
+ wash: 'var(--attn-wash)',
99
+ },
100
+ fail: {
101
+ DEFAULT: 'var(--fail)',
102
+ wash: 'var(--fail-wash)',
103
+ },
104
+ idle: {
105
+ DEFAULT: 'var(--idle)',
106
+ wash: 'var(--idle-wash)',
107
+ },
108
+ },
109
+ fontFamily: {
110
+ sans: ['var(--font-ui)'],
111
+ mono: ['var(--font-mono)'],
112
+ },
113
+ fontSize: {
114
+ 'step--1': 'var(--step--1)',
115
+ 'step-0': 'var(--step-0)',
116
+ 'step-1': 'var(--step-1)',
117
+ 'step-2': 'var(--step-2)',
118
+ 'step-3': 'var(--step-3)',
119
+ },
120
+ borderRadius: {
121
+ sm: 'var(--r-sm)',
122
+ md: 'var(--r-md)',
123
+ lg: 'var(--r-lg)',
124
+ },
125
+ boxShadow: {
126
+ elevation: '0 18px 44px -12px rgba(0,0,0,.66)',
127
+ },
128
+ keyframes: {
129
+ 'pulse-ring': {
130
+ '0%, 100%': { opacity: 1, boxShadow: '0 0 0 0 var(--accent-wash)' },
131
+ '50%': { opacity: 0.55, boxShadow: '0 0 0 6px transparent' },
132
+ },
133
+ },
134
+ animation: {
135
+ 'pulse-ring': 'pulse-ring 2s ease-in-out infinite',
136
+ },
137
+ },
138
+ },
139
+ plugins: [animate],
140
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "baseUrl": ".",
19
+ "paths": {
20
+ "@/*": ["./src/*"]
21
+ }
22
+ },
23
+ "include": ["src"],
24
+ "references": [{ "path": "./tsconfig.node.json" }]
25
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true,
8
+ "strict": true
9
+ },
10
+ "include": ["vite.config.ts"]
11
+ }