@walwal-harness/cli 7.1.40 → 7.1.42
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/metrics/route.ts +17 -0
- package/apps/harness-dashboard/app/api/wake/route.ts +41 -0
- package/apps/harness-dashboard/app/brick.css +482 -0
- package/apps/harness-dashboard/app/layout.tsx +13 -0
- package/apps/harness-dashboard/app/page.tsx +2 -2
- package/apps/harness-dashboard/components/Scene.tsx +690 -36
- package/apps/harness-dashboard/components/brick/BrickDashboard.tsx +149 -0
- package/apps/harness-dashboard/components/brick/reports.tsx +291 -0
- package/apps/harness-dashboard/components/brick/tweaks.tsx +738 -0
- package/apps/harness-dashboard/components/brick/ui.tsx +463 -0
- package/apps/harness-dashboard/components/brick/views-graph.tsx +305 -0
- package/apps/harness-dashboard/components/brick/views-grid.tsx +543 -0
- package/apps/harness-dashboard/components/brick/views-timeline.tsx +238 -0
- 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/brick/adapter.ts +423 -0
- package/apps/harness-dashboard/lib/brick/contract.ts +247 -0
- package/apps/harness-dashboard/lib/harness-state.ts +107 -796
- package/apps/harness-dashboard/lib/metrics/sampler.ts +545 -0
- 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/scripts/harness-worker-dispatch.sh +31 -7
- 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
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/* =============================================================
|
|
4
|
+
walwal-harness · BRICK OFFICE shared UI primitives + chrome
|
|
5
|
+
Ported from design_handoff_harness_dashboard/harness/ui.jsx.
|
|
6
|
+
|
|
7
|
+
Babel-inline globals -> ES modules. No window.* access.
|
|
8
|
+
classNames/CSS preserved verbatim (CSS lives in app/brick.css).
|
|
9
|
+
`statusColor` lives in lib/brick/contract — imported here.
|
|
10
|
+
|
|
11
|
+
Exports: StatusDot, Pill, Label, Panel, Spark, Bars, AgentGlyph,
|
|
12
|
+
Header, Sidebar.
|
|
13
|
+
============================================================= */
|
|
14
|
+
|
|
15
|
+
import React, { useState } from "react";
|
|
16
|
+
import {
|
|
17
|
+
statusColor,
|
|
18
|
+
type AgentDef,
|
|
19
|
+
type ContractMetrics,
|
|
20
|
+
type ContractAlert,
|
|
21
|
+
} from "../../lib/brick/contract";
|
|
22
|
+
import type { MissionDoc, OwnerPromptEntry } from "../../lib/types";
|
|
23
|
+
|
|
24
|
+
/* ---- the three top-level views (COMMAND / ORCHESTRATION / TIMELINE) --- */
|
|
25
|
+
export type BrickView = "grid" | "graph" | "timeline";
|
|
26
|
+
|
|
27
|
+
/* ---- StatusDot -------------------------------------------------------- */
|
|
28
|
+
export interface StatusDotProps {
|
|
29
|
+
status: string;
|
|
30
|
+
/** Optional override colour; defaults to `statusColor(status)`. */
|
|
31
|
+
hue?: string;
|
|
32
|
+
size?: number;
|
|
33
|
+
pulse?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function StatusDot({
|
|
37
|
+
status,
|
|
38
|
+
hue,
|
|
39
|
+
size = 8,
|
|
40
|
+
pulse = true,
|
|
41
|
+
}: StatusDotProps): React.JSX.Element {
|
|
42
|
+
const c = hue || statusColor(status);
|
|
43
|
+
const animate =
|
|
44
|
+
pulse &&
|
|
45
|
+
(status === "live" ||
|
|
46
|
+
status === "typing" ||
|
|
47
|
+
status === "talking" ||
|
|
48
|
+
status === "active" ||
|
|
49
|
+
status === "alert");
|
|
50
|
+
return (
|
|
51
|
+
<span className="dot-wrap" style={{ width: size, height: size }}>
|
|
52
|
+
{animate && <span className="dot-pulse" style={{ background: c }} />}
|
|
53
|
+
<span
|
|
54
|
+
className="dot-core"
|
|
55
|
+
style={{ background: c, width: size, height: size }}
|
|
56
|
+
/>
|
|
57
|
+
</span>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ---- Pill ------------------------------------------------------------- */
|
|
62
|
+
export interface PillProps {
|
|
63
|
+
children: React.ReactNode;
|
|
64
|
+
tone?: "mute" | "ok" | "alert";
|
|
65
|
+
glow?: string;
|
|
66
|
+
onClick?: () => void;
|
|
67
|
+
active?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function Pill({
|
|
71
|
+
children,
|
|
72
|
+
tone = "mute",
|
|
73
|
+
glow,
|
|
74
|
+
onClick,
|
|
75
|
+
active,
|
|
76
|
+
}: PillProps): React.JSX.Element {
|
|
77
|
+
return (
|
|
78
|
+
<span
|
|
79
|
+
className={"pill pill-" + tone + (active ? " is-active" : "")}
|
|
80
|
+
onClick={onClick}
|
|
81
|
+
style={onClick ? { cursor: "pointer" } : undefined}
|
|
82
|
+
>
|
|
83
|
+
{glow && <span className="pill-glow" style={{ background: glow }} />}
|
|
84
|
+
{children}
|
|
85
|
+
</span>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ---- Label ------------------------------------------------------------ */
|
|
90
|
+
export interface LabelProps {
|
|
91
|
+
children: React.ReactNode;
|
|
92
|
+
right?: React.ReactNode;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function Label({ children, right }: LabelProps): React.JSX.Element {
|
|
96
|
+
return (
|
|
97
|
+
<div className="seclabel">
|
|
98
|
+
<span>{children}</span>
|
|
99
|
+
{right != null && <span className="seclabel-right">{right}</span>}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* ---- Panel ------------------------------------------------------------ */
|
|
105
|
+
export interface PanelProps {
|
|
106
|
+
title?: React.ReactNode;
|
|
107
|
+
right?: React.ReactNode;
|
|
108
|
+
children: React.ReactNode;
|
|
109
|
+
className?: string;
|
|
110
|
+
flush?: boolean;
|
|
111
|
+
scroll?: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function Panel({
|
|
115
|
+
title,
|
|
116
|
+
right,
|
|
117
|
+
children,
|
|
118
|
+
className = "",
|
|
119
|
+
flush,
|
|
120
|
+
scroll,
|
|
121
|
+
}: PanelProps): React.JSX.Element {
|
|
122
|
+
return (
|
|
123
|
+
<section className={"panel " + className}>
|
|
124
|
+
{title && <Label right={right}>{title}</Label>}
|
|
125
|
+
<div
|
|
126
|
+
className={
|
|
127
|
+
"panel-body" + (flush ? " flush" : "") + (scroll ? " scroll" : "")
|
|
128
|
+
}
|
|
129
|
+
>
|
|
130
|
+
{children}
|
|
131
|
+
</div>
|
|
132
|
+
</section>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ---- Spark (SVG line/area sparkline) ---------------------------------- */
|
|
137
|
+
export interface SparkProps {
|
|
138
|
+
data: number[];
|
|
139
|
+
color?: string;
|
|
140
|
+
w?: number;
|
|
141
|
+
h?: number;
|
|
142
|
+
fill?: boolean;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function Spark({
|
|
146
|
+
data,
|
|
147
|
+
color = "#4cc2ff",
|
|
148
|
+
w = 80,
|
|
149
|
+
h = 22,
|
|
150
|
+
fill = true,
|
|
151
|
+
}: SparkProps): React.JSX.Element | null {
|
|
152
|
+
if (!data || !data.length) return null;
|
|
153
|
+
const max = Math.max(...data, 1);
|
|
154
|
+
const min = Math.min(...data, 0);
|
|
155
|
+
const span = max - min || 1;
|
|
156
|
+
const step = w / (data.length - 1);
|
|
157
|
+
const pts = data.map(
|
|
158
|
+
(v, i): [number, number] => [i * step, h - ((v - min) / span) * (h - 3) - 1.5]
|
|
159
|
+
);
|
|
160
|
+
const line = pts
|
|
161
|
+
.map((p, i) => (i ? "L" : "M") + p[0].toFixed(1) + " " + p[1].toFixed(1))
|
|
162
|
+
.join(" ");
|
|
163
|
+
const area = line + ` L${w} ${h} L0 ${h} Z`;
|
|
164
|
+
const gid = "sg" + color.replace("#", "");
|
|
165
|
+
return (
|
|
166
|
+
<svg
|
|
167
|
+
width={w}
|
|
168
|
+
height={h}
|
|
169
|
+
className="spark"
|
|
170
|
+
preserveAspectRatio="none"
|
|
171
|
+
viewBox={`0 0 ${w} ${h}`}
|
|
172
|
+
>
|
|
173
|
+
<defs>
|
|
174
|
+
<linearGradient id={gid} x1="0" y1="0" x2="0" y2="1">
|
|
175
|
+
<stop offset="0" stopColor={color} stopOpacity="0.28" />
|
|
176
|
+
<stop offset="1" stopColor={color} stopOpacity="0" />
|
|
177
|
+
</linearGradient>
|
|
178
|
+
</defs>
|
|
179
|
+
{fill && <path d={area} fill={`url(#${gid})`} />}
|
|
180
|
+
<path
|
|
181
|
+
d={line}
|
|
182
|
+
fill="none"
|
|
183
|
+
stroke={color}
|
|
184
|
+
strokeWidth="1.4"
|
|
185
|
+
strokeLinejoin="round"
|
|
186
|
+
/>
|
|
187
|
+
</svg>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* ---- Bars (bar-style sparkline, for spark counts) --------------------- */
|
|
192
|
+
export interface BarsProps {
|
|
193
|
+
data: number[];
|
|
194
|
+
color?: string;
|
|
195
|
+
w?: number;
|
|
196
|
+
h?: number;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function Bars({
|
|
200
|
+
data,
|
|
201
|
+
color = "#4cc2ff",
|
|
202
|
+
w = 80,
|
|
203
|
+
h = 22,
|
|
204
|
+
}: BarsProps): React.JSX.Element {
|
|
205
|
+
const max = Math.max(...data, 1);
|
|
206
|
+
const bw = w / data.length;
|
|
207
|
+
return (
|
|
208
|
+
<svg width={w} height={h} className="spark" viewBox={`0 0 ${w} ${h}`}>
|
|
209
|
+
{data.map((v, i) => {
|
|
210
|
+
const bh = Math.max(1, (v / max) * (h - 2));
|
|
211
|
+
return (
|
|
212
|
+
<rect
|
|
213
|
+
key={i}
|
|
214
|
+
x={i * bw + 0.5}
|
|
215
|
+
y={h - bh}
|
|
216
|
+
width={Math.max(1, bw - 1)}
|
|
217
|
+
height={bh}
|
|
218
|
+
fill={color}
|
|
219
|
+
opacity={0.35 + 0.65 * (i / data.length)}
|
|
220
|
+
rx="0.5"
|
|
221
|
+
/>
|
|
222
|
+
);
|
|
223
|
+
})}
|
|
224
|
+
</svg>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* ---- AgentGlyph ------------------------------------------------------- */
|
|
229
|
+
export interface AgentGlyphProps {
|
|
230
|
+
agent: Pick<AgentDef, "name" | "hue">;
|
|
231
|
+
size?: number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function AgentGlyph({
|
|
235
|
+
agent,
|
|
236
|
+
size = 26,
|
|
237
|
+
}: AgentGlyphProps): React.JSX.Element {
|
|
238
|
+
return (
|
|
239
|
+
<span
|
|
240
|
+
className="glyph"
|
|
241
|
+
style={{ width: size, height: size, color: agent.hue, borderColor: agent.hue + "55" }}
|
|
242
|
+
>
|
|
243
|
+
{agent.name}
|
|
244
|
+
</span>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* ---- Header ----------------------------------------------------------- */
|
|
249
|
+
export interface HeaderProps {
|
|
250
|
+
metrics: ContractMetrics;
|
|
251
|
+
view: BrickView;
|
|
252
|
+
setView: (v: BrickView) => void;
|
|
253
|
+
alerts: ContractAlert[];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const HEADER_VIEWS: ReadonlyArray<{ id: BrickView; label: string }> = [
|
|
257
|
+
{ id: "grid", label: "COMMAND" },
|
|
258
|
+
{ id: "graph", label: "ORCHESTRATION" },
|
|
259
|
+
{ id: "timeline", label: "TIMELINE" },
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
const HEADER_LEGEND: ReadonlyArray<string> = [
|
|
263
|
+
"idle",
|
|
264
|
+
"typing",
|
|
265
|
+
"talking",
|
|
266
|
+
"alert",
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
export function Header({
|
|
270
|
+
metrics,
|
|
271
|
+
view,
|
|
272
|
+
setView,
|
|
273
|
+
alerts,
|
|
274
|
+
}: HeaderProps): React.JSX.Element {
|
|
275
|
+
return (
|
|
276
|
+
<header className="hdr">
|
|
277
|
+
<div className="hdr-left">
|
|
278
|
+
<div className="logo">
|
|
279
|
+
WALWAL<span className="logo-dot">·</span>HARNESS
|
|
280
|
+
</div>
|
|
281
|
+
<span className="hdr-tag">ai_camera_web</span>
|
|
282
|
+
<span className="hdr-crumb">live operations dashboard</span>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<nav className="viewswitch">
|
|
286
|
+
{HEADER_VIEWS.map((v) => (
|
|
287
|
+
<button
|
|
288
|
+
key={v.id}
|
|
289
|
+
className={"vs-btn" + (view === v.id ? " is-active" : "")}
|
|
290
|
+
onClick={() => setView(v.id)}
|
|
291
|
+
>
|
|
292
|
+
{v.label}
|
|
293
|
+
</button>
|
|
294
|
+
))}
|
|
295
|
+
</nav>
|
|
296
|
+
|
|
297
|
+
<div className="hdr-right">
|
|
298
|
+
<Pill tone="ok" glow="#3fd17a">
|
|
299
|
+
SSE <b>{metrics.sse}</b>
|
|
300
|
+
</Pill>
|
|
301
|
+
<Pill tone="mute">
|
|
302
|
+
Hot-fix <b>{metrics.hotfix}</b>
|
|
303
|
+
</Pill>
|
|
304
|
+
{alerts.length > 0 && (
|
|
305
|
+
<Pill tone="alert" glow="#f0506b">
|
|
306
|
+
alerts <b>{alerts.length}</b>
|
|
307
|
+
</Pill>
|
|
308
|
+
)}
|
|
309
|
+
<span className="legend">
|
|
310
|
+
{HEADER_LEGEND.map((s) => (
|
|
311
|
+
<span key={s} className="legend-item">
|
|
312
|
+
<StatusDot status={s} size={6} pulse={false} />
|
|
313
|
+
{s}
|
|
314
|
+
</span>
|
|
315
|
+
))}
|
|
316
|
+
</span>
|
|
317
|
+
</div>
|
|
318
|
+
</header>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* ---- Sidebar ---------------------------------------------------------- */
|
|
323
|
+
|
|
324
|
+
/** One row in the workflow-history list. */
|
|
325
|
+
export interface SidebarEntry {
|
|
326
|
+
/** Stable id for selection / React key. */
|
|
327
|
+
id: string;
|
|
328
|
+
/** The displayed tag, e.g. "GOAL" | "HOT-FIX" | "SUBMIT" | "SESSION". */
|
|
329
|
+
tag: string;
|
|
330
|
+
/** The displayed workflow name. */
|
|
331
|
+
name: string;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** The mockup's static look (used when no real data is supplied). */
|
|
335
|
+
const GOAL_HISTORY_FALLBACK: ReadonlyArray<SidebarEntry> = [
|
|
336
|
+
{ id: "goal-3-test-coverage-scenario", tag: "GOAL", name: "goal-3-test-coverage-scenario" },
|
|
337
|
+
{ id: "ops-dev-session", tag: "SESSION", name: "ops-dev-session" },
|
|
338
|
+
{ id: "goal-2-rule-preset", tag: "GOAL", name: "goal-2-rule-preset" },
|
|
339
|
+
{ id: "hotfix-6-preset-apply", tag: "HOT-FIX", name: "hotfix-6-preset-apply" },
|
|
340
|
+
{ id: "hotfix-5-preset-save", tag: "HOT-FIX", name: "hotfix-5-preset-save" },
|
|
341
|
+
{ id: "hotfix-4-preset-apply", tag: "HOT-FIX", name: "hotfix-4-preset-apply" },
|
|
342
|
+
{ id: "hotfix-3-preset-save", tag: "HOT-FIX", name: "hotfix-3-preset-save" },
|
|
343
|
+
{ id: "hotfix-2-rename-safe", tag: "HOT-FIX", name: "hotfix-2-rename-safe" },
|
|
344
|
+
{ id: "hotfix-1-preset-apply", tag: "HOT-FIX", name: "hotfix-1-preset-apply" },
|
|
345
|
+
{ id: "submission-1-preset", tag: "SUBMIT", name: "submission-1-preset" },
|
|
346
|
+
{ id: "goal-1-prd-spec-docs", tag: "GOAL", name: "goal-1-prd-spec-docs" },
|
|
347
|
+
];
|
|
348
|
+
|
|
349
|
+
/** Map a real mission type to a sidebar display tag. */
|
|
350
|
+
function missionTag(type: MissionDoc["type"]): string {
|
|
351
|
+
switch (type) {
|
|
352
|
+
case "goal":
|
|
353
|
+
return "GOAL";
|
|
354
|
+
case "hotfix":
|
|
355
|
+
return "HOT-FIX";
|
|
356
|
+
case "submission":
|
|
357
|
+
return "SUBMIT";
|
|
358
|
+
case "feature":
|
|
359
|
+
return "GOAL";
|
|
360
|
+
default:
|
|
361
|
+
return "SESSION";
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Map a real owner-prompt type to a sidebar display tag. */
|
|
366
|
+
function promptTag(type: OwnerPromptEntry["type"]): string {
|
|
367
|
+
switch (type) {
|
|
368
|
+
case "goal":
|
|
369
|
+
return "GOAL";
|
|
370
|
+
case "hot-fix":
|
|
371
|
+
return "HOT-FIX";
|
|
372
|
+
case "submission":
|
|
373
|
+
return "SUBMIT";
|
|
374
|
+
default:
|
|
375
|
+
return "SESSION";
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** CSS class suffix for a tag, matching the mockup: lowercase, no dash. */
|
|
380
|
+
function tagClass(tag: string): string {
|
|
381
|
+
return "tag-" + tag.toLowerCase().replace("-", "");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Build the workflow-history list from real data when available.
|
|
386
|
+
* Prefers missions (richer label/type); falls back to ownerHistory; and
|
|
387
|
+
* finally to the static mockup list so the design renders unchanged offline.
|
|
388
|
+
*/
|
|
389
|
+
export function buildSidebarEntries(
|
|
390
|
+
missions?: MissionDoc[],
|
|
391
|
+
ownerHistory?: OwnerPromptEntry[]
|
|
392
|
+
): SidebarEntry[] {
|
|
393
|
+
if (missions && missions.length > 0) {
|
|
394
|
+
return missions.map((m) => ({
|
|
395
|
+
id: m.missionId,
|
|
396
|
+
tag: missionTag(m.type),
|
|
397
|
+
name: m.label || m.missionId,
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
if (ownerHistory && ownerHistory.length > 0) {
|
|
401
|
+
return ownerHistory.map((p, i) => {
|
|
402
|
+
const firstLine = (p.content ?? "").split("\n", 1)[0].trim();
|
|
403
|
+
const name = firstLine.length > 0 ? firstLine : p.type;
|
|
404
|
+
return {
|
|
405
|
+
id: `prompt-${i}-${p.ts}`,
|
|
406
|
+
tag: promptTag(p.type),
|
|
407
|
+
name,
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
return GOAL_HISTORY_FALLBACK.map((e) => ({ ...e }));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export interface SidebarProps {
|
|
415
|
+
collapsed: boolean;
|
|
416
|
+
onToggle: () => void;
|
|
417
|
+
/** Real missions; when present the list shows real goals/hotfixes/etc. */
|
|
418
|
+
missions?: MissionDoc[];
|
|
419
|
+
/** Real owner prompt history; used when missions are absent. */
|
|
420
|
+
ownerHistory?: OwnerPromptEntry[];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function Sidebar({
|
|
424
|
+
collapsed,
|
|
425
|
+
onToggle,
|
|
426
|
+
missions,
|
|
427
|
+
ownerHistory,
|
|
428
|
+
}: SidebarProps): React.JSX.Element {
|
|
429
|
+
const entries = buildSidebarEntries(missions, ownerHistory);
|
|
430
|
+
const [sel, setSel] = useState<string>(entries[0]?.id ?? "");
|
|
431
|
+
return (
|
|
432
|
+
<aside className={"sidebar" + (collapsed ? " is-collapsed" : "")}>
|
|
433
|
+
<div className="sb-head">
|
|
434
|
+
<span className="sb-avatar">AI</span>
|
|
435
|
+
{!collapsed && (
|
|
436
|
+
<div className="sb-id">
|
|
437
|
+
<div className="sb-name">Administrator</div>
|
|
438
|
+
<div className="sb-sub">workflow history</div>
|
|
439
|
+
</div>
|
|
440
|
+
)}
|
|
441
|
+
<button className="sb-toggle" onClick={onToggle} title="collapse">
|
|
442
|
+
{collapsed ? "›" : "‹"}
|
|
443
|
+
</button>
|
|
444
|
+
</div>
|
|
445
|
+
{!collapsed && (
|
|
446
|
+
<div className="sb-list">
|
|
447
|
+
{entries.map((g) => (
|
|
448
|
+
<button
|
|
449
|
+
key={g.id}
|
|
450
|
+
className={"sb-item" + (sel === g.id ? " is-active" : "")}
|
|
451
|
+
onClick={() => setSel(g.id)}
|
|
452
|
+
>
|
|
453
|
+
<span className={"sb-tag " + tagClass(g.tag)}>
|
|
454
|
+
/{g.tag.toLowerCase()}
|
|
455
|
+
</span>
|
|
456
|
+
<span className="sb-itemname">{g.name}</span>
|
|
457
|
+
</button>
|
|
458
|
+
))}
|
|
459
|
+
</div>
|
|
460
|
+
)}
|
|
461
|
+
</aside>
|
|
462
|
+
);
|
|
463
|
+
}
|