@walwal-harness/cli 7.1.40 → 7.1.41
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.
- package/HR-Resource/cdo/SKILL.md +9 -1
- package/HR-Resource/ceo/SKILL.md +33 -2
- package/HR-Resource/coo/SKILL.md +9 -1
- package/HR-Resource/cqo/SKILL.md +9 -1
- package/HR-Resource/cto/SKILL.md +10 -2
- package/HR-Resource/ops/SKILL.md +12 -4
- package/HR-Resource/ops-prompt-inspector/SKILL.md +11 -6
- package/HR-Resource/ops-prompt-inspector/assets/prompt-inspector-static.js +180 -0
- package/HR-Resource/ops-prompt-inspector/scripts/setup.py +66 -3
- package/apps/harness-dashboard/app/api/wake/route.ts +41 -0
- package/apps/harness-dashboard/components/Scene.tsx +568 -35
- package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +1 -202
- package/apps/harness-dashboard/lib/__tests__/sandbox-e2e.test.ts +490 -0
- package/apps/harness-dashboard/lib/harness-state.ts +107 -796
- package/apps/harness-dashboard/lib/types.ts +22 -222
- package/apps/harness-dashboard/playwright.config.ts +26 -7
- package/assets/templates/AGENTS-ko.md.template +1 -1
- package/assets/templates/AGENTS.md.template +12 -2
- package/assets/templates/HARNESS.md +2 -0
- package/assets/templates/company-pipeline-manifest.json +5 -14
- package/assets/templates/worker-report.md.template +38 -0
- package/bin/init.js +2 -1
- package/commands/goal.md +4 -2
- package/commands/hot-fix.md +1 -1
- package/commands/submission.md +1 -1
- package/package.json +1 -1
- package/scripts/conductor-tick.sh +12 -0
- package/scripts/harness-agenda.sh +105 -0
- package/scripts/harness-company-block.sh +89 -0
- package/scripts/harness-company-complete.sh +17 -8
- package/scripts/harness-company-cycle.sh +54 -0
- package/scripts/harness-progress-set.sh +8 -2
- package/scripts/harness-stop.sh +76 -0
- package/scripts/harness-wake-control.sh +151 -0
- package/scripts/harness-wake.sh +5 -0
- package/apps/harness-dashboard/components/Drawer.tsx +0 -146
- package/apps/harness-dashboard/components/Header.tsx +0 -40
- package/apps/harness-dashboard/components/MissionTimeline.tsx +0 -214
- package/apps/harness-dashboard/components/OrgTree.tsx +0 -459
- package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +0 -56
- package/apps/harness-dashboard/components/drawer/DesignPreviewTab.tsx +0 -160
- package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +0 -89
- package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +0 -42
- package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +0 -50
- package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +0 -96
- package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +0 -576
- package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +0 -160
- package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +0 -90
- package/apps/harness-dashboard/components/drawer/TracksTab.tsx +0 -47
- package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +0 -95
- package/apps/harness-dashboard/components/three/DeptBoard.tsx +0 -175
- package/apps/harness-dashboard/components/three/Floor3D.tsx +0 -404
- package/apps/harness-dashboard/components/three/GoalCard3D.tsx +0 -123
- package/apps/harness-dashboard/components/three/HypothesisCards.tsx +0 -71
- package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +0 -98
- package/apps/harness-dashboard/components/three/Minifig3D.tsx +0 -391
- package/apps/harness-dashboard/components/three/OpsSiren.tsx +0 -99
- package/apps/harness-dashboard/components/three/Stage3D.tsx +0 -104
- package/apps/harness-dashboard/components/three/TrackRibbons.tsx +0 -81
- package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +0 -16
- package/apps/harness-dashboard/lib/__tests__/iso.test.ts +0 -42
- package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +0 -183
- package/apps/harness-dashboard/lib/agent-roster.ts +0 -42
- package/apps/harness-dashboard/lib/furniture.ts +0 -165
- package/apps/harness-dashboard/lib/i18n.ts +0 -26
- package/apps/harness-dashboard/lib/iso.ts +0 -150
- package/apps/harness-dashboard/lib/markdown.tsx +0 -306
- package/apps/harness-dashboard/lib/path-planning.ts +0 -72
- package/apps/harness-dashboard/lib/state-mapping.ts +0 -146
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { useEffect, useRef } from "react";
|
|
3
|
-
|
|
4
|
-
export type DrawerTab =
|
|
5
|
-
| "mission-flow" // Mission document hierarchy (PRIMARY)
|
|
6
|
-
| "history" // Owner prompt history
|
|
7
|
-
| "gotchas" // Harness gotcha knowledge base
|
|
8
|
-
| "design-preview" // CDO visual sample iframe
|
|
9
|
-
| "mission-doc" // Single doc markdown view
|
|
10
|
-
| "logs"; // Agent log (technical)
|
|
11
|
-
|
|
12
|
-
interface DrawerProps {
|
|
13
|
-
open: boolean;
|
|
14
|
-
tab: DrawerTab;
|
|
15
|
-
title: string;
|
|
16
|
-
onClose: () => void;
|
|
17
|
-
onTabChange: (tab: DrawerTab) => void;
|
|
18
|
-
children: React.ReactNode;
|
|
19
|
-
mode?: "overlay" | "inline";
|
|
20
|
-
scrollResetKey?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const TABS: Array<{ id: DrawerTab; label: string }> = [
|
|
24
|
-
{ id: "mission-flow", label: "Mission Flow" },
|
|
25
|
-
{ id: "history", label: "History" },
|
|
26
|
-
{ id: "gotchas", label: "Gotchas" },
|
|
27
|
-
{ id: "design-preview", label: "Preview" },
|
|
28
|
-
{ id: "mission-doc", label: "Document" },
|
|
29
|
-
{ id: "logs", label: "Agent Log" },
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
export function Drawer({ open, tab, title, onClose, onTabChange, children, mode = "overlay", scrollResetKey }: DrawerProps) {
|
|
33
|
-
const contentRef = useRef<HTMLDivElement | null>(null);
|
|
34
|
-
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
if (!open || mode === "inline") return;
|
|
37
|
-
const onKey = (e: KeyboardEvent) => {
|
|
38
|
-
if (e.key === "Escape") onClose();
|
|
39
|
-
};
|
|
40
|
-
window.addEventListener("keydown", onKey);
|
|
41
|
-
return () => window.removeEventListener("keydown", onKey);
|
|
42
|
-
}, [open, onClose, mode]);
|
|
43
|
-
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
contentRef.current?.scrollTo({ top: 0, left: 0 });
|
|
46
|
-
}, [scrollResetKey]);
|
|
47
|
-
|
|
48
|
-
if (mode === "inline") {
|
|
49
|
-
return (
|
|
50
|
-
<aside
|
|
51
|
-
role="complementary"
|
|
52
|
-
aria-label="Detail panel"
|
|
53
|
-
data-testid="drawer"
|
|
54
|
-
data-open="true"
|
|
55
|
-
className="glass-panel h-full min-h-0 rounded-lg flex flex-col overflow-hidden"
|
|
56
|
-
>
|
|
57
|
-
<header className="flex items-center justify-between border-b border-white/10 px-4 py-3">
|
|
58
|
-
<h2 className="text-sm font-semibold tracking-tight text-gray-100 truncate">
|
|
59
|
-
{title}
|
|
60
|
-
</h2>
|
|
61
|
-
</header>
|
|
62
|
-
<nav className="flex flex-wrap border-b border-white/10 px-2 py-2 text-[11px] font-mono">
|
|
63
|
-
{TABS.map((t) => (
|
|
64
|
-
<button
|
|
65
|
-
key={t.id}
|
|
66
|
-
type="button"
|
|
67
|
-
data-testid={`drawer-tab-${t.id}`}
|
|
68
|
-
data-active={t.id === tab}
|
|
69
|
-
onClick={() => onTabChange(t.id)}
|
|
70
|
-
className={`min-w-[33%] flex-1 rounded-md px-2 py-2 transition-colors ${
|
|
71
|
-
t.id === tab
|
|
72
|
-
? "bg-white/12 text-gray-100 shadow-inner"
|
|
73
|
-
: "text-gray-500 hover:bg-white/6 hover:text-gray-200"
|
|
74
|
-
}`}
|
|
75
|
-
>
|
|
76
|
-
{t.label}
|
|
77
|
-
</button>
|
|
78
|
-
))}
|
|
79
|
-
</nav>
|
|
80
|
-
<div ref={contentRef} className="scrollbar-hidden flex-1 overflow-y-auto px-4 py-3 text-xs text-gray-200 font-mono">
|
|
81
|
-
{children}
|
|
82
|
-
</div>
|
|
83
|
-
</aside>
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<>
|
|
89
|
-
{/* Click-outside backdrop. Pointer-events flip with `open`. */}
|
|
90
|
-
<div
|
|
91
|
-
data-testid="drawer-backdrop"
|
|
92
|
-
aria-hidden="true"
|
|
93
|
-
onClick={onClose}
|
|
94
|
-
className={`fixed inset-0 bg-black/30 transition-opacity duration-200 ${
|
|
95
|
-
open ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
|
|
96
|
-
}`}
|
|
97
|
-
/>
|
|
98
|
-
<aside
|
|
99
|
-
role="complementary"
|
|
100
|
-
aria-label="Detail panel"
|
|
101
|
-
aria-hidden={!open}
|
|
102
|
-
data-testid="drawer"
|
|
103
|
-
data-open={open}
|
|
104
|
-
className={`fixed top-0 right-0 h-[100dvh] w-full sm:w-[50vw] sm:min-w-[600px] bg-brick-bg border-l border-brick-wall shadow-2xl flex flex-col transition-transform duration-300 ease-out ${
|
|
105
|
-
open ? "translate-x-0" : "translate-x-full"
|
|
106
|
-
}`}
|
|
107
|
-
>
|
|
108
|
-
<header className="flex items-center justify-between border-b border-brick-wall px-4 py-3">
|
|
109
|
-
<h2 className="text-sm font-mono uppercase tracking-widest text-gray-200">
|
|
110
|
-
{title}
|
|
111
|
-
</h2>
|
|
112
|
-
<button
|
|
113
|
-
type="button"
|
|
114
|
-
onClick={onClose}
|
|
115
|
-
aria-label="Close panel"
|
|
116
|
-
data-testid="drawer-close"
|
|
117
|
-
className="rounded p-1 text-gray-400 hover:text-gray-100 hover:bg-brick-wall/40"
|
|
118
|
-
>
|
|
119
|
-
✕
|
|
120
|
-
</button>
|
|
121
|
-
</header>
|
|
122
|
-
<nav className="flex flex-wrap border-b border-brick-wall text-[11px] font-mono">
|
|
123
|
-
{TABS.map((t) => (
|
|
124
|
-
<button
|
|
125
|
-
key={t.id}
|
|
126
|
-
type="button"
|
|
127
|
-
data-testid={`drawer-tab-${t.id}`}
|
|
128
|
-
data-active={t.id === tab}
|
|
129
|
-
onClick={() => onTabChange(t.id)}
|
|
130
|
-
className={`min-w-[33%] flex-1 px-2 py-2 transition-colors ${
|
|
131
|
-
t.id === tab
|
|
132
|
-
? "bg-brick-wall/50 text-gray-100 border-b-2 border-aura-typing"
|
|
133
|
-
: "text-gray-500 hover:text-gray-200"
|
|
134
|
-
}`}
|
|
135
|
-
>
|
|
136
|
-
{t.label}
|
|
137
|
-
</button>
|
|
138
|
-
))}
|
|
139
|
-
</nav>
|
|
140
|
-
<div ref={contentRef} className="scrollbar-hidden flex-1 overflow-y-auto px-4 py-3 text-xs text-gray-200 font-mono">
|
|
141
|
-
{children}
|
|
142
|
-
</div>
|
|
143
|
-
</aside>
|
|
144
|
-
</>
|
|
145
|
-
);
|
|
146
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { Wordmark } from "./Wordmark";
|
|
3
|
-
import { resolveHarnessRoot } from "@/lib/harness-root";
|
|
4
|
-
|
|
5
|
-
export function Header() {
|
|
6
|
-
const root = resolveHarnessRoot();
|
|
7
|
-
const projectName = path.basename(root);
|
|
8
|
-
return (
|
|
9
|
-
<header className="border-b border-brick-wall bg-brick-bg/80 backdrop-blur">
|
|
10
|
-
<div className="mx-auto flex max-w-[1280px] items-center justify-between px-6 py-4">
|
|
11
|
-
<div className="flex items-baseline gap-4">
|
|
12
|
-
<Wordmark />
|
|
13
|
-
<span className="hidden items-baseline gap-2 sm:inline-flex">
|
|
14
|
-
<span
|
|
15
|
-
className="rounded bg-brick-wall/40 px-2 py-0.5 font-mono text-xs font-semibold text-amber-300"
|
|
16
|
-
title={root}
|
|
17
|
-
>
|
|
18
|
-
{projectName}
|
|
19
|
-
</span>
|
|
20
|
-
<span className="text-xs text-gray-400">walwal-harness 라이브 운영 대시보드</span>
|
|
21
|
-
</span>
|
|
22
|
-
</div>
|
|
23
|
-
<nav className="flex items-center gap-3 text-xs font-mono text-gray-500">
|
|
24
|
-
<span className="inline-flex items-center gap-1">
|
|
25
|
-
<span className="size-2 rounded-full bg-aura-idle" /> idle
|
|
26
|
-
</span>
|
|
27
|
-
<span className="inline-flex items-center gap-1">
|
|
28
|
-
<span className="size-2 rounded-full bg-aura-typing" /> typing
|
|
29
|
-
</span>
|
|
30
|
-
<span className="inline-flex items-center gap-1">
|
|
31
|
-
<span className="size-2 rounded-full bg-aura-talking" /> talking
|
|
32
|
-
</span>
|
|
33
|
-
<span className="inline-flex items-center gap-1">
|
|
34
|
-
<span className="size-2 rounded-full bg-aura-alert" /> alert
|
|
35
|
-
</span>
|
|
36
|
-
</nav>
|
|
37
|
-
</div>
|
|
38
|
-
</header>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import type { MissionDoc } from "@/lib/types";
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
missions: MissionDoc[];
|
|
6
|
-
activeMissionId: string | null;
|
|
7
|
-
onMissionClick: (mission: MissionDoc) => void;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
type ChipRole =
|
|
11
|
-
| "owner"
|
|
12
|
-
| "ceo"
|
|
13
|
-
| "coo"
|
|
14
|
-
| "cdo"
|
|
15
|
-
| "cto"
|
|
16
|
-
| "cqo"
|
|
17
|
-
| "ops"
|
|
18
|
-
| "worker";
|
|
19
|
-
|
|
20
|
-
function chipColor(role: ChipRole): string {
|
|
21
|
-
switch (role) {
|
|
22
|
-
case "owner":
|
|
23
|
-
return "bg-amber-500/15 text-amber-300 border-amber-500/30";
|
|
24
|
-
case "ceo":
|
|
25
|
-
return "bg-cyan-400/15 text-cyan-300 border-cyan-400/30";
|
|
26
|
-
case "coo":
|
|
27
|
-
return "bg-emerald-500/15 text-emerald-300 border-emerald-500/30";
|
|
28
|
-
case "cdo":
|
|
29
|
-
return "bg-purple-500/15 text-purple-300 border-purple-500/30";
|
|
30
|
-
case "cto":
|
|
31
|
-
return "bg-blue-400/15 text-blue-300 border-blue-400/30";
|
|
32
|
-
case "cqo":
|
|
33
|
-
return "bg-amber-400/15 text-amber-300 border-amber-400/30";
|
|
34
|
-
case "ops":
|
|
35
|
-
return "bg-rose-400/15 text-rose-300 border-rose-400/30";
|
|
36
|
-
case "worker":
|
|
37
|
-
return "bg-gray-500/15 text-gray-300 border-gray-500/30";
|
|
38
|
-
default:
|
|
39
|
-
return "bg-gray-500/15 text-gray-400 border-gray-600/30";
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function Chip({
|
|
44
|
-
role,
|
|
45
|
-
label,
|
|
46
|
-
badge,
|
|
47
|
-
}: {
|
|
48
|
-
role: ChipRole;
|
|
49
|
-
label: string;
|
|
50
|
-
badge?: string;
|
|
51
|
-
}) {
|
|
52
|
-
return (
|
|
53
|
-
<span
|
|
54
|
-
className={`inline-flex items-center gap-1 rounded border px-1.5 py-0.5 font-mono text-[9px] ${chipColor(role)}`}
|
|
55
|
-
>
|
|
56
|
-
{label}
|
|
57
|
-
{badge && <span className="opacity-80">{badge}</span>}
|
|
58
|
-
</span>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function Arrow() {
|
|
63
|
-
return <span className="text-[10px] text-gray-600 shrink-0">→</span>;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function buildFlowChain(mission: MissionDoc): React.ReactNode {
|
|
67
|
-
// Pre-worker CXX: planning phase (before implementation)
|
|
68
|
-
const preWorker: Array<{ key: "coo" | "cdo" | "cto"; role: ChipRole }> = [
|
|
69
|
-
{ key: "coo", role: "coo" },
|
|
70
|
-
{ key: "cdo", role: "cdo" },
|
|
71
|
-
{ key: "cto", role: "cto" },
|
|
72
|
-
];
|
|
73
|
-
// Post-worker CXX: verification phase (after implementation)
|
|
74
|
-
const postWorker: Array<{ key: "cqo" | "ops"; role: ChipRole }> = [
|
|
75
|
-
{ key: "cqo", role: "cqo" },
|
|
76
|
-
{ key: "ops", role: "ops" },
|
|
77
|
-
];
|
|
78
|
-
|
|
79
|
-
const isCqoAccepted =
|
|
80
|
-
typeof mission.cqo === "string" && mission.cqo.includes("ACCEPTED");
|
|
81
|
-
|
|
82
|
-
const chips: React.ReactNode[] = [];
|
|
83
|
-
let key = 0;
|
|
84
|
-
|
|
85
|
-
chips.push(<Chip key={key++} role="owner" label="Owner" />);
|
|
86
|
-
chips.push(<Arrow key={key++} />);
|
|
87
|
-
chips.push(<Chip key={key++} role="ceo" label="CEO" />);
|
|
88
|
-
|
|
89
|
-
// Planning phase: COO, CDO, CTO
|
|
90
|
-
for (const { key: roleKey, role } of preWorker) {
|
|
91
|
-
if (mission.cxxPresent.includes(roleKey)) {
|
|
92
|
-
chips.push(<Arrow key={key++} />);
|
|
93
|
-
chips.push(<Chip key={key++} role={role} label={roleKey.toUpperCase()} />);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// CTO-owned implementation workers. Legacy flat workers remain visible here
|
|
98
|
-
// with owner=unknown so protocol bypasses are not hidden.
|
|
99
|
-
for (const worker of mission.workers.filter((w) => w.owner === "cto" || w.owner === "unknown")) {
|
|
100
|
-
chips.push(<Arrow key={key++} />);
|
|
101
|
-
const badge = worker.status === "COMPLETE" ? "✅" : "⏳";
|
|
102
|
-
const shortName =
|
|
103
|
-
worker.name.length > 12 ? worker.name.slice(0, 12) + "…" : worker.name;
|
|
104
|
-
chips.push(
|
|
105
|
-
<Chip key={key++} role="worker" label={shortName} badge={badge} />
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Verification phase: CQO, OPS
|
|
110
|
-
for (const { key: roleKey, role } of postWorker) {
|
|
111
|
-
if (mission.cxxPresent.includes(roleKey)) {
|
|
112
|
-
chips.push(<Arrow key={key++} />);
|
|
113
|
-
const badge = roleKey === "cqo" && isCqoAccepted ? "✅" : undefined;
|
|
114
|
-
chips.push(
|
|
115
|
-
<Chip key={key++} role={role} label={roleKey.toUpperCase()} badge={badge} />
|
|
116
|
-
);
|
|
117
|
-
if (roleKey === "cqo") {
|
|
118
|
-
for (const worker of mission.workers.filter((w) => w.owner === "cqo")) {
|
|
119
|
-
chips.push(<Arrow key={key++} />);
|
|
120
|
-
const workerBadge = worker.status === "COMPLETE" ? "✅" : "⏳";
|
|
121
|
-
const shortName =
|
|
122
|
-
worker.name.length > 12 ? worker.name.slice(0, 12) + "…" : worker.name;
|
|
123
|
-
chips.push(
|
|
124
|
-
<Chip key={key++} role="worker" label={shortName} badge={workerBadge} />
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return (
|
|
132
|
-
<div className="flex flex-wrap items-center gap-1 min-w-0">{chips}</div>
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function MissionTimeline({
|
|
137
|
-
missions,
|
|
138
|
-
activeMissionId,
|
|
139
|
-
onMissionClick,
|
|
140
|
-
}: Props) {
|
|
141
|
-
if (!missions.length) {
|
|
142
|
-
return (
|
|
143
|
-
<div className="glass-panel rounded-lg px-3 py-4 text-center text-[11px] text-gray-500">
|
|
144
|
-
No missions found in .harness/documents/
|
|
145
|
-
</div>
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return (
|
|
150
|
-
<div className="space-y-1.5">
|
|
151
|
-
{missions.map((mission) => {
|
|
152
|
-
const isActive = mission.missionId === activeMissionId;
|
|
153
|
-
const typeBadge =
|
|
154
|
-
mission.type === "hotfix"
|
|
155
|
-
? "[🔥 hot-fix]"
|
|
156
|
-
: mission.type === "submission"
|
|
157
|
-
? "[+ submission]"
|
|
158
|
-
: mission.type === "goal" || mission.type === "feature"
|
|
159
|
-
? "[✦ goal]"
|
|
160
|
-
: "[— unknown]";
|
|
161
|
-
const shortId =
|
|
162
|
-
mission.missionId.length > 28
|
|
163
|
-
? mission.missionId.slice(0, 28) + "…"
|
|
164
|
-
: mission.missionId;
|
|
165
|
-
|
|
166
|
-
let dateStr = "—";
|
|
167
|
-
try {
|
|
168
|
-
dateStr = new Date(mission.ts).toLocaleDateString(undefined, {
|
|
169
|
-
month: "short",
|
|
170
|
-
day: "numeric",
|
|
171
|
-
});
|
|
172
|
-
} catch {
|
|
173
|
-
// ignore invalid date
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return (
|
|
177
|
-
<button
|
|
178
|
-
key={mission.missionId}
|
|
179
|
-
type="button"
|
|
180
|
-
onClick={() => onMissionClick(mission)}
|
|
181
|
-
className={`w-full rounded-lg px-3 py-2.5 text-left transition-colors ${
|
|
182
|
-
isActive
|
|
183
|
-
? "glass-panel"
|
|
184
|
-
: "border border-white/10 bg-white/[0.035] hover:border-white/20"
|
|
185
|
-
}`}
|
|
186
|
-
>
|
|
187
|
-
<div className="flex items-start justify-between gap-2 mb-1.5">
|
|
188
|
-
<div className="flex items-center gap-2 min-w-0">
|
|
189
|
-
<span
|
|
190
|
-
className={`shrink-0 font-mono text-[9px] rounded px-1.5 py-0.5 border ${
|
|
191
|
-
mission.type === "hotfix"
|
|
192
|
-
? "bg-rose-500/15 text-rose-300 border-rose-500/30"
|
|
193
|
-
: mission.type === "submission"
|
|
194
|
-
? "bg-sky-500/15 text-sky-300 border-sky-500/30"
|
|
195
|
-
: "bg-emerald-500/15 text-emerald-300 border-emerald-500/30"
|
|
196
|
-
}`}
|
|
197
|
-
>
|
|
198
|
-
{typeBadge}
|
|
199
|
-
</span>
|
|
200
|
-
<span className="font-mono text-[10px] text-gray-200 truncate">
|
|
201
|
-
{shortId}
|
|
202
|
-
</span>
|
|
203
|
-
</div>
|
|
204
|
-
<span className="shrink-0 font-mono text-[9px] text-gray-500">
|
|
205
|
-
{dateStr}
|
|
206
|
-
</span>
|
|
207
|
-
</div>
|
|
208
|
-
{buildFlowChain(mission)}
|
|
209
|
-
</button>
|
|
210
|
-
);
|
|
211
|
-
})}
|
|
212
|
-
</div>
|
|
213
|
-
);
|
|
214
|
-
}
|