@a5c-ai/babysitter-paperclip 0.0.2-staging.02a0ee21
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/BABYSITTER.md +46 -0
- package/README.md +229 -0
- package/esbuild.config.mjs +12 -0
- package/package.json +29 -0
- package/src/__tests__/delegating-adapter.test.ts +85 -0
- package/src/__tests__/types.test.ts +29 -0
- package/src/babysitter-bridge.ts +313 -0
- package/src/delegating-adapter.ts +97 -0
- package/src/harness-plugin-installer.ts +202 -0
- package/src/manifest.ts +101 -0
- package/src/types.ts +130 -0
- package/src/ui/BabysitterDashboard.tsx +142 -0
- package/src/ui/BabysitterSidebar.tsx +123 -0
- package/src/ui/BreakpointApproval.tsx +212 -0
- package/src/ui/RunDetailTab.tsx +169 -0
- package/src/ui/index.tsx +9 -0
- package/src/ui/styles.ts +75 -0
- package/src/worker.ts +595 -0
- package/tsconfig.json +19 -0
- package/versions.json +3 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the Babysitter Paperclip plugin.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Adapter type mapping from Paperclip agent adapterType to babysitter harness name. */
|
|
6
|
+
export const ADAPTER_TYPE_MAP: Record<string, string> = {
|
|
7
|
+
claude_local: "claude-code",
|
|
8
|
+
codex_local: "codex",
|
|
9
|
+
gemini_local: "gemini-cli",
|
|
10
|
+
cursor_local: "cursor",
|
|
11
|
+
github_copilot: "github-copilot",
|
|
12
|
+
opencode_local: "opencode",
|
|
13
|
+
pi_local: "pi",
|
|
14
|
+
omp_local: "oh-my-pi",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** A babysitter run tracked by the plugin. */
|
|
18
|
+
export interface TrackedRun {
|
|
19
|
+
runId: string;
|
|
20
|
+
processId: string;
|
|
21
|
+
agentId: string;
|
|
22
|
+
companyId: string;
|
|
23
|
+
harnessName: string;
|
|
24
|
+
status: "running" | "waiting" | "completed" | "failed";
|
|
25
|
+
createdAt: string;
|
|
26
|
+
lastIteratedAt?: string;
|
|
27
|
+
pendingBreakpoints: PendingBreakpoint[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A pending breakpoint awaiting user approval.
|
|
31
|
+
*
|
|
32
|
+
* Breakpoint metadata comes from the SDK's breakpoint intrinsic
|
|
33
|
+
* (runtime/intrinsics/breakpoint.ts). The task.json for a breakpoint has:
|
|
34
|
+
* kind: "breakpoint"
|
|
35
|
+
* metadata.payload: { question, title, options, expert, tags, strategy, ... }
|
|
36
|
+
*
|
|
37
|
+
* The stop hook in the underlying harness (e.g., Claude Code) checks if only
|
|
38
|
+
* breakpoints are pending and allows the agent to exit (approve decision),
|
|
39
|
+
* pausing the orchestration loop until the breakpoint is resolved externally.
|
|
40
|
+
*
|
|
41
|
+
* CRITICAL: Both approve and reject use --status ok.
|
|
42
|
+
* Rejection sends { approved: false, feedback: "..." }.
|
|
43
|
+
* Never use --status error for rejection -- that triggers RUN_FAILED.
|
|
44
|
+
*/
|
|
45
|
+
export interface PendingBreakpoint {
|
|
46
|
+
effectId: string;
|
|
47
|
+
title: string;
|
|
48
|
+
question?: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
options?: string[];
|
|
51
|
+
expert?: string | string[];
|
|
52
|
+
tags?: string[];
|
|
53
|
+
strategy?: "single" | "first-response-wins" | "collect-all" | "quorum";
|
|
54
|
+
previousFeedback?: string;
|
|
55
|
+
attempt?: number;
|
|
56
|
+
requestedAt: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Overview data returned by the runs-overview data handler. */
|
|
60
|
+
export interface RunsOverview {
|
|
61
|
+
activeRuns: TrackedRun[];
|
|
62
|
+
pendingBreakpoints: number;
|
|
63
|
+
totalRuns: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Detail data for a single run. */
|
|
67
|
+
export interface RunDetail {
|
|
68
|
+
run: TrackedRun;
|
|
69
|
+
events: RunEvent[];
|
|
70
|
+
pendingEffects: PendingEffect[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A journal event from a babysitter run. */
|
|
74
|
+
export interface RunEvent {
|
|
75
|
+
seq: number;
|
|
76
|
+
type: string;
|
|
77
|
+
recordedAt: string;
|
|
78
|
+
data: Record<string, unknown>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A pending effect from a babysitter run. */
|
|
82
|
+
export interface PendingEffect {
|
|
83
|
+
effectId: string;
|
|
84
|
+
kind: string;
|
|
85
|
+
label: string;
|
|
86
|
+
taskId: string;
|
|
87
|
+
requestedAt: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Result of detecting which harness an agent uses. */
|
|
91
|
+
export interface HarnessDetectionResult {
|
|
92
|
+
harnessName: string;
|
|
93
|
+
detectionTier: "agent-metadata" | "env-probe" | "config" | "fallback";
|
|
94
|
+
confidence: "high" | "medium" | "low";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Breakpoint task definition metadata as written by the SDK.
|
|
99
|
+
* Extracted from task.json files in runs/<runId>/tasks/<effectId>/.
|
|
100
|
+
*/
|
|
101
|
+
export interface BreakpointTaskDef {
|
|
102
|
+
kind: "breakpoint";
|
|
103
|
+
title: string;
|
|
104
|
+
metadata: {
|
|
105
|
+
payload: {
|
|
106
|
+
question?: string;
|
|
107
|
+
title?: string;
|
|
108
|
+
options?: string[];
|
|
109
|
+
expert?: string | string[];
|
|
110
|
+
tags?: string[];
|
|
111
|
+
strategy?: string;
|
|
112
|
+
previousFeedback?: string;
|
|
113
|
+
attempt?: number;
|
|
114
|
+
[key: string]: unknown;
|
|
115
|
+
};
|
|
116
|
+
requestedAt: string;
|
|
117
|
+
label: string;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Harness plugin installation status.
|
|
123
|
+
* Used to verify the underlying harness has its babysitter plugin installed.
|
|
124
|
+
*/
|
|
125
|
+
export interface HarnessPluginStatus {
|
|
126
|
+
harnessName: string;
|
|
127
|
+
cliAvailable: boolean;
|
|
128
|
+
pluginInstalled: boolean;
|
|
129
|
+
installCommand?: string;
|
|
130
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard widget showing babysitter run overview.
|
|
3
|
+
*
|
|
4
|
+
* Displays active runs, pending breakpoints count, and quick status badges.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { usePluginData } from "@paperclipai/plugin-sdk/ui";
|
|
8
|
+
import type { PluginWidgetProps } from "@paperclipai/plugin-sdk/ui";
|
|
9
|
+
|
|
10
|
+
interface RunsOverview {
|
|
11
|
+
activeRuns: Array<{
|
|
12
|
+
runId: string;
|
|
13
|
+
processId: string;
|
|
14
|
+
status: string;
|
|
15
|
+
pendingBreakpoints: Array<{ effectId: string; title: string }>;
|
|
16
|
+
}>;
|
|
17
|
+
pendingBreakpoints: number;
|
|
18
|
+
totalRuns: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function BabysitterDashboard({ context }: PluginWidgetProps) {
|
|
22
|
+
const { data, loading, error, refresh } = usePluginData<RunsOverview>(
|
|
23
|
+
"runs-overview",
|
|
24
|
+
{ companyId: context.companyId }
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (loading) {
|
|
28
|
+
return <div style={{ padding: 16 }}>Loading runs...</div>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (error) {
|
|
32
|
+
return (
|
|
33
|
+
<div style={{ padding: 16, color: "var(--destructive)" }}>
|
|
34
|
+
Error: {error.message}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!data) return null;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div style={{ display: "grid", gap: 12, padding: 16 }}>
|
|
43
|
+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
44
|
+
<strong>Babysitter Orchestration</strong>
|
|
45
|
+
<button onClick={refresh} style={{ fontSize: 12 }}>
|
|
46
|
+
Refresh
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 8 }}>
|
|
51
|
+
<StatCard label="Active" value={data.activeRuns.length} />
|
|
52
|
+
<StatCard label="Breakpoints" value={data.pendingBreakpoints} highlight={data.pendingBreakpoints > 0} />
|
|
53
|
+
<StatCard label="Total" value={data.totalRuns} />
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{data.activeRuns.length > 0 && (
|
|
57
|
+
<div style={{ display: "grid", gap: 4 }}>
|
|
58
|
+
{data.activeRuns.slice(0, 5).map((run) => (
|
|
59
|
+
<div
|
|
60
|
+
key={run.runId}
|
|
61
|
+
style={{
|
|
62
|
+
display: "flex",
|
|
63
|
+
justifyContent: "space-between",
|
|
64
|
+
alignItems: "center",
|
|
65
|
+
padding: "4px 8px",
|
|
66
|
+
borderRadius: 4,
|
|
67
|
+
background: "var(--muted)",
|
|
68
|
+
fontSize: 12,
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<span style={{ fontFamily: "monospace" }}>
|
|
72
|
+
{run.processId}
|
|
73
|
+
</span>
|
|
74
|
+
<StatusBadge status={run.status} />
|
|
75
|
+
</div>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{data.activeRuns.length === 0 && (
|
|
81
|
+
<div style={{ color: "var(--muted-foreground)", fontSize: 13 }}>
|
|
82
|
+
No active runs
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function StatCard({
|
|
90
|
+
label,
|
|
91
|
+
value,
|
|
92
|
+
highlight,
|
|
93
|
+
}: {
|
|
94
|
+
label: string;
|
|
95
|
+
value: number;
|
|
96
|
+
highlight?: boolean;
|
|
97
|
+
}) {
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
style={{
|
|
101
|
+
textAlign: "center",
|
|
102
|
+
padding: "8px 4px",
|
|
103
|
+
borderRadius: 6,
|
|
104
|
+
background: highlight ? "var(--destructive)" : "var(--muted)",
|
|
105
|
+
color: highlight ? "var(--destructive-foreground)" : "inherit",
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<div style={{ fontSize: 20, fontWeight: 600 }}>{value}</div>
|
|
109
|
+
<div style={{ fontSize: 11, opacity: 0.7 }}>{label}</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function StatusBadge({ status }: { status: string }) {
|
|
115
|
+
const colors: Record<string, string> = {
|
|
116
|
+
running: "#22c55e",
|
|
117
|
+
waiting: "#eab308",
|
|
118
|
+
completed: "#6b7280",
|
|
119
|
+
failed: "#ef4444",
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<span
|
|
124
|
+
style={{
|
|
125
|
+
display: "inline-flex",
|
|
126
|
+
alignItems: "center",
|
|
127
|
+
gap: 4,
|
|
128
|
+
fontSize: 11,
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
<span
|
|
132
|
+
style={{
|
|
133
|
+
width: 6,
|
|
134
|
+
height: 6,
|
|
135
|
+
borderRadius: "50%",
|
|
136
|
+
background: colors[status] ?? "#6b7280",
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
{status}
|
|
140
|
+
</span>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar panel showing compact babysitter status.
|
|
3
|
+
*
|
|
4
|
+
* Displays active run count, pending breakpoints badge, and quick links.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { usePluginData, usePluginAction } from "@paperclipai/plugin-sdk/ui";
|
|
8
|
+
import type { PluginSidebarProps } from "@paperclipai/plugin-sdk/ui";
|
|
9
|
+
|
|
10
|
+
interface RunsOverview {
|
|
11
|
+
activeRuns: Array<{
|
|
12
|
+
runId: string;
|
|
13
|
+
processId: string;
|
|
14
|
+
status: string;
|
|
15
|
+
pendingBreakpoints: Array<{ effectId: string }>;
|
|
16
|
+
}>;
|
|
17
|
+
pendingBreakpoints: number;
|
|
18
|
+
totalRuns: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function BabysitterSidebar({ context }: PluginSidebarProps) {
|
|
22
|
+
const { data, loading, refresh } = usePluginData<RunsOverview>(
|
|
23
|
+
"runs-overview",
|
|
24
|
+
{ companyId: context.companyId }
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (loading) {
|
|
28
|
+
return <div style={{ padding: 8, fontSize: 12 }}>Loading...</div>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!data) return null;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div style={{ padding: 12, display: "grid", gap: 8 }}>
|
|
35
|
+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
36
|
+
<strong style={{ fontSize: 13 }}>Babysitter</strong>
|
|
37
|
+
<button onClick={refresh} style={{ fontSize: 10, padding: "2px 6px" }}>
|
|
38
|
+
Refresh
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div style={{ display: "grid", gap: 4 }}>
|
|
43
|
+
<SidebarRow
|
|
44
|
+
label="Active runs"
|
|
45
|
+
value={String(data.activeRuns.length)}
|
|
46
|
+
/>
|
|
47
|
+
<SidebarRow
|
|
48
|
+
label="Breakpoints"
|
|
49
|
+
value={String(data.pendingBreakpoints)}
|
|
50
|
+
highlight={data.pendingBreakpoints > 0}
|
|
51
|
+
/>
|
|
52
|
+
<SidebarRow
|
|
53
|
+
label="Total runs"
|
|
54
|
+
value={String(data.totalRuns)}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{data.activeRuns.length > 0 && (
|
|
59
|
+
<div style={{ borderTop: "1px solid var(--border)", paddingTop: 8, marginTop: 4 }}>
|
|
60
|
+
{data.activeRuns.slice(0, 3).map((run) => (
|
|
61
|
+
<div
|
|
62
|
+
key={run.runId}
|
|
63
|
+
style={{
|
|
64
|
+
fontSize: 11,
|
|
65
|
+
padding: "3px 0",
|
|
66
|
+
display: "flex",
|
|
67
|
+
justifyContent: "space-between",
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<span style={{ fontFamily: "monospace", opacity: 0.8 }}>
|
|
71
|
+
{run.processId.length > 20
|
|
72
|
+
? run.processId.slice(0, 20) + "..."
|
|
73
|
+
: run.processId}
|
|
74
|
+
</span>
|
|
75
|
+
<span
|
|
76
|
+
style={{
|
|
77
|
+
fontSize: 10,
|
|
78
|
+
padding: "1px 4px",
|
|
79
|
+
borderRadius: 3,
|
|
80
|
+
background:
|
|
81
|
+
run.status === "waiting" ? "#eab308" : "#22c55e",
|
|
82
|
+
color: "white",
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
{run.status}
|
|
86
|
+
</span>
|
|
87
|
+
</div>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function SidebarRow({
|
|
96
|
+
label,
|
|
97
|
+
value,
|
|
98
|
+
highlight,
|
|
99
|
+
}: {
|
|
100
|
+
label: string;
|
|
101
|
+
value: string;
|
|
102
|
+
highlight?: boolean;
|
|
103
|
+
}) {
|
|
104
|
+
return (
|
|
105
|
+
<div
|
|
106
|
+
style={{
|
|
107
|
+
display: "flex",
|
|
108
|
+
justifyContent: "space-between",
|
|
109
|
+
fontSize: 12,
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
<span style={{ color: "var(--muted-foreground)" }}>{label}</span>
|
|
113
|
+
<span
|
|
114
|
+
style={{
|
|
115
|
+
fontWeight: highlight ? 700 : 400,
|
|
116
|
+
color: highlight ? "var(--destructive)" : "inherit",
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
{value}
|
|
120
|
+
</span>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Breakpoint approval/rejection UI component.
|
|
3
|
+
*
|
|
4
|
+
* Renders the breakpoint details with approve/reject actions.
|
|
5
|
+
* Critical: both approve and reject use --status ok with the babysitter CLI.
|
|
6
|
+
* Rejection sends { approved: false, feedback: "..." }.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useState } from "react";
|
|
10
|
+
import { usePluginAction } from "@paperclipai/plugin-sdk/ui";
|
|
11
|
+
|
|
12
|
+
interface BreakpointApprovalProps {
|
|
13
|
+
runId: string;
|
|
14
|
+
effectId: string;
|
|
15
|
+
title: string;
|
|
16
|
+
/** The breakpoint question from task.json metadata.payload.question */
|
|
17
|
+
question?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
/** Explicit options from the process (e.g., ["Approve", "Request changes"]) */
|
|
20
|
+
options?: string[];
|
|
21
|
+
/** Domain expert routing from breakpoint routing fields */
|
|
22
|
+
expert?: string | string[];
|
|
23
|
+
/** Tags for categorization */
|
|
24
|
+
tags?: string[];
|
|
25
|
+
/** Previous rejection feedback (shown on retry) */
|
|
26
|
+
previousFeedback?: string;
|
|
27
|
+
/** Current retry attempt number */
|
|
28
|
+
attempt?: number;
|
|
29
|
+
companyId: string;
|
|
30
|
+
onResolved?: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function BreakpointApproval({
|
|
34
|
+
runId,
|
|
35
|
+
effectId,
|
|
36
|
+
title,
|
|
37
|
+
question,
|
|
38
|
+
description,
|
|
39
|
+
options,
|
|
40
|
+
expert,
|
|
41
|
+
tags,
|
|
42
|
+
previousFeedback,
|
|
43
|
+
attempt,
|
|
44
|
+
companyId,
|
|
45
|
+
onResolved,
|
|
46
|
+
}: BreakpointApprovalProps) {
|
|
47
|
+
const approve = usePluginAction("approve-breakpoint");
|
|
48
|
+
const reject = usePluginAction("reject-breakpoint");
|
|
49
|
+
|
|
50
|
+
const [feedback, setFeedback] = useState("");
|
|
51
|
+
const [submitting, setSubmitting] = useState(false);
|
|
52
|
+
const [error, setError] = useState<string | null>(null);
|
|
53
|
+
const [resolved, setResolved] = useState(false);
|
|
54
|
+
|
|
55
|
+
async function handleApprove(response?: string) {
|
|
56
|
+
setSubmitting(true);
|
|
57
|
+
setError(null);
|
|
58
|
+
try {
|
|
59
|
+
await approve({ runId, effectId, response, companyId });
|
|
60
|
+
setResolved(true);
|
|
61
|
+
onResolved?.();
|
|
62
|
+
} catch (err) {
|
|
63
|
+
setError(err instanceof Error ? err.message : "Approval failed");
|
|
64
|
+
} finally {
|
|
65
|
+
setSubmitting(false);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function handleReject() {
|
|
70
|
+
if (!feedback.trim()) {
|
|
71
|
+
setError("Feedback is required when rejecting a breakpoint");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
setSubmitting(true);
|
|
75
|
+
setError(null);
|
|
76
|
+
try {
|
|
77
|
+
await reject({ runId, effectId, feedback: feedback.trim(), companyId });
|
|
78
|
+
setResolved(true);
|
|
79
|
+
onResolved?.();
|
|
80
|
+
} catch (err) {
|
|
81
|
+
setError(err instanceof Error ? err.message : "Rejection failed");
|
|
82
|
+
} finally {
|
|
83
|
+
setSubmitting(false);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (resolved) {
|
|
88
|
+
return (
|
|
89
|
+
<div style={{ padding: 12, background: "var(--muted)", borderRadius: 6 }}>
|
|
90
|
+
Breakpoint resolved.
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
style={{
|
|
98
|
+
border: "1px solid var(--border)",
|
|
99
|
+
borderRadius: 8,
|
|
100
|
+
padding: 16,
|
|
101
|
+
display: "grid",
|
|
102
|
+
gap: 12,
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
<div>
|
|
106
|
+
<strong>{question ?? title}</strong>
|
|
107
|
+
{description && (
|
|
108
|
+
<p style={{ margin: "4px 0 0", fontSize: 13, color: "var(--muted-foreground)" }}>
|
|
109
|
+
{description}
|
|
110
|
+
</p>
|
|
111
|
+
)}
|
|
112
|
+
{/* Show retry context when this is a re-ask after rejection */}
|
|
113
|
+
{previousFeedback && (
|
|
114
|
+
<div
|
|
115
|
+
style={{
|
|
116
|
+
marginTop: 8,
|
|
117
|
+
padding: "6px 10px",
|
|
118
|
+
background: "var(--destructive)",
|
|
119
|
+
color: "var(--destructive-foreground)",
|
|
120
|
+
borderRadius: 4,
|
|
121
|
+
fontSize: 12,
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
Previous feedback (attempt {attempt ?? "?"}): {previousFeedback}
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
{/* Show routing info */}
|
|
128
|
+
{(expert || (tags && tags.length > 0)) && (
|
|
129
|
+
<div style={{ marginTop: 4, fontSize: 11, color: "var(--muted-foreground)" }}>
|
|
130
|
+
{expert && (
|
|
131
|
+
<span>
|
|
132
|
+
Expert: {Array.isArray(expert) ? expert.join(", ") : expert}
|
|
133
|
+
</span>
|
|
134
|
+
)}
|
|
135
|
+
{tags && tags.length > 0 && (
|
|
136
|
+
<span style={{ marginLeft: expert ? 12 : 0 }}>
|
|
137
|
+
Tags: {tags.join(", ")}
|
|
138
|
+
</span>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{options && options.length > 0 && (
|
|
145
|
+
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
|
146
|
+
{options.map((opt) => (
|
|
147
|
+
<button
|
|
148
|
+
key={opt}
|
|
149
|
+
onClick={() => handleApprove(opt)}
|
|
150
|
+
disabled={submitting}
|
|
151
|
+
style={{ fontSize: 13 }}
|
|
152
|
+
>
|
|
153
|
+
{opt}
|
|
154
|
+
</button>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
<div style={{ display: "grid", gap: 8 }}>
|
|
160
|
+
<textarea
|
|
161
|
+
value={feedback}
|
|
162
|
+
onChange={(e) => setFeedback(e.target.value)}
|
|
163
|
+
placeholder="Feedback (required for rejection)..."
|
|
164
|
+
rows={2}
|
|
165
|
+
style={{
|
|
166
|
+
width: "100%",
|
|
167
|
+
padding: 8,
|
|
168
|
+
borderRadius: 4,
|
|
169
|
+
border: "1px solid var(--border)",
|
|
170
|
+
resize: "vertical",
|
|
171
|
+
fontSize: 13,
|
|
172
|
+
}}
|
|
173
|
+
/>
|
|
174
|
+
|
|
175
|
+
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
|
|
176
|
+
<button
|
|
177
|
+
onClick={handleReject}
|
|
178
|
+
disabled={submitting}
|
|
179
|
+
style={{
|
|
180
|
+
background: "var(--destructive)",
|
|
181
|
+
color: "var(--destructive-foreground)",
|
|
182
|
+
padding: "6px 16px",
|
|
183
|
+
borderRadius: 4,
|
|
184
|
+
border: "none",
|
|
185
|
+
cursor: submitting ? "not-allowed" : "pointer",
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
{submitting ? "..." : "Reject"}
|
|
189
|
+
</button>
|
|
190
|
+
<button
|
|
191
|
+
onClick={() => handleApprove(feedback || undefined)}
|
|
192
|
+
disabled={submitting}
|
|
193
|
+
style={{
|
|
194
|
+
background: "var(--primary)",
|
|
195
|
+
color: "var(--primary-foreground)",
|
|
196
|
+
padding: "6px 16px",
|
|
197
|
+
borderRadius: 4,
|
|
198
|
+
border: "none",
|
|
199
|
+
cursor: submitting ? "not-allowed" : "pointer",
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
{submitting ? "..." : "Approve"}
|
|
203
|
+
</button>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
{error && (
|
|
208
|
+
<div style={{ color: "var(--destructive)", fontSize: 12 }}>{error}</div>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|