stagent 0.1.10 → 0.1.12
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/README.md +58 -27
- package/package.json +3 -3
- package/src/__tests__/e2e/blueprint.test.ts +63 -0
- package/src/__tests__/e2e/cross-runtime.test.ts +77 -0
- package/src/__tests__/e2e/helpers.ts +286 -0
- package/src/__tests__/e2e/parallel-workflow.test.ts +120 -0
- package/src/__tests__/e2e/sequence-workflow.test.ts +109 -0
- package/src/__tests__/e2e/setup.ts +156 -0
- package/src/__tests__/e2e/single-task.test.ts +170 -0
- package/src/app/api/command-palette/recent/route.ts +41 -18
- package/src/app/api/context/batch/route.ts +44 -0
- package/src/app/api/permissions/presets/route.ts +80 -0
- package/src/app/api/playbook/status/route.ts +15 -0
- package/src/app/api/profiles/route.ts +23 -21
- package/src/app/api/settings/pricing/route.ts +15 -0
- package/src/app/costs/page.tsx +53 -43
- package/src/app/globals.css +0 -5
- package/src/app/playbook/[slug]/page.tsx +76 -0
- package/src/app/playbook/page.tsx +54 -0
- package/src/app/profiles/page.tsx +7 -4
- package/src/app/settings/page.tsx +2 -2
- package/src/app/tasks/page.tsx +5 -0
- package/src/components/costs/cost-dashboard.tsx +226 -320
- package/src/components/dashboard/activity-feed.tsx +6 -2
- package/src/components/notifications/batch-proposal-review.tsx +150 -0
- package/src/components/notifications/notification-item.tsx +6 -3
- package/src/components/notifications/pending-approval-host.tsx +57 -11
- package/src/components/playbook/adoption-heatmap.tsx +69 -0
- package/src/components/playbook/journey-card.tsx +110 -0
- package/src/components/playbook/playbook-action-button.tsx +22 -0
- package/src/components/playbook/playbook-browser.tsx +143 -0
- package/src/components/playbook/playbook-card.tsx +102 -0
- package/src/components/playbook/playbook-detail-view.tsx +223 -0
- package/src/components/playbook/playbook-homepage.tsx +142 -0
- package/src/components/playbook/playbook-toc.tsx +90 -0
- package/src/components/playbook/playbook-updated-badge.tsx +23 -0
- package/src/components/playbook/related-docs.tsx +30 -0
- package/src/components/profiles/__tests__/learned-context-panel.test.tsx +175 -0
- package/src/components/profiles/context-proposal-review.tsx +7 -3
- package/src/components/profiles/learned-context-panel.tsx +116 -8
- package/src/components/profiles/profile-detail-view.tsx +7 -19
- package/src/components/profiles/profile-form-view.tsx +0 -22
- package/src/components/settings/__tests__/auth-config-section.test.tsx +147 -0
- package/src/components/settings/api-key-form.tsx +5 -43
- package/src/components/settings/auth-config-section.tsx +10 -6
- package/src/components/settings/auth-status-badge.tsx +8 -0
- package/src/components/settings/budget-guardrails-section.tsx +403 -620
- package/src/components/settings/connection-test-control.tsx +63 -0
- package/src/components/settings/permissions-section.tsx +85 -75
- package/src/components/settings/permissions-sections.tsx +24 -0
- package/src/components/settings/presets-section.tsx +159 -0
- package/src/components/settings/pricing-registry-panel.tsx +164 -0
- package/src/components/shared/app-sidebar.tsx +2 -0
- package/src/components/shared/command-palette.tsx +30 -0
- package/src/components/shared/light-markdown.tsx +134 -0
- package/src/components/workflows/loop-status-view.tsx +8 -4
- package/src/components/workflows/workflow-status-view.tsx +16 -9
- package/src/lib/agents/__tests__/claude-agent.test.ts +7 -2
- package/src/lib/agents/__tests__/learned-context.test.ts +500 -0
- package/src/lib/agents/__tests__/pattern-extractor.test.ts +243 -0
- package/src/lib/agents/__tests__/sweep.test.ts +202 -0
- package/src/lib/agents/claude-agent.ts +104 -78
- package/src/lib/agents/learned-context.ts +32 -28
- package/src/lib/agents/learning-session.ts +234 -0
- package/src/lib/agents/pattern-extractor.ts +34 -64
- package/src/lib/agents/profiles/__tests__/sort.test.ts +42 -0
- package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/general/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/researcher/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/sweep/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +0 -1
- package/src/lib/agents/profiles/registry.ts +0 -1
- package/src/lib/agents/profiles/sort.ts +7 -0
- package/src/lib/agents/profiles/types.ts +0 -1
- package/src/lib/agents/runtime/catalog.ts +1 -1
- package/src/lib/agents/runtime/claude.ts +66 -0
- package/src/lib/constants/settings.ts +1 -0
- package/src/lib/constants/task-status.ts +6 -0
- package/src/lib/data/seed-data/profiles.ts +0 -3
- package/src/lib/db/schema.ts +3 -0
- package/src/lib/docs/adoption.ts +105 -0
- package/src/lib/docs/journey-tracker.ts +21 -0
- package/src/lib/docs/reader.ts +102 -0
- package/src/lib/docs/types.ts +54 -0
- package/src/lib/docs/usage-stage.ts +60 -0
- package/src/lib/notifications/actionable.ts +18 -10
- package/src/lib/settings/__tests__/budget-guardrails.test.ts +86 -24
- package/src/lib/settings/budget-guardrails.ts +213 -85
- package/src/lib/settings/permission-presets.ts +150 -0
- package/src/lib/settings/runtime-setup.ts +71 -0
- package/src/lib/usage/__tests__/ledger.test.ts +29 -5
- package/src/lib/usage/__tests__/pricing-registry.test.ts +78 -0
- package/src/lib/usage/ledger.ts +4 -2
- package/src/lib/usage/pricing-registry.ts +570 -0
- package/src/lib/usage/pricing.ts +15 -41
- package/src/lib/utils/__tests__/learned-context-history.test.ts +171 -0
- package/src/lib/utils/learned-context-history.ts +150 -0
- package/src/lib/validators/__tests__/profile.test.ts +0 -15
- package/src/lib/validators/__tests__/settings.test.ts +23 -16
- package/src/lib/validators/profile.ts +0 -1
- package/src/lib/validators/settings.ts +3 -9
- package/src/lib/workflows/__tests__/engine.test.ts +2 -0
- package/src/lib/workflows/engine.ts +20 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { LearnedContextPanel } from "@/components/profiles/learned-context-panel";
|
|
4
|
+
import type { LearnedContextRow } from "@/lib/db/schema";
|
|
5
|
+
|
|
6
|
+
const { fetchMock } = vi.hoisted(() => ({
|
|
7
|
+
fetchMock: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("sonner", () => ({
|
|
11
|
+
toast: {
|
|
12
|
+
success: vi.fn(),
|
|
13
|
+
error: vi.fn(),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("@/lib/utils/format-timestamp", () => ({
|
|
18
|
+
formatTimestamp: () => "just now",
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
function makeRow(
|
|
22
|
+
overrides: Partial<LearnedContextRow> & {
|
|
23
|
+
id: string;
|
|
24
|
+
version: number;
|
|
25
|
+
changeType: LearnedContextRow["changeType"];
|
|
26
|
+
}
|
|
27
|
+
): LearnedContextRow {
|
|
28
|
+
return {
|
|
29
|
+
id: overrides.id,
|
|
30
|
+
profileId: overrides.profileId ?? "general",
|
|
31
|
+
version: overrides.version,
|
|
32
|
+
content: overrides.content ?? null,
|
|
33
|
+
diff: overrides.diff ?? null,
|
|
34
|
+
changeType: overrides.changeType,
|
|
35
|
+
sourceTaskId: overrides.sourceTaskId ?? null,
|
|
36
|
+
proposalNotificationId: overrides.proposalNotificationId ?? null,
|
|
37
|
+
proposedAdditions: overrides.proposedAdditions ?? null,
|
|
38
|
+
approvedBy: overrides.approvedBy ?? null,
|
|
39
|
+
createdAt: overrides.createdAt ?? new Date("2026-03-17T12:00:00.000Z"),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("LearnedContextPanel", () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
fetchMock.mockReset();
|
|
46
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
vi.unstubAllGlobals();
|
|
51
|
+
vi.clearAllMocks();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("shows singular and plural version labels", async () => {
|
|
55
|
+
fetchMock.mockResolvedValueOnce({
|
|
56
|
+
ok: true,
|
|
57
|
+
json: vi.fn().mockResolvedValue({
|
|
58
|
+
history: [
|
|
59
|
+
makeRow({
|
|
60
|
+
id: "v1",
|
|
61
|
+
version: 1,
|
|
62
|
+
changeType: "approved",
|
|
63
|
+
content: "Validate inputs",
|
|
64
|
+
diff: "Validate inputs",
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
67
|
+
currentSize: 15,
|
|
68
|
+
limit: 8000,
|
|
69
|
+
needsSummarization: false,
|
|
70
|
+
}),
|
|
71
|
+
} as Response);
|
|
72
|
+
|
|
73
|
+
const { rerender } = render(<LearnedContextPanel profileId="general" />);
|
|
74
|
+
|
|
75
|
+
expect(await screen.findByText("1 version")).toBeInTheDocument();
|
|
76
|
+
|
|
77
|
+
fetchMock.mockResolvedValueOnce({
|
|
78
|
+
ok: true,
|
|
79
|
+
json: vi.fn().mockResolvedValue({
|
|
80
|
+
history: [
|
|
81
|
+
makeRow({
|
|
82
|
+
id: "v2",
|
|
83
|
+
version: 2,
|
|
84
|
+
changeType: "approved",
|
|
85
|
+
content: "Validate inputs\n\nUse retries",
|
|
86
|
+
diff: "Use retries",
|
|
87
|
+
}),
|
|
88
|
+
makeRow({
|
|
89
|
+
id: "v1",
|
|
90
|
+
version: 1,
|
|
91
|
+
changeType: "approved",
|
|
92
|
+
content: "Validate inputs",
|
|
93
|
+
diff: "Validate inputs",
|
|
94
|
+
}),
|
|
95
|
+
],
|
|
96
|
+
currentSize: 28,
|
|
97
|
+
limit: 8000,
|
|
98
|
+
needsSummarization: false,
|
|
99
|
+
}),
|
|
100
|
+
} as Response);
|
|
101
|
+
|
|
102
|
+
rerender(<LearnedContextPanel profileId="general-v2" />);
|
|
103
|
+
|
|
104
|
+
expect(await screen.findByText("2 versions")).toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("renders badges, rollback content preview, and a unified diff toggle", async () => {
|
|
108
|
+
fetchMock.mockResolvedValueOnce({
|
|
109
|
+
ok: true,
|
|
110
|
+
json: vi.fn().mockResolvedValue({
|
|
111
|
+
history: [
|
|
112
|
+
makeRow({
|
|
113
|
+
id: "v4",
|
|
114
|
+
version: 4,
|
|
115
|
+
changeType: "rollback",
|
|
116
|
+
content: "Validate inputs",
|
|
117
|
+
diff: "Rolled back to version 1",
|
|
118
|
+
}),
|
|
119
|
+
makeRow({
|
|
120
|
+
id: "v3",
|
|
121
|
+
version: 3,
|
|
122
|
+
changeType: "summarization",
|
|
123
|
+
content: "Validate inputs\nUse retries",
|
|
124
|
+
diff: "Summarized from 80 to 42 chars",
|
|
125
|
+
}),
|
|
126
|
+
makeRow({
|
|
127
|
+
id: "v2",
|
|
128
|
+
version: 2,
|
|
129
|
+
changeType: "proposal",
|
|
130
|
+
diff: "Always include retry guidance",
|
|
131
|
+
}),
|
|
132
|
+
makeRow({
|
|
133
|
+
id: "v1",
|
|
134
|
+
version: 1,
|
|
135
|
+
changeType: "approved",
|
|
136
|
+
content: "Validate inputs",
|
|
137
|
+
diff: "Validate inputs",
|
|
138
|
+
}),
|
|
139
|
+
],
|
|
140
|
+
currentSize: 32,
|
|
141
|
+
limit: 8000,
|
|
142
|
+
needsSummarization: false,
|
|
143
|
+
}),
|
|
144
|
+
} as Response);
|
|
145
|
+
|
|
146
|
+
render(<LearnedContextPanel profileId="general" />);
|
|
147
|
+
|
|
148
|
+
expect((await screen.findAllByText("Rollback")).length).toBeGreaterThan(0);
|
|
149
|
+
expect(screen.getByText("Summarized")).toBeInTheDocument();
|
|
150
|
+
expect(screen.getByText("Proposed")).toBeInTheDocument();
|
|
151
|
+
expect(screen.getAllByText("Approved")).not.toHaveLength(0);
|
|
152
|
+
|
|
153
|
+
const rollbackCard = screen.getByText("v4").closest(".surface-card-muted");
|
|
154
|
+
expect(rollbackCard).not.toBeNull();
|
|
155
|
+
expect(
|
|
156
|
+
within(rollbackCard as HTMLElement).getByText("Restored Context")
|
|
157
|
+
).toBeInTheDocument();
|
|
158
|
+
expect(
|
|
159
|
+
within(rollbackCard as HTMLElement).getByText("Validate inputs")
|
|
160
|
+
).toBeInTheDocument();
|
|
161
|
+
|
|
162
|
+
const showDiffButtons = screen.getAllByRole("button", { name: /Show Diff/i });
|
|
163
|
+
fireEvent.click(showDiffButtons[0]);
|
|
164
|
+
|
|
165
|
+
const diffPanel = await screen.findByText("Unified Diff");
|
|
166
|
+
expect(diffPanel).toBeInTheDocument();
|
|
167
|
+
expect(screen.getByText("vs v3")).toBeInTheDocument();
|
|
168
|
+
expect(
|
|
169
|
+
screen.getByText((content, element) => {
|
|
170
|
+
return content === "-" && element?.getAttribute("aria-hidden") === "true";
|
|
171
|
+
})
|
|
172
|
+
).toBeInTheDocument();
|
|
173
|
+
expect(screen.getByRole("button", { name: /Hide Diff/i })).toBeInTheDocument();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -6,6 +6,7 @@ import { toast } from "sonner";
|
|
|
6
6
|
|
|
7
7
|
import { Button } from "@/components/ui/button";
|
|
8
8
|
import { Textarea } from "@/components/ui/textarea";
|
|
9
|
+
import { LightMarkdown } from "@/components/shared/light-markdown";
|
|
9
10
|
|
|
10
11
|
interface ContextProposalReviewProps {
|
|
11
12
|
notificationId: string;
|
|
@@ -99,9 +100,12 @@ export function ContextProposalReview({
|
|
|
99
100
|
placeholder="Edit the proposed context additions..."
|
|
100
101
|
/>
|
|
101
102
|
) : (
|
|
102
|
-
<
|
|
103
|
-
{proposedAdditions}
|
|
104
|
-
|
|
103
|
+
<LightMarkdown
|
|
104
|
+
content={proposedAdditions}
|
|
105
|
+
maxHeight="max-h-48"
|
|
106
|
+
stripBracketTags
|
|
107
|
+
className="rounded-lg bg-background/50 p-3"
|
|
108
|
+
/>
|
|
105
109
|
)}
|
|
106
110
|
</div>
|
|
107
111
|
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { useCallback, useEffect, useState } from "react";
|
|
4
4
|
import {
|
|
5
5
|
Brain,
|
|
6
|
+
ChevronDown,
|
|
7
|
+
ChevronUp,
|
|
6
8
|
Check,
|
|
7
9
|
Clock,
|
|
8
10
|
History,
|
|
@@ -10,7 +12,6 @@ import {
|
|
|
10
12
|
Plus,
|
|
11
13
|
RotateCcw,
|
|
12
14
|
Sparkles,
|
|
13
|
-
X,
|
|
14
15
|
} from "lucide-react";
|
|
15
16
|
import { toast } from "sonner";
|
|
16
17
|
|
|
@@ -18,8 +19,13 @@ import { Badge } from "@/components/ui/badge";
|
|
|
18
19
|
import { Button } from "@/components/ui/button";
|
|
19
20
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
20
21
|
import { Textarea } from "@/components/ui/textarea";
|
|
22
|
+
import { LightMarkdown } from "@/components/shared/light-markdown";
|
|
21
23
|
import { formatTimestamp } from "@/lib/utils/format-timestamp";
|
|
22
24
|
import type { LearnedContextRow } from "@/lib/db/schema";
|
|
25
|
+
import {
|
|
26
|
+
buildLearnedContextHistoryEntries,
|
|
27
|
+
hasMeaningfulDerivedDiff,
|
|
28
|
+
} from "@/lib/utils/learned-context-history";
|
|
23
29
|
|
|
24
30
|
interface ContextHistoryResponse {
|
|
25
31
|
history: LearnedContextRow[];
|
|
@@ -49,6 +55,7 @@ export function LearnedContextPanel({ profileId }: LearnedContextPanelProps) {
|
|
|
49
55
|
const [addingManual, setAddingManual] = useState(false);
|
|
50
56
|
const [manualContent, setManualContent] = useState("");
|
|
51
57
|
const [submitting, setSubmitting] = useState(false);
|
|
58
|
+
const [expandedDiffs, setExpandedDiffs] = useState<Record<string, boolean>>({});
|
|
52
59
|
|
|
53
60
|
const refresh = useCallback(async () => {
|
|
54
61
|
try {
|
|
@@ -66,6 +73,10 @@ export function LearnedContextPanel({ profileId }: LearnedContextPanelProps) {
|
|
|
66
73
|
refresh();
|
|
67
74
|
}, [refresh]);
|
|
68
75
|
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
setExpandedDiffs({});
|
|
78
|
+
}, [profileId]);
|
|
79
|
+
|
|
69
80
|
async function handleAddManual() {
|
|
70
81
|
if (!manualContent.trim()) return;
|
|
71
82
|
setSubmitting(true);
|
|
@@ -121,6 +132,7 @@ export function LearnedContextPanel({ profileId }: LearnedContextPanelProps) {
|
|
|
121
132
|
}
|
|
122
133
|
|
|
123
134
|
const history = data?.history ?? [];
|
|
135
|
+
const entries = buildLearnedContextHistoryEntries(history);
|
|
124
136
|
const currentSize = data?.currentSize ?? 0;
|
|
125
137
|
const limit = data?.limit ?? 8000;
|
|
126
138
|
const usagePercent = Math.min((currentSize / limit) * 100, 100);
|
|
@@ -134,7 +146,7 @@ export function LearnedContextPanel({ profileId }: LearnedContextPanelProps) {
|
|
|
134
146
|
Learned Context
|
|
135
147
|
{history.length > 0 && (
|
|
136
148
|
<Badge variant="secondary" className="text-xs">
|
|
137
|
-
{history.length} versions
|
|
149
|
+
{history.length} {history.length === 1 ? "version" : "versions"}
|
|
138
150
|
</Badge>
|
|
139
151
|
)}
|
|
140
152
|
</CardTitle>
|
|
@@ -223,11 +235,25 @@ export function LearnedContextPanel({ profileId }: LearnedContextPanelProps) {
|
|
|
223
235
|
Version History
|
|
224
236
|
</div>
|
|
225
237
|
<div className="max-h-72 space-y-2 overflow-y-auto">
|
|
226
|
-
{
|
|
238
|
+
{entries.map((entry) => {
|
|
239
|
+
const { row, snapshotContent, derivedDiff } = entry;
|
|
227
240
|
const badgeConfig = CHANGE_TYPE_BADGE[row.changeType] ?? {
|
|
228
241
|
variant: "outline" as const,
|
|
229
242
|
label: row.changeType,
|
|
230
243
|
};
|
|
244
|
+
const diffId = `learned-context-diff-${row.id}`;
|
|
245
|
+
const diffExpanded = expandedDiffs[row.id] ?? false;
|
|
246
|
+
const canShowDiff = hasMeaningfulDerivedDiff(derivedDiff);
|
|
247
|
+
const previewLabel =
|
|
248
|
+
row.changeType === "rollback"
|
|
249
|
+
? "Restored Context"
|
|
250
|
+
: row.changeType === "summarization"
|
|
251
|
+
? "Summarized Context"
|
|
252
|
+
: row.changeType === "approved"
|
|
253
|
+
? "Approved Context"
|
|
254
|
+
: row.changeType === "proposal"
|
|
255
|
+
? "Proposed Additions"
|
|
256
|
+
: "Change Summary";
|
|
231
257
|
return (
|
|
232
258
|
<div
|
|
233
259
|
key={row.id}
|
|
@@ -269,11 +295,93 @@ export function LearnedContextPanel({ profileId }: LearnedContextPanelProps) {
|
|
|
269
295
|
</span>
|
|
270
296
|
</div>
|
|
271
297
|
</div>
|
|
272
|
-
|
|
273
|
-
<
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
298
|
+
<div className="mt-3 space-y-2">
|
|
299
|
+
<div className="flex items-center justify-between gap-3">
|
|
300
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
301
|
+
{previewLabel}
|
|
302
|
+
</p>
|
|
303
|
+
{canShowDiff && (
|
|
304
|
+
<Button
|
|
305
|
+
size="sm"
|
|
306
|
+
variant="ghost"
|
|
307
|
+
className="h-6 px-2 text-xs"
|
|
308
|
+
aria-expanded={diffExpanded}
|
|
309
|
+
aria-controls={diffId}
|
|
310
|
+
onClick={() =>
|
|
311
|
+
setExpandedDiffs((current) => ({
|
|
312
|
+
...current,
|
|
313
|
+
[row.id]: !current[row.id],
|
|
314
|
+
}))
|
|
315
|
+
}
|
|
316
|
+
>
|
|
317
|
+
{diffExpanded ? (
|
|
318
|
+
<ChevronUp className="mr-1 h-3 w-3" />
|
|
319
|
+
) : (
|
|
320
|
+
<ChevronDown className="mr-1 h-3 w-3" />
|
|
321
|
+
)}
|
|
322
|
+
{diffExpanded ? "Hide Diff" : "Show Diff"}
|
|
323
|
+
</Button>
|
|
324
|
+
)}
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
{snapshotContent ? (
|
|
328
|
+
<LightMarkdown
|
|
329
|
+
content={snapshotContent}
|
|
330
|
+
maxHeight="max-h-28"
|
|
331
|
+
className="rounded-md bg-background/50 p-2"
|
|
332
|
+
/>
|
|
333
|
+
) : row.diff ? (
|
|
334
|
+
<LightMarkdown
|
|
335
|
+
content={row.diff}
|
|
336
|
+
maxHeight="max-h-24"
|
|
337
|
+
className="rounded-md bg-background/50 p-2"
|
|
338
|
+
/>
|
|
339
|
+
) : null}
|
|
340
|
+
|
|
341
|
+
{canShowDiff && diffExpanded && derivedDiff && (
|
|
342
|
+
<div
|
|
343
|
+
id={diffId}
|
|
344
|
+
className="surface-scroll rounded-md border border-border/60 bg-background/60 p-2"
|
|
345
|
+
>
|
|
346
|
+
<div className="mb-2 flex items-center justify-between gap-2 text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
347
|
+
<span>Unified Diff</span>
|
|
348
|
+
<span>
|
|
349
|
+
{derivedDiff.previousVersion === null
|
|
350
|
+
? "Initial version"
|
|
351
|
+
: `vs v${derivedDiff.previousVersion}`}
|
|
352
|
+
</span>
|
|
353
|
+
</div>
|
|
354
|
+
<div className="max-h-44 space-y-1 overflow-y-auto font-mono text-xs">
|
|
355
|
+
{derivedDiff.lines.map((line, index) => {
|
|
356
|
+
const toneClass =
|
|
357
|
+
line.kind === "added"
|
|
358
|
+
? "bg-status-completed/10 text-status-completed"
|
|
359
|
+
: line.kind === "removed"
|
|
360
|
+
? "bg-destructive/10 text-destructive"
|
|
361
|
+
: "bg-muted/40 text-muted-foreground";
|
|
362
|
+
const prefix =
|
|
363
|
+
line.kind === "added"
|
|
364
|
+
? "+"
|
|
365
|
+
: line.kind === "removed"
|
|
366
|
+
? "-"
|
|
367
|
+
: " ";
|
|
368
|
+
|
|
369
|
+
return (
|
|
370
|
+
<div
|
|
371
|
+
key={`${row.id}-diff-${index}`}
|
|
372
|
+
className={`grid grid-cols-[auto_minmax(0,1fr)] gap-2 rounded-sm px-2 py-1 ${toneClass}`}
|
|
373
|
+
>
|
|
374
|
+
<span aria-hidden="true">{prefix}</span>
|
|
375
|
+
<span className="whitespace-pre-wrap break-words">
|
|
376
|
+
{line.value || " "}
|
|
377
|
+
</span>
|
|
378
|
+
</div>
|
|
379
|
+
);
|
|
380
|
+
})}
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
)}
|
|
384
|
+
</div>
|
|
277
385
|
</div>
|
|
278
386
|
);
|
|
279
387
|
})}
|
|
@@ -6,6 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
6
6
|
import { Badge } from "@/components/ui/badge";
|
|
7
7
|
import { Button } from "@/components/ui/button";
|
|
8
8
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
9
|
+
import { LightMarkdown } from "@/components/shared/light-markdown";
|
|
9
10
|
import {
|
|
10
11
|
Copy,
|
|
11
12
|
Pencil,
|
|
@@ -18,7 +19,6 @@ import {
|
|
|
18
19
|
Sparkles,
|
|
19
20
|
Tag,
|
|
20
21
|
User,
|
|
21
|
-
Thermometer,
|
|
22
22
|
Repeat,
|
|
23
23
|
FileOutput,
|
|
24
24
|
Wrench,
|
|
@@ -262,20 +262,6 @@ export function ProfileDetailView({ profileId, isBuiltin, initialProfile }: Prof
|
|
|
262
262
|
<CardTitle className="text-sm font-medium">Configuration</CardTitle>
|
|
263
263
|
</CardHeader>
|
|
264
264
|
<CardContent className="space-y-3">
|
|
265
|
-
{/* Temperature Gauge */}
|
|
266
|
-
{profile.temperature !== undefined && (
|
|
267
|
-
<div className="flex items-center gap-2">
|
|
268
|
-
<Thermometer className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
269
|
-
<span className="text-xs text-muted-foreground w-20">Temperature</span>
|
|
270
|
-
<div className="flex-1 h-1.5 rounded-full bg-muted">
|
|
271
|
-
<div
|
|
272
|
-
className="h-full rounded-full bg-primary"
|
|
273
|
-
style={{ width: `${(profile.temperature / 2) * 100}%` }}
|
|
274
|
-
/>
|
|
275
|
-
</div>
|
|
276
|
-
<span className="text-xs font-medium w-8 text-right">{profile.temperature}</span>
|
|
277
|
-
</div>
|
|
278
|
-
)}
|
|
279
265
|
{/* Max Turns */}
|
|
280
266
|
{profile.maxTurns !== undefined && (
|
|
281
267
|
<div className="flex items-center gap-2">
|
|
@@ -294,7 +280,7 @@ export function ProfileDetailView({ profileId, isBuiltin, initialProfile }: Prof
|
|
|
294
280
|
</Badge>
|
|
295
281
|
</div>
|
|
296
282
|
)}
|
|
297
|
-
{!profile.
|
|
283
|
+
{!profile.maxTurns && !profile.outputFormat && (
|
|
298
284
|
<p className="text-sm text-muted-foreground">Default configuration</p>
|
|
299
285
|
)}
|
|
300
286
|
</CardContent>
|
|
@@ -435,9 +421,11 @@ export function ProfileDetailView({ profileId, isBuiltin, initialProfile }: Prof
|
|
|
435
421
|
<span className="text-muted-foreground text-xs group-open:rotate-90 transition-transform">▶</span>
|
|
436
422
|
</summary>
|
|
437
423
|
<div className="surface-panel mt-2 rounded-lg p-4">
|
|
438
|
-
<
|
|
439
|
-
{profile.skillMd}
|
|
440
|
-
|
|
424
|
+
<LightMarkdown
|
|
425
|
+
content={profile.skillMd}
|
|
426
|
+
maxHeight="max-h-64"
|
|
427
|
+
className="surface-scroll rounded-lg p-4"
|
|
428
|
+
/>
|
|
441
429
|
</div>
|
|
442
430
|
</details>
|
|
443
431
|
)}
|
|
@@ -70,7 +70,6 @@ export function ProfileFormView({
|
|
|
70
70
|
]);
|
|
71
71
|
const [codexInstructions, setCodexInstructions] = useState("");
|
|
72
72
|
const [allowedTools, setAllowedTools] = useState("");
|
|
73
|
-
const [temperature, setTemperature] = useState(0.5);
|
|
74
73
|
const [maxTurns, setMaxTurns] = useState(30);
|
|
75
74
|
const [outputFormat, setOutputFormat] = useState("");
|
|
76
75
|
const [submitting, setSubmitting] = useState(false);
|
|
@@ -94,7 +93,6 @@ export function ProfileFormView({
|
|
|
94
93
|
profile.runtimeOverrides?.["openai-codex-app-server"]?.instructions ?? ""
|
|
95
94
|
);
|
|
96
95
|
setAllowedTools(profile.allowedTools?.join(", ") ?? "");
|
|
97
|
-
setTemperature(profile.temperature ?? 0.5);
|
|
98
96
|
setMaxTurns(profile.maxTurns ?? 30);
|
|
99
97
|
setOutputFormat(profile.outputFormat ?? "");
|
|
100
98
|
})
|
|
@@ -145,7 +143,6 @@ export function ProfileFormView({
|
|
|
145
143
|
}
|
|
146
144
|
: undefined,
|
|
147
145
|
allowedTools: parseCommaSeparated(allowedTools),
|
|
148
|
-
temperature,
|
|
149
146
|
maxTurns,
|
|
150
147
|
outputFormat: outputFormat.trim() || undefined,
|
|
151
148
|
};
|
|
@@ -307,25 +304,6 @@ export function ProfileFormView({
|
|
|
307
304
|
{/* Model Tuning */}
|
|
308
305
|
<FormSectionCard icon={SlidersHorizontal} title="Model Tuning">
|
|
309
306
|
<div className="space-y-4">
|
|
310
|
-
<div className="space-y-2">
|
|
311
|
-
<div className="flex items-center justify-between">
|
|
312
|
-
<Label htmlFor="profile-temp">Temperature</Label>
|
|
313
|
-
<Badge variant="secondary" className="tabular-nums text-xs">
|
|
314
|
-
{temperature.toFixed(2)}
|
|
315
|
-
</Badge>
|
|
316
|
-
</div>
|
|
317
|
-
<div className="slider-temperature">
|
|
318
|
-
<Slider
|
|
319
|
-
id="profile-temp"
|
|
320
|
-
min={0}
|
|
321
|
-
max={1}
|
|
322
|
-
step={0.05}
|
|
323
|
-
value={[temperature]}
|
|
324
|
-
onValueChange={([v]) => setTemperature(v)}
|
|
325
|
-
/>
|
|
326
|
-
</div>
|
|
327
|
-
<p className="text-xs text-muted-foreground">Lower = deterministic, higher = creative</p>
|
|
328
|
-
</div>
|
|
329
307
|
<div className="space-y-2">
|
|
330
308
|
<div className="flex items-center justify-between">
|
|
331
309
|
<Label htmlFor="profile-turns">Max Turns</Label>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { AuthConfigSection } from "@/components/settings/auth-config-section";
|
|
5
|
+
|
|
6
|
+
vi.mock("@/lib/agents/runtime/catalog", () => ({
|
|
7
|
+
DEFAULT_AGENT_RUNTIME: "claude-code",
|
|
8
|
+
getRuntimeCatalogEntry: () => ({
|
|
9
|
+
id: "claude-code",
|
|
10
|
+
label: "Claude Code",
|
|
11
|
+
}),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe("auth config section", () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
vi.stubGlobal(
|
|
18
|
+
"fetch",
|
|
19
|
+
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
20
|
+
const url = String(input);
|
|
21
|
+
const method = init?.method ?? "GET";
|
|
22
|
+
|
|
23
|
+
if (url === "/api/settings" && method === "GET") {
|
|
24
|
+
return {
|
|
25
|
+
ok: true,
|
|
26
|
+
json: async () => ({
|
|
27
|
+
method: "oauth",
|
|
28
|
+
hasKey: false,
|
|
29
|
+
apiKeySource: "oauth",
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (url === "/api/settings/test" && method === "POST") {
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
json: async () => ({
|
|
38
|
+
connected: true,
|
|
39
|
+
apiKeySource: "oauth",
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (url === "/api/settings" && method === "POST") {
|
|
45
|
+
const body = JSON.parse(String(init?.body ?? "{}")) as { method?: string };
|
|
46
|
+
return {
|
|
47
|
+
ok: true,
|
|
48
|
+
json: async () => ({
|
|
49
|
+
method: body.method ?? "oauth",
|
|
50
|
+
hasKey: false,
|
|
51
|
+
apiKeySource: body.method === "oauth" ? "oauth" : "unknown",
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
throw new Error(`Unexpected fetch: ${method} ${url}`);
|
|
57
|
+
})
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
vi.unstubAllGlobals();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("renders an OAuth test button and shows success feedback", async () => {
|
|
66
|
+
render(<AuthConfigSection />);
|
|
67
|
+
|
|
68
|
+
const testButton = await screen.findByRole("button", { name: "Test Connection" });
|
|
69
|
+
expect(testButton).toBeInTheDocument();
|
|
70
|
+
expect(screen.queryByText("Test OAuth connection")).not.toBeInTheDocument();
|
|
71
|
+
|
|
72
|
+
fireEvent.click(testButton);
|
|
73
|
+
|
|
74
|
+
await waitFor(() => {
|
|
75
|
+
expect(screen.getByText("Connected")).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("shows the returned error message when the OAuth test fails", async () => {
|
|
80
|
+
vi.stubGlobal(
|
|
81
|
+
"fetch",
|
|
82
|
+
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
83
|
+
const url = String(input);
|
|
84
|
+
const method = init?.method ?? "GET";
|
|
85
|
+
|
|
86
|
+
if (url === "/api/settings" && method === "GET") {
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
json: async () => ({
|
|
90
|
+
method: "oauth",
|
|
91
|
+
hasKey: false,
|
|
92
|
+
apiKeySource: "oauth",
|
|
93
|
+
}),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (url === "/api/settings/test" && method === "POST") {
|
|
98
|
+
return {
|
|
99
|
+
ok: true,
|
|
100
|
+
json: async () => ({
|
|
101
|
+
connected: false,
|
|
102
|
+
error: "OAuth token expired",
|
|
103
|
+
}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (url === "/api/settings" && method === "POST") {
|
|
108
|
+
const body = JSON.parse(String(init?.body ?? "{}")) as { method?: string };
|
|
109
|
+
return {
|
|
110
|
+
ok: true,
|
|
111
|
+
json: async () => ({
|
|
112
|
+
method: body.method ?? "oauth",
|
|
113
|
+
hasKey: false,
|
|
114
|
+
apiKeySource: body.method === "oauth" ? "oauth" : "unknown",
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
throw new Error(`Unexpected fetch: ${method} ${url}`);
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
render(<AuthConfigSection />);
|
|
124
|
+
|
|
125
|
+
fireEvent.click(await screen.findByRole("button", { name: "Test Connection" }));
|
|
126
|
+
|
|
127
|
+
await waitFor(() => {
|
|
128
|
+
expect(screen.getByText("OAuth token expired")).toBeInTheDocument();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("clears the inline test result when switching auth methods", async () => {
|
|
133
|
+
render(<AuthConfigSection />);
|
|
134
|
+
|
|
135
|
+
fireEvent.click(await screen.findByRole("button", { name: "Test Connection" }));
|
|
136
|
+
|
|
137
|
+
await waitFor(() => {
|
|
138
|
+
expect(screen.getByText("Connected")).toBeInTheDocument();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
fireEvent.click(screen.getByRole("button", { name: /API Key/i }));
|
|
142
|
+
|
|
143
|
+
await waitFor(() => {
|
|
144
|
+
expect(screen.queryByText("Connected")).not.toBeInTheDocument();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|