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.
- code_coordinator-0.5.46.dist-info/METADATA +625 -0
- code_coordinator-0.5.46.dist-info/RECORD +295 -0
- code_coordinator-0.5.46.dist-info/WHEEL +5 -0
- code_coordinator-0.5.46.dist-info/entry_points.txt +2 -0
- code_coordinator-0.5.46.dist-info/licenses/LICENSE +110 -0
- code_coordinator-0.5.46.dist-info/top_level.txt +1 -0
- coord/__init__.py +176 -0
- coord/_board_mapping.py +229 -0
- coord/acceptance.py +468 -0
- coord/acceptance_drivers.py +632 -0
- coord/agent.py +7517 -0
- coord/agent_app.py +1555 -0
- coord/agent_update.py +417 -0
- coord/agents/opencode/.gitignore +13 -0
- coord/agents/opencode/agents/work.md +129 -0
- coord/agents/opencode/routing.jsonc +49 -0
- coord/audit.py +301 -0
- coord/auto_loop.py +1440 -0
- coord/board_bool_guard.py +72 -0
- coord/board_service.py +141 -0
- coord/board_wire.py +309 -0
- coord/brain.py +581 -0
- coord/branch_model.py +214 -0
- coord/cargo_cache.py +258 -0
- coord/ci_github.py +386 -0
- coord/ci_store.py +560 -0
- coord/claim.py +353 -0
- coord/cli.py +454 -0
- coord/client.py +610 -0
- coord/commands/__init__.py +1 -0
- coord/commands/_common.py +329 -0
- coord/commands/acceptance.py +916 -0
- coord/commands/agent_ops.py +1339 -0
- coord/commands/audit.py +131 -0
- coord/commands/chat.py +320 -0
- coord/commands/dispatch.py +1780 -0
- coord/commands/dispatch_workers.py +4894 -0
- coord/commands/drive.py +616 -0
- coord/commands/drive_queue.py +1203 -0
- coord/commands/gate_a.py +217 -0
- coord/commands/gates.py +89 -0
- coord/commands/issues.py +681 -0
- coord/commands/lifecycle.py +513 -0
- coord/commands/merge.py +1900 -0
- coord/commands/milestone.py +2081 -0
- coord/commands/plan_followup.py +1243 -0
- coord/commands/plans.py +156 -0
- coord/commands/release.py +2232 -0
- coord/commands/report.py +341 -0
- coord/commands/review.py +1523 -0
- coord/commands/scorecard.py +252 -0
- coord/commands/sessions.py +1930 -0
- coord/commands/setup.py +576 -0
- coord/commands/status.py +2089 -0
- coord/commands/terminal.py +385 -0
- coord/commands/test_gate.py +775 -0
- coord/commands/tui.py +288 -0
- coord/comments.py +718 -0
- coord/config.py +3032 -0
- coord/conflict_fix.py +633 -0
- coord/dao.py +483 -0
- coord/dashboard/__init__.py +0 -0
- coord/dashboard/fixture.py +376 -0
- coord/dashboard/index.html +658 -0
- coord/dashboard/server.py +1894 -0
- coord/dashboard/terminal.py +382 -0
- coord/dashboard/webapp/.gitignore +9 -0
- coord/dashboard/webapp/components.json +17 -0
- coord/dashboard/webapp/dist/assets/Gallery-da3qNiIw.js +71 -0
- coord/dashboard/webapp/dist/assets/Terminal-9CEnUXvW.css +32 -0
- coord/dashboard/webapp/dist/assets/Terminal-skVFCxPU.js +63 -0
- coord/dashboard/webapp/dist/assets/index-DltfZR5f.js +184 -0
- coord/dashboard/webapp/dist/assets/index-Dq4kwTdw.css +1 -0
- coord/dashboard/webapp/dist/assets/workbox-window.prod.es5-BqEJf4Xk.js +2 -0
- coord/dashboard/webapp/dist/icons/icon-192.png +0 -0
- coord/dashboard/webapp/dist/icons/icon-512.png +0 -0
- coord/dashboard/webapp/dist/icons/icon.svg +5 -0
- coord/dashboard/webapp/dist/index.html +38 -0
- coord/dashboard/webapp/dist/manifest.webmanifest +1 -0
- coord/dashboard/webapp/dist/sw.js +1 -0
- coord/dashboard/webapp/dist/workbox-e4022e15.js +1 -0
- coord/dashboard/webapp/e2e/available-gates-terminal.spec.ts +75 -0
- coord/dashboard/webapp/e2e/deep-link.spec.ts +172 -0
- coord/dashboard/webapp/e2e/fixtureServer.ts +155 -0
- coord/dashboard/webapp/e2e/live-update-fixture.spec.ts +113 -0
- coord/dashboard/webapp/e2e/realtime.spec.ts +238 -0
- coord/dashboard/webapp/e2e/shell.spec.ts +309 -0
- coord/dashboard/webapp/e2e/smoke.spec.ts +191 -0
- coord/dashboard/webapp/e2e/terminal.spec.ts +420 -0
- coord/dashboard/webapp/e2e/theme.spec.ts +138 -0
- coord/dashboard/webapp/eslint.config.js +20 -0
- coord/dashboard/webapp/index.html +37 -0
- coord/dashboard/webapp/node_modules/flatted/python/flatted.py +144 -0
- coord/dashboard/webapp/package-lock.json +10584 -0
- coord/dashboard/webapp/package.json +63 -0
- coord/dashboard/webapp/playwright.acceptance.config.ts +166 -0
- coord/dashboard/webapp/playwright.config.ts +93 -0
- coord/dashboard/webapp/postcss.config.js +6 -0
- coord/dashboard/webapp/public/icons/icon-192.png +0 -0
- coord/dashboard/webapp/public/icons/icon-512.png +0 -0
- coord/dashboard/webapp/public/icons/icon.svg +5 -0
- coord/dashboard/webapp/src/App.tsx +140 -0
- coord/dashboard/webapp/src/api/client.ts +199 -0
- coord/dashboard/webapp/src/api/generated.ts +176 -0
- coord/dashboard/webapp/src/components/ConnectionBadge.tsx +52 -0
- coord/dashboard/webapp/src/components/Detail.tsx +800 -0
- coord/dashboard/webapp/src/components/Gallery.tsx +341 -0
- coord/dashboard/webapp/src/components/Home.tsx +435 -0
- coord/dashboard/webapp/src/components/MobileKeyBar.tsx +280 -0
- coord/dashboard/webapp/src/components/PanelHeader.tsx +59 -0
- coord/dashboard/webapp/src/components/PipelineCard.tsx +168 -0
- coord/dashboard/webapp/src/components/SessionCard.tsx +99 -0
- coord/dashboard/webapp/src/components/SessionDetail.tsx +140 -0
- coord/dashboard/webapp/src/components/SessionsList.tsx +81 -0
- coord/dashboard/webapp/src/components/Terminal.tsx +376 -0
- coord/dashboard/webapp/src/components/__tests__/ConnectionBadge.test.tsx +81 -0
- coord/dashboard/webapp/src/components/__tests__/Detail.test.tsx +680 -0
- coord/dashboard/webapp/src/components/__tests__/Gallery.test.tsx +83 -0
- coord/dashboard/webapp/src/components/__tests__/Home.test.tsx +271 -0
- coord/dashboard/webapp/src/components/__tests__/MobileKeyBar.test.tsx +197 -0
- coord/dashboard/webapp/src/components/__tests__/PipelineCard.test.tsx +143 -0
- coord/dashboard/webapp/src/components/__tests__/SessionCard.test.tsx +106 -0
- coord/dashboard/webapp/src/components/__tests__/Terminal.test.tsx +504 -0
- coord/dashboard/webapp/src/components/ui/badge.tsx +41 -0
- coord/dashboard/webapp/src/components/ui/button.tsx +54 -0
- coord/dashboard/webapp/src/components/ui/card.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/dialog.tsx +99 -0
- coord/dashboard/webapp/src/components/ui/dropdown-menu.tsx +189 -0
- coord/dashboard/webapp/src/components/ui/empty-state.tsx +35 -0
- coord/dashboard/webapp/src/components/ui/sheet.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/skeleton.tsx +9 -0
- coord/dashboard/webapp/src/components/ui/tabs.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/theme-provider.tsx +78 -0
- coord/dashboard/webapp/src/components/ui/theme-toggle.tsx +20 -0
- coord/dashboard/webapp/src/components/ui/toast.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/toaster.tsx +30 -0
- coord/dashboard/webapp/src/components/ui/tooltip.tsx +26 -0
- coord/dashboard/webapp/src/components/ui/use-toast.ts +134 -0
- coord/dashboard/webapp/src/index.css +210 -0
- coord/dashboard/webapp/src/lib/pipeline.ts +29 -0
- coord/dashboard/webapp/src/lib/utils.ts +6 -0
- coord/dashboard/webapp/src/main.tsx +46 -0
- coord/dashboard/webapp/src/realtime/RealtimeProvider.tsx +112 -0
- coord/dashboard/webapp/src/realtime/__tests__/RealtimeProvider.test.tsx +189 -0
- coord/dashboard/webapp/src/realtime/__tests__/connection.test.ts +255 -0
- coord/dashboard/webapp/src/realtime/connection.ts +227 -0
- coord/dashboard/webapp/src/realtime/events.ts +100 -0
- coord/dashboard/webapp/src/routes/__tests__/paths.test.ts +92 -0
- coord/dashboard/webapp/src/routes/paths.ts +92 -0
- coord/dashboard/webapp/src/shell/ActivityRail.tsx +335 -0
- coord/dashboard/webapp/src/shell/AppShell.tsx +276 -0
- coord/dashboard/webapp/src/shell/ComingSoon.tsx +33 -0
- coord/dashboard/webapp/src/shell/EmptyDetail.tsx +26 -0
- coord/dashboard/webapp/src/shell/RouteNotFound.tsx +33 -0
- coord/dashboard/webapp/src/shell/ShellLayout.tsx +147 -0
- coord/dashboard/webapp/src/shell/StatusBar.tsx +46 -0
- coord/dashboard/webapp/src/shell/__tests__/ShellLayout.test.tsx +520 -0
- coord/dashboard/webapp/src/shell/__tests__/shellState.test.ts +95 -0
- coord/dashboard/webapp/src/shell/__tests__/stubViewport.ts +40 -0
- coord/dashboard/webapp/src/shell/breakpoints.ts +87 -0
- coord/dashboard/webapp/src/shell/railItems.ts +105 -0
- coord/dashboard/webapp/src/shell/shellState.ts +174 -0
- coord/dashboard/webapp/src/shell/useRegionFocus.ts +95 -0
- coord/dashboard/webapp/src/test-setup.ts +41 -0
- coord/dashboard/webapp/src/vite-env.d.ts +2 -0
- coord/dashboard/webapp/tailwind.config.js +140 -0
- coord/dashboard/webapp/tsconfig.json +25 -0
- coord/dashboard/webapp/tsconfig.node.json +11 -0
- coord/dashboard/webapp/vite.config.ts +71 -0
- coord/db.py +1076 -0
- coord/dead_end.py +332 -0
- coord/deploy/README.md +33 -0
- coord/deploy/coord-agent.service +89 -0
- coord/deploy/coord-db-backup.service +60 -0
- coord/deploy/coord-db-backup.sh +74 -0
- coord/deploy/coord-db-backup.timer +18 -0
- coord/deploy/coord-drive-queue.service +117 -0
- coord/deploy/coord-drive-queue.timer +39 -0
- coord/deploy/coord-notify.service +48 -0
- coord/deploy/coord-notify.timer +24 -0
- coord/deploy/coord-release-propagate.service +83 -0
- coord/deploy/coord-release-propagate.timer +38 -0
- coord/deploy/coord-release-window.service +119 -0
- coord/deploy/coord-release-window.timer +36 -0
- coord/deploy/coord-serve.service +82 -0
- coord/deploy/coord-web-dist-build.service +43 -0
- coord/deploy/coord-web-dist-build.timer +36 -0
- coord/deploy/coord-web.service +125 -0
- coord/deploy_manifest.py +80 -0
- coord/deploy_units.py +384 -0
- coord/deps.py +115 -0
- coord/diagnose.py +1623 -0
- coord/dispatch.py +1009 -0
- coord/dist_name.py +123 -0
- coord/drive.py +3101 -0
- coord/drive_queue.py +2298 -0
- coord/drive_state.py +870 -0
- coord/events.py +381 -0
- coord/failure_class.py +914 -0
- coord/filelock.py +168 -0
- coord/fleet_config_health.py +300 -0
- coord/freshness.py +206 -0
- coord/gate_a.py +469 -0
- coord/gate_b.py +411 -0
- coord/gate_snapshot.py +385 -0
- coord/gates.py +582 -0
- coord/github_ops.py +1954 -0
- coord/goal.py +125 -0
- coord/graph_health.py +348 -0
- coord/health/__init__.py +69 -0
- coord/health/aggregate.py +129 -0
- coord/health/checks/__init__.py +13 -0
- coord/health/checks/agent_install.py +280 -0
- coord/health/checks/cargo_targets.py +171 -0
- coord/health/checks/claude_binary.py +65 -0
- coord/health/checks/deploy_lane_facts.py +458 -0
- coord/health/checks/disk.py +99 -0
- coord/health/checks/fleet_board.py +89 -0
- coord/health/checks/fleet_deploy_lanes.py +469 -0
- coord/health/checks/fleet_phantom.py +69 -0
- coord/health/checks/fleet_unit_drift.py +151 -0
- coord/health/checks/graph.py +192 -0
- coord/health/checks/plan_usage.py +88 -0
- coord/health/checks/repo_state.py +161 -0
- coord/health/checks/spawned_coord.py +465 -0
- coord/health/checks/timer_active.py +254 -0
- coord/health/checks/toolchain.py +547 -0
- coord/health/checks/unit_drift.py +648 -0
- coord/health/checks/unit_enablement.py +171 -0
- coord/health/checks/worktrees.py +96 -0
- coord/health/cli.py +121 -0
- coord/health/context.py +106 -0
- coord/health/fleet_snapshot.py +477 -0
- coord/health/models.py +250 -0
- coord/health/pypi.py +231 -0
- coord/health/registry.py +240 -0
- coord/health/render.py +82 -0
- coord/health/units.py +60 -0
- coord/hooks.py +106 -0
- coord/housekeeping.py +204 -0
- coord/interactive.py +4286 -0
- coord/issue_store.py +1496 -0
- coord/liveness_auditor.py +293 -0
- coord/machine_pause.py +755 -0
- coord/merge_queue.py +4681 -0
- coord/milestone_chat.py +600 -0
- coord/milestone_dispatch.py +943 -0
- coord/milestone_gate.py +709 -0
- coord/milestone_order.py +840 -0
- coord/mock_author.py +334 -0
- coord/models.py +891 -0
- coord/network.py +269 -0
- coord/new_issue_chat.py +229 -0
- coord/notify.py +3226 -0
- coord/openapi.py +404 -0
- coord/overlap_fence.py +133 -0
- coord/parentage.py +200 -0
- coord/parentage_github.py +58 -0
- coord/pipeline.py +481 -0
- coord/plan_parser.py +266 -0
- coord/plans.py +543 -0
- coord/platform_paths.py +43 -0
- coord/pr_body_lint.py +67 -0
- coord/prereqs.py +533 -0
- coord/progress.py +425 -0
- coord/providers/__init__.py +683 -0
- coord/providers/base.py +218 -0
- coord/providers/claude.py +284 -0
- coord/providers/claude_pty.py +610 -0
- coord/providers/opencode.py +896 -0
- coord/reconcile.py +2233 -0
- coord/refine_chat.py +485 -0
- coord/release_cordon.py +525 -0
- coord/release_propagate.py +1176 -0
- coord/release_verify.py +777 -0
- coord/release_window.py +322 -0
- coord/reports.py +1643 -0
- coord/revalidate.py +1101 -0
- coord/review.py +3317 -0
- coord/scorecard.py +484 -0
- coord/serve_app.py +7192 -0
- coord/skills/update-issue/SKILL.md +93 -0
- coord/smoke.py +1030 -0
- coord/split_work.py +210 -0
- coord/stage_projection.py +650 -0
- coord/state.py +5720 -0
- coord/test_author.py +1064 -0
- coord/test_chat.py +352 -0
- coord/test_orchestrator.py +494 -0
- coord/test_report.py +178 -0
- coord/tui_release.py +271 -0
- coord/usage.py +753 -0
- coord/usage_limits.py +358 -0
- coord/usage_rollup.py +709 -0
- 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
|
+
}
|