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,280 @@
1
+ /**
2
+ * MobileKeyBar — the bottom-half input surface for driving the interactive
3
+ * `claude` TUI from a phone (#1070, part of epic #1064). Sits below
4
+ * `Terminal`'s xterm pane and sends the byte sequences a mobile soft-keyboard
5
+ * makes painful to reach: Esc, arrows, Ctrl-C, Tab, `/`, plus a line-input
6
+ * field for typing a command and submitting it.
7
+ *
8
+ * This component knows nothing about WebSockets or xterm -- it just calls
9
+ * `onSend(bytes)` with a raw string (one JS char per terminal byte, e.g.
10
+ * `'\x1b[A'` for Up) and `onControl(action)` for copy-mode control frames,
11
+ * leaving the actual write-to-PTY / JSON-frame plumbing to the caller
12
+ * (`Terminal.tsx`'s `sendRaw` / `sendControl`, same path `term.onData` uses).
13
+ *
14
+ * ## Scroll mode (#1299)
15
+ *
16
+ * Tapping **Scroll** enters a server-driven copy-mode (via
17
+ * `{"type":"copy-mode","action":"enter"}`) so the user can scroll back through
18
+ * tmux pane history from the phone without needing access to the prefix key or
19
+ * tmux's copy-mode key bindings. While in scroll mode:
20
+ * - An unmistakable **SCROLL** badge makes the current mode obvious.
21
+ * - The normal key rows are replaced by Page Up / Page Down / Top / Bottom
22
+ * controls and an Exit button.
23
+ * - Submitting the line-input sends `exit` first so the text lands in the
24
+ * live pane, not in tmux's copy-mode search.
25
+ *
26
+ * `connectionKey` is incremented by `Terminal.tsx` on every WebSocket open
27
+ * (including reconnects). A reconnect resets copy-mode server-side (a fresh
28
+ * `tmux attach-session` starts outside copy-mode), so the component resets the
29
+ * scroll toggle on `connectionKey` change so the badge always reflects reality.
30
+ *
31
+ * Multi-line submission to `claude` needs bracketed paste + a settle delay +
32
+ * a *separate* trailing `\r` (see the #425/#426 PTY-submission notes) -- out
33
+ * of scope here. The line-input only ever sends a single line followed by
34
+ * `\r`, matching the issue's "get single-line + Enter solid first" guidance.
35
+ *
36
+ * Every soft key is a `type="button"` with `onMouseDown` preventing the
37
+ * default focus change, so tapping a key never blurs the line-input field --
38
+ * if the phone's native keyboard is up because the user is mid-line, a
39
+ * Ctrl-C or arrow tap shouldn't dismiss it.
40
+ */
41
+ import { useEffect, useState, type FormEvent, type ReactNode } from 'react'
42
+ import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight, CornerDownLeft } from 'lucide-react'
43
+ import { cn } from '@/lib/utils'
44
+
45
+ /** Escape / control byte sequences a real terminal would emit for these keys. */
46
+ export const KEY_BYTES = {
47
+ escape: '\x1b',
48
+ up: '\x1b[A',
49
+ down: '\x1b[B',
50
+ right: '\x1b[C',
51
+ left: '\x1b[D',
52
+ enter: '\r',
53
+ ctrlC: '\x03',
54
+ tab: '\x09',
55
+ slash: '/',
56
+ } as const
57
+
58
+ export interface MobileKeyBarProps {
59
+ /** Send raw bytes (as a JS string) down the terminal's input path. */
60
+ onSend: (data: string) => void
61
+ /** Send a copy-mode control action (e.g. "enter", "page-up") to the server. */
62
+ onControl: (action: string) => void
63
+ /**
64
+ * Incremented by `Terminal.tsx` on every WebSocket open (initial connect and
65
+ * reconnects). A change signals that server-side copy-mode state has been
66
+ * cleared, so the component resets its scroll toggle to avoid a stale badge.
67
+ */
68
+ connectionKey?: number
69
+ }
70
+
71
+ interface SoftKeyProps {
72
+ ariaLabel: string
73
+ bytes: string
74
+ onSend: (data: string) => void
75
+ className?: string
76
+ children: ReactNode
77
+ }
78
+
79
+ function SoftKey({ ariaLabel, bytes, onSend, className, children }: SoftKeyProps) {
80
+ return (
81
+ <button
82
+ type="button"
83
+ aria-label={ariaLabel}
84
+ onMouseDown={(e) => e.preventDefault()}
85
+ onClick={() => onSend(bytes)}
86
+ className={cn(
87
+ 'flex items-center justify-center rounded-md border border-border bg-card py-3',
88
+ 'text-sm font-medium text-foreground active:bg-accent',
89
+ className,
90
+ )}
91
+ >
92
+ {children}
93
+ </button>
94
+ )
95
+ }
96
+
97
+ interface ControlKeyProps {
98
+ ariaLabel: string
99
+ action: string
100
+ onControl: (action: string) => void
101
+ className?: string
102
+ children: ReactNode
103
+ }
104
+
105
+ function ControlKey({ ariaLabel, action, onControl, className, children }: ControlKeyProps) {
106
+ return (
107
+ <button
108
+ type="button"
109
+ aria-label={ariaLabel}
110
+ onMouseDown={(e) => e.preventDefault()}
111
+ onClick={() => onControl(action)}
112
+ className={cn(
113
+ 'flex items-center justify-center rounded-md border border-border bg-card py-3',
114
+ 'text-sm font-medium text-foreground active:bg-accent',
115
+ className,
116
+ )}
117
+ >
118
+ {children}
119
+ </button>
120
+ )
121
+ }
122
+
123
+ export default function MobileKeyBar({ onSend, onControl, connectionKey }: MobileKeyBarProps) {
124
+ const [line, setLine] = useState('')
125
+ const [scrollMode, setScrollMode] = useState(false)
126
+
127
+ // Reset scroll toggle whenever the WebSocket reconnects. A fresh
128
+ // `tmux attach-session` always starts outside copy-mode, so a stale
129
+ // "SCROLL" badge would lie about the server's real state.
130
+ useEffect(() => {
131
+ setScrollMode(false)
132
+ }, [connectionKey])
133
+
134
+ const enterScroll = () => {
135
+ setScrollMode(true)
136
+ onControl('enter')
137
+ }
138
+
139
+ const exitScroll = () => {
140
+ setScrollMode(false)
141
+ onControl('exit')
142
+ }
143
+
144
+ const handleSubmit = (e: FormEvent) => {
145
+ e.preventDefault()
146
+ if (!line) return
147
+ // If we're in scroll mode, exit copy-mode first so the typed text lands in
148
+ // the live pane rather than tmux's copy-mode search buffer.
149
+ if (scrollMode) {
150
+ onControl('exit')
151
+ setScrollMode(false)
152
+ }
153
+ onSend(line + KEY_BYTES.enter)
154
+ setLine('')
155
+ }
156
+
157
+ return (
158
+ <div className="flex h-full flex-col justify-end gap-2 px-2 py-2" data-testid="mobile-key-bar">
159
+ {scrollMode ? (
160
+ // ── Scroll mode: copy-mode controls ──────────────────────────────────
161
+ <>
162
+ {/* Badge — always visible so the user knows where taps are going. */}
163
+ <div
164
+ role="status"
165
+ aria-label="Scroll mode active"
166
+ className="flex items-center justify-center rounded-md bg-primary px-3 py-1 text-xs font-bold uppercase tracking-widest text-primary-foreground"
167
+ >
168
+ SCROLL
169
+ </div>
170
+
171
+ {/* Scrollback navigation row */}
172
+ <div className="grid grid-cols-4 gap-2">
173
+ <ControlKey ariaLabel="Page Up" action="page-up" onControl={onControl}>
174
+ Pg Up
175
+ </ControlKey>
176
+ <ControlKey ariaLabel="Page Down" action="page-down" onControl={onControl}>
177
+ Pg Dn
178
+ </ControlKey>
179
+ <ControlKey ariaLabel="Top" action="top" onControl={onControl}>
180
+ Top
181
+ </ControlKey>
182
+ <ControlKey ariaLabel="Bottom" action="bottom" onControl={onControl}>
183
+ Bottom
184
+ </ControlKey>
185
+ </div>
186
+
187
+ {/* Exit scroll mode */}
188
+ <button
189
+ type="button"
190
+ aria-label="Exit Scroll"
191
+ onMouseDown={(e) => e.preventDefault()}
192
+ onClick={exitScroll}
193
+ className={cn(
194
+ 'flex w-full items-center justify-center rounded-md border border-primary py-3',
195
+ 'text-sm font-medium text-primary active:bg-accent',
196
+ )}
197
+ >
198
+ Exit Scroll
199
+ </button>
200
+ </>
201
+ ) : (
202
+ // ── Normal mode: standard key rows + Scroll toggle ───────────────────
203
+ <>
204
+ <div className="grid grid-cols-4 gap-2">
205
+ <SoftKey ariaLabel="Escape" bytes={KEY_BYTES.escape} onSend={onSend}>
206
+ Esc
207
+ </SoftKey>
208
+ <SoftKey ariaLabel="Tab" bytes={KEY_BYTES.tab} onSend={onSend}>
209
+ Tab
210
+ </SoftKey>
211
+ <SoftKey ariaLabel="Ctrl-C" bytes={KEY_BYTES.ctrlC} onSend={onSend}>
212
+ Ctrl-C
213
+ </SoftKey>
214
+ <SoftKey ariaLabel="Slash" bytes={KEY_BYTES.slash} onSend={onSend}>
215
+ /
216
+ </SoftKey>
217
+ </div>
218
+
219
+ <div className="grid grid-cols-4 gap-2">
220
+ <SoftKey ariaLabel="Left" bytes={KEY_BYTES.left} onSend={onSend}>
221
+ <ArrowLeft size={18} />
222
+ </SoftKey>
223
+ <SoftKey ariaLabel="Up" bytes={KEY_BYTES.up} onSend={onSend}>
224
+ <ArrowUp size={18} />
225
+ </SoftKey>
226
+ <SoftKey ariaLabel="Down" bytes={KEY_BYTES.down} onSend={onSend}>
227
+ <ArrowDown size={18} />
228
+ </SoftKey>
229
+ <SoftKey ariaLabel="Right" bytes={KEY_BYTES.right} onSend={onSend}>
230
+ <ArrowRight size={18} />
231
+ </SoftKey>
232
+ </div>
233
+
234
+ {/* Scroll toggle — enters copy-mode for pane history (#1299). */}
235
+ <button
236
+ type="button"
237
+ aria-label="Scroll"
238
+ onMouseDown={(e) => e.preventDefault()}
239
+ onClick={enterScroll}
240
+ className={cn(
241
+ 'flex w-full items-center justify-center rounded-md border border-border bg-card py-3',
242
+ 'text-sm font-medium text-foreground active:bg-accent',
243
+ )}
244
+ >
245
+ Scroll
246
+ </button>
247
+ </>
248
+ )}
249
+
250
+ <SoftKey ariaLabel="Enter" bytes={KEY_BYTES.enter} onSend={onSend} className="w-full gap-2">
251
+ <CornerDownLeft size={16} />
252
+ Enter
253
+ </SoftKey>
254
+
255
+ <form onSubmit={handleSubmit} className="flex gap-2">
256
+ <input
257
+ type="text"
258
+ value={line}
259
+ onChange={(e) => setLine(e.target.value)}
260
+ placeholder="Type a command…"
261
+ aria-label="Command line"
262
+ inputMode="text"
263
+ enterKeyHint="send"
264
+ autoCapitalize="off"
265
+ autoCorrect="off"
266
+ autoComplete="off"
267
+ spellCheck={false}
268
+ className="min-w-0 flex-1 rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary"
269
+ />
270
+ <button
271
+ type="submit"
272
+ aria-label="Send line"
273
+ className="shrink-0 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground active:opacity-80"
274
+ >
275
+ Send
276
+ </button>
277
+ </form>
278
+ </div>
279
+ )
280
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The list panel's head (#1547) — the mock's `.panel-title`: view name, a
3
+ * mono count of what the machine is tracking, and a slot on the right for
4
+ * whatever the panel wants to say about its own freshness.
5
+ *
6
+ * The `co` mark is narrow-only (`md:hidden`, and Tailwind's `md` is 768px —
7
+ * the same value as `BREAKPOINT_MEDIUM_PX`). On narrow the rail is a bottom
8
+ * icon row with no head, so without this the phone would lose the app's
9
+ * identity entirely; from 768px up the rail head carries it and a second mark
10
+ * here would just be a duplicate.
11
+ *
12
+ * The theme toggle (#1551) is narrow-only for the same reason and by the same
13
+ * mechanism: `docs/mocks/web/pipeline-narrow.html`'s per-view topbar carries
14
+ * its own Theme icon-button (`id="theme"`) because the bottom nav has no room
15
+ * for it — narrow's rail deliberately shows only `ready` nav entries
16
+ * (`ActivityRail.tsx`) and a dimmed un-tappable settings-style control there
17
+ * would eat thumb real estate. From 768px up the rail foot already has
18
+ * "Theme" (`ActivityRail.tsx`), so this one is `md:hidden` — CSS `display:
19
+ * none`, which Playwright's (and every screen reader's) accessibility-tree
20
+ * role queries already exclude, exactly like the `co` mark above — never two
21
+ * "Switch to … theme" buttons live in the tree at once.
22
+ */
23
+ import type { ReactNode } from 'react'
24
+ import { ThemeToggle } from '@/components/ui/theme-toggle'
25
+
26
+ export interface PanelHeaderProps {
27
+ title: string
28
+ /** Mono count shown after the title; omitted when undefined. */
29
+ count?: number
30
+ /** Word after the count, e.g. "tracked" → `7 tracked`. */
31
+ countLabel?: string
32
+ /** Right-hand slot (freshness, refresh pulse, …). */
33
+ children?: ReactNode
34
+ }
35
+
36
+ export function PanelHeader({ title, count, countLabel = 'tracked', children }: PanelHeaderProps) {
37
+ return (
38
+ <header className="mb-4 flex items-center gap-2.5">
39
+ <span
40
+ aria-hidden="true"
41
+ className="grid h-[22px] w-[22px] flex-none place-items-center rounded-md bg-gradient-to-br from-brand to-brand-dim font-mono text-[.62rem] font-bold tracking-[-.02em] text-[#0b1013] md:hidden"
42
+ >
43
+ co
44
+ </span>
45
+ <h1 className="text-step-1 font-semibold tracking-[-.01em] text-foreground">{title}</h1>
46
+ {count != null && (
47
+ <span className="font-mono text-[.75rem] text-faint">
48
+ {count} {countLabel}
49
+ </span>
50
+ )}
51
+ <div className="ml-auto flex items-center gap-2">
52
+ {children}
53
+ <div className="md:hidden">
54
+ <ThemeToggle />
55
+ </div>
56
+ </div>
57
+ </header>
58
+ )
59
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * PipelineCard — a phone-friendly card for one in-flight pipeline item.
3
+ *
4
+ * Shows: issue title, repo + issue number, stage chips (work→test→review→merge),
5
+ * machine name, and an overall status badge.
6
+ */
7
+ import { cn } from '@/lib/utils'
8
+ import { type PipelineView, type PipelineStage } from '@/api/client'
9
+
10
+ // ── Stage display helpers ─────────────────────────────────────────────────────
11
+
12
+ /**
13
+ * Map the internal stage name from pipeline.py to the user-facing label.
14
+ * Work Order matches the TUI: Work → Test → Review → Merge.
15
+ */
16
+ const STAGE_LABEL: Record<string, string> = {
17
+ coding: 'work',
18
+ smoke: 'test',
19
+ review: 'review',
20
+ merge: 'merge',
21
+ }
22
+
23
+ /**
24
+ * Fine-grained `current_stage` values that indicate the pipeline is currently
25
+ * running (i.e. a subprocess is in progress — no human action required yet).
26
+ */
27
+ const RUNNING_STAGES = new Set([
28
+ 'coding',
29
+ 'review_running',
30
+ 'smoke_running',
31
+ 'merging',
32
+ ])
33
+
34
+ /**
35
+ * Fine-grained `current_stage` values that are terminal failures or need
36
+ * human attention before the pipeline can advance.
37
+ */
38
+ const FAILED_STAGES = new Set([
39
+ 'failed',
40
+ 'review_failed',
41
+ 'smoke_failed',
42
+ ])
43
+
44
+ /** Return Tailwind classes for a single stage chip. */
45
+ function stageChipClass(stage: PipelineStage, currentStage: string): string {
46
+ const base = 'rounded px-1.5 py-0.5 text-xs font-medium'
47
+
48
+ if (stage.is_current) {
49
+ // Active stage — colour by failure vs running
50
+ if (FAILED_STAGES.has(currentStage)) {
51
+ return cn(base, 'bg-destructive text-destructive-foreground')
52
+ }
53
+ return cn(base, 'bg-primary text-primary-foreground')
54
+ }
55
+
56
+ switch (stage.status) {
57
+ case 'completed':
58
+ return cn(base, 'bg-green-700 text-white')
59
+ case 'skipped':
60
+ return cn(base, 'border border-border text-muted-foreground opacity-40')
61
+ default:
62
+ // "waiting"
63
+ return cn(base, 'border border-border text-muted-foreground')
64
+ }
65
+ }
66
+
67
+ // ── Overall status badge ──────────────────────────────────────────────────────
68
+
69
+ interface StatusInfo {
70
+ label: string
71
+ className: string
72
+ }
73
+
74
+ /** Map fine-grained current_stage to a human badge + colour. */
75
+ function stageStatusInfo(currentStage: string): StatusInfo {
76
+ if (RUNNING_STAGES.has(currentStage)) {
77
+ return { label: 'running', className: 'bg-primary text-primary-foreground' }
78
+ }
79
+ if (FAILED_STAGES.has(currentStage)) {
80
+ return { label: 'failed', className: 'bg-destructive text-destructive-foreground' }
81
+ }
82
+ switch (currentStage) {
83
+ case 'merged':
84
+ return { label: 'merged', className: 'bg-green-700 text-white' }
85
+ case 'merge_ready':
86
+ return { label: 'mergeable', className: 'bg-yellow-600 text-black' }
87
+ case 'review_done':
88
+ return { label: 'review ✓', className: 'bg-yellow-600 text-black' }
89
+ case 'smoke_passed':
90
+ return { label: 'test ✓', className: 'bg-yellow-600 text-black' }
91
+ case 'done':
92
+ return { label: 'work done', className: 'bg-yellow-600 text-black' }
93
+ default:
94
+ return { label: currentStage.replace(/_/g, ' '), className: 'bg-secondary text-secondary-foreground' }
95
+ }
96
+ }
97
+
98
+ // ── Component ─────────────────────────────────────────────────────────────────
99
+
100
+ export interface PipelineCardProps {
101
+ view: PipelineView
102
+ onClick: () => void
103
+ /**
104
+ * This row's item is the one open in the detail panel (#1547).
105
+ *
106
+ * Only ever true on a viewport wide enough to show list and detail at once —
107
+ * on a phone the detail replaces the list, so there is nothing to mark. The
108
+ * treatment is the mock's `.row[aria-selected="true"]`: an accent bar on the
109
+ * leading edge plus a stronger border, never a fill, because the accent
110
+ * means "work is happening here" and a selected row hasn't earned it.
111
+ */
112
+ selected?: boolean
113
+ }
114
+
115
+ export function PipelineCard({ view, onClick, selected }: PipelineCardProps) {
116
+ const { label: statusLabel, className: statusClass } = stageStatusInfo(view.current_stage)
117
+
118
+ return (
119
+ <button
120
+ type="button"
121
+ onClick={onClick}
122
+ aria-current={selected ? 'true' : undefined}
123
+ className={cn(
124
+ 'relative w-full overflow-hidden rounded-lg border border-border bg-card p-4 text-left shadow-sm transition-colors active:bg-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
125
+ selected && 'border-line-strong before:absolute before:inset-y-2 before:left-0 before:w-[2px] before:rounded-r-sm before:bg-brand before:content-[\'\']',
126
+ )}
127
+ >
128
+ {/* Top row: issue title + status badge */}
129
+ <div className="flex items-start justify-between gap-3">
130
+ <p className="flex-1 truncate text-sm font-medium text-card-foreground">
131
+ {view.issue_title}
132
+ </p>
133
+ <span
134
+ className={cn(
135
+ 'shrink-0 rounded-full px-2 py-0.5 text-xs font-semibold',
136
+ statusClass,
137
+ )}
138
+ >
139
+ {statusLabel}
140
+ </span>
141
+ </div>
142
+
143
+ {/* Second row: repo#N + machine */}
144
+ <p className="mt-1 text-xs text-muted-foreground">
145
+ {view.repo_name} <span className="font-mono">#{view.issue_number}</span>
146
+ {' · '}
147
+ {view.machine_name}
148
+ </p>
149
+
150
+ {/* Stage chips */}
151
+ <div className="mt-3 flex flex-wrap gap-1.5">
152
+ {view.stages.map((stage) => (
153
+ <span
154
+ key={stage.name}
155
+ className={stageChipClass(stage, view.current_stage)}
156
+ >
157
+ {STAGE_LABEL[stage.name] ?? stage.name}
158
+ </span>
159
+ ))}
160
+ </div>
161
+
162
+ {/* Review findings pending indicator */}
163
+ {view.review_findings_pending && (
164
+ <p className="mt-2 text-xs text-yellow-500">⚠ Review findings not yet posted</p>
165
+ )}
166
+ </button>
167
+ )
168
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * SessionCard — a phone-friendly card for one live coord-* interactive
3
+ * tmux session (#1066's `GET /api/sessions`). Tapping navigates to the
4
+ * `/terminal/:sessionId` take-over view (#1065's `/ws/terminal/{id}` bridge).
5
+ */
6
+ import { cn } from '@/lib/utils'
7
+ import { type SessionInfo } from '@/api/client'
8
+
9
+ interface StatusInfo {
10
+ label: string
11
+ className: string
12
+ }
13
+
14
+ /**
15
+ * Human-facing label for the session's assignment type
16
+ * (`session.stage` is literally `Assignment.type`, e.g. "work"/"smoke"/
17
+ * "review"/"conflict-fix"/"fix"/"plan"/... — see `server.py`'s
18
+ * `"stage": assignment.type if assignment else None`).
19
+ *
20
+ * This is deliberately its own map, not `PipelineCard`'s `STAGE_LABEL` —
21
+ * that one is keyed by `pipeline.py`'s *derived* stage names ("coding",
22
+ * "merge", ...), which only accidentally overlap with real `Assignment.type`
23
+ * values on "smoke" and "review" (#1276 review). There is no literal
24
+ * `type="merge"`; both the automated #241 rebase-conflict worker and the
25
+ * interactive `--merge-of` session dispatch as `type="conflict-fix"`, so
26
+ * that's translated to the "merge" label users expect here.
27
+ */
28
+ const ASSIGNMENT_TYPE_LABEL: Record<string, string> = {
29
+ smoke: 'test',
30
+ review: 'review',
31
+ 'conflict-fix': 'merge',
32
+ }
33
+
34
+ function stageLabel(stage: string): string {
35
+ return ASSIGNMENT_TYPE_LABEL[stage] ?? stage
36
+ }
37
+
38
+ /** Badge reflecting whether the session is live/attached/ended. */
39
+ function sessionStatusInfo(session: SessionInfo): StatusInfo {
40
+ if (session.pane_dead) {
41
+ return { label: 'ended', className: 'bg-secondary text-secondary-foreground' }
42
+ }
43
+ if (session.attached) {
44
+ return { label: 'attached', className: 'bg-yellow-600 text-black' }
45
+ }
46
+ return { label: 'live', className: 'bg-primary text-primary-foreground' }
47
+ }
48
+
49
+ export interface SessionCardProps {
50
+ session: SessionInfo
51
+ onClick: () => void
52
+ }
53
+
54
+ export function SessionCard({ session, onClick }: SessionCardProps) {
55
+ const { label: statusLabel, className: statusClass } = sessionStatusInfo(session)
56
+ const title = session.issue_title ?? session.session_name
57
+
58
+ return (
59
+ <button
60
+ type="button"
61
+ onClick={onClick}
62
+ className="w-full rounded-lg border border-border bg-card p-4 text-left shadow-sm transition-colors active:bg-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
63
+ >
64
+ {/* Top row: title + assignment-type chip (when resolvable) + status badge */}
65
+ <div className="flex items-start justify-between gap-3">
66
+ <p className="flex-1 truncate text-sm font-medium text-card-foreground">
67
+ {title}
68
+ </p>
69
+ <div className="flex shrink-0 items-center gap-1.5">
70
+ {session.stage && (
71
+ <span className="rounded border border-border px-1.5 py-0.5 text-xs font-medium text-muted-foreground">
72
+ {stageLabel(session.stage)}
73
+ </span>
74
+ )}
75
+ <span
76
+ className={cn(
77
+ 'rounded-full px-2 py-0.5 text-xs font-semibold',
78
+ statusClass,
79
+ )}
80
+ >
81
+ {statusLabel}
82
+ </span>
83
+ </div>
84
+ </div>
85
+
86
+ {/* Second row: repo#N + machine, when the session maps to a tracked assignment */}
87
+ {(session.repo || session.machine) && (
88
+ <p className="mt-1 text-xs text-muted-foreground">
89
+ {session.repo}
90
+ {session.issue !== null && (
91
+ <span className="font-mono"> #{session.issue}</span>
92
+ )}
93
+ {session.repo && session.machine && ' · '}
94
+ {session.machine}
95
+ </p>
96
+ )}
97
+ </button>
98
+ )
99
+ }