stagent 0.1.0 → 0.1.2

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.
Files changed (64) hide show
  1. package/README.md +33 -30
  2. package/dist/cli.js +376 -49
  3. package/package.json +23 -24
  4. package/public/desktop-icon-512.png +0 -0
  5. package/public/icon-512.png +0 -0
  6. package/src/app/api/data/clear/route.ts +0 -7
  7. package/src/app/api/data/seed/route.ts +0 -7
  8. package/src/app/api/profiles/[id]/context/route.ts +109 -0
  9. package/src/components/dashboard/__tests__/accessibility.test.tsx +42 -0
  10. package/src/components/documents/__tests__/document-upload-dialog.test.tsx +46 -0
  11. package/src/components/notifications/__tests__/pending-approval-host.test.tsx +122 -0
  12. package/src/components/notifications/__tests__/permission-response-actions.test.tsx +79 -0
  13. package/src/components/notifications/pending-approval-host.tsx +49 -25
  14. package/src/components/profiles/context-proposal-review.tsx +145 -0
  15. package/src/components/profiles/learned-context-panel.tsx +286 -0
  16. package/src/components/profiles/profile-detail-view.tsx +4 -0
  17. package/src/components/projects/__tests__/dialog-focus.test.tsx +87 -0
  18. package/src/components/tasks/__tests__/kanban-board-accessibility.test.tsx +59 -0
  19. package/src/lib/__tests__/setup-verify.test.ts +28 -0
  20. package/src/lib/__tests__/utils.test.ts +29 -0
  21. package/src/lib/agents/__tests__/claude-agent.test.ts +946 -0
  22. package/src/lib/agents/__tests__/execution-manager.test.ts +63 -0
  23. package/src/lib/agents/__tests__/router.test.ts +61 -0
  24. package/src/lib/agents/claude-agent.ts +34 -5
  25. package/src/lib/agents/learned-context.ts +322 -0
  26. package/src/lib/agents/pattern-extractor.ts +150 -0
  27. package/src/lib/agents/profiles/__tests__/compatibility.test.ts +76 -0
  28. package/src/lib/agents/profiles/__tests__/registry.test.ts +177 -0
  29. package/src/lib/agents/profiles/builtins/sweep/SKILL.md +47 -0
  30. package/src/lib/agents/profiles/builtins/sweep/profile.yaml +12 -0
  31. package/src/lib/agents/runtime/__tests__/catalog.test.ts +38 -0
  32. package/src/lib/agents/runtime/openai-codex.ts +1 -1
  33. package/src/lib/agents/sweep.ts +65 -0
  34. package/src/lib/constants/__tests__/task-status.test.ts +119 -0
  35. package/src/lib/data/seed-data/__tests__/profiles.test.ts +141 -0
  36. package/src/lib/db/__tests__/bootstrap.test.ts +56 -0
  37. package/src/lib/db/bootstrap.ts +301 -0
  38. package/src/lib/db/index.ts +2 -205
  39. package/src/lib/db/migrations/0004_add_documents.sql +2 -1
  40. package/src/lib/db/migrations/0005_add_document_preprocessing.sql +2 -0
  41. package/src/lib/db/migrations/0006_add_agent_profile.sql +1 -0
  42. package/src/lib/db/migrations/0007_add_usage_metering_ledger.sql +9 -2
  43. package/src/lib/db/migrations/meta/_journal.json +43 -1
  44. package/src/lib/db/schema.ts +34 -0
  45. package/src/lib/desktop/__tests__/sidecar-launch.test.ts +70 -0
  46. package/src/lib/desktop/sidecar-launch.ts +85 -0
  47. package/src/lib/documents/__tests__/context-builder.test.ts +57 -0
  48. package/src/lib/documents/__tests__/output-scanner.test.ts +141 -0
  49. package/src/lib/notifications/actionable.ts +21 -7
  50. package/src/lib/settings/__tests__/auth.test.ts +220 -0
  51. package/src/lib/settings/__tests__/budget-guardrails.test.ts +181 -0
  52. package/src/lib/tauri-bridge.ts +138 -0
  53. package/src/lib/usage/__tests__/ledger.test.ts +284 -0
  54. package/src/lib/utils/__tests__/crypto.test.ts +90 -0
  55. package/src/lib/validators/__tests__/profile.test.ts +119 -0
  56. package/src/lib/validators/__tests__/project.test.ts +82 -0
  57. package/src/lib/validators/__tests__/settings.test.ts +151 -0
  58. package/src/lib/validators/__tests__/task.test.ts +144 -0
  59. package/src/lib/workflows/__tests__/definition-validation.test.ts +164 -0
  60. package/src/lib/workflows/__tests__/engine.test.ts +114 -0
  61. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  62. package/src/lib/workflows/__tests__/parallel.test.ts +75 -0
  63. package/src/lib/workflows/__tests__/swarm.test.ts +97 -0
  64. package/src/test/setup.ts +10 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stagent",
3
- "version": "0.1.0",
4
- "description": "Governed AI agent workspace for supervised local execution, workflows, documents, and provider runtimes.",
3
+ "version": "0.1.2",
4
+ "description": "Governed desktop AI agent workspace for supervised local execution, workflows, documents, and provider runtimes.",
5
5
  "keywords": [
6
6
  "ai",
7
7
  "agents",
@@ -15,54 +15,54 @@
15
15
  ],
16
16
  "license": "Apache-2.0",
17
17
  "type": "module",
18
- "repository": {
19
- "type": "git",
20
- "url": "https://github.com/navam-io/stagent.git"
21
- },
22
- "bugs": {
23
- "url": "https://github.com/navam-io/stagent/issues"
24
- },
25
- "homepage": "https://github.com/navam-io/stagent#readme",
26
18
  "bin": {
27
19
  "stagent": "./dist/cli.js"
28
20
  },
29
21
  "files": [
30
22
  "dist/",
31
23
  "src/",
32
- "!src/**/__tests__/",
33
- "!src/**/*.test.ts",
34
- "!src/**/*.test.tsx",
35
- "!src/**/*.spec.ts",
36
- "!src/**/*.spec.tsx",
37
- "!src/test/",
38
24
  "public/",
39
25
  "next.config.mjs",
40
26
  "tsconfig.json",
41
- "drizzle.config.ts",
42
27
  "postcss.config.mjs",
43
28
  "components.json",
44
- "package.json"
29
+ "drizzle.config.ts"
45
30
  ],
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/navam-io/stagent.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/navam-io/stagent/issues"
37
+ },
38
+ "homepage": "https://github.com/navam-io/stagent#readme",
46
39
  "scripts": {
47
40
  "dev": "next dev --turbopack",
48
41
  "build": "next build",
49
42
  "build:cli": "tsup",
50
- "prepublishOnly": "npm run build:cli",
51
- "smoke:npm": "node scripts/smoke-npm-package.mjs",
43
+ "desktop:dev": "node scripts/tauri.mjs dev",
44
+ "desktop:build": "node scripts/tauri.mjs build",
45
+ "desktop:smoke": "node scripts/desktop-sidecar-smoke.mjs",
46
+ "desktop:smoke:dmg": "node scripts/desktop-mounted-dmg-smoke.mjs",
47
+ "desktop:icon": "node scripts/tauri.mjs icon",
48
+ "desktop:release": "node scripts/release-desktop.mjs",
52
49
  "test": "vitest run",
53
50
  "test:watch": "vitest",
54
51
  "test:coverage": "vitest run --coverage",
55
- "test:ui": "vitest --ui"
52
+ "test:ui": "vitest --ui",
53
+ "prepublishOnly": "npm run build:cli"
56
54
  },
57
55
  "engines": {
58
56
  "node": ">=20.0.0"
59
57
  },
60
58
  "dependencies": {
61
59
  "@anthropic-ai/claude-agent-sdk": "^0.2.71",
60
+ "@anthropic-ai/sdk": "^0.78.0",
62
61
  "@dnd-kit/core": "^6.3.1",
63
62
  "@dnd-kit/sortable": "^10.0.0",
64
63
  "@dnd-kit/utilities": "^3.2.2",
65
64
  "@hookform/resolvers": "^5.2.2",
65
+ "@tailwindcss/postcss": "^4",
66
66
  "@tailwindcss/typography": "^0.5",
67
67
  "better-sqlite3": "^12",
68
68
  "class-variance-authority": "^0.7.1",
@@ -90,10 +90,11 @@
90
90
  "tailwind-merge": "^3",
91
91
  "tw-animate-css": "^1",
92
92
  "xlsx": "^0.18.5",
93
+ "tailwindcss": "^4",
94
+ "typescript": "^5",
93
95
  "zod": "^4.3.6"
94
96
  },
95
97
  "devDependencies": {
96
- "@tailwindcss/postcss": "^4",
97
98
  "@testing-library/dom": "^10.4.1",
98
99
  "@testing-library/jest-dom": "^6.9.1",
99
100
  "@testing-library/react": "^16.3.2",
@@ -106,9 +107,7 @@
106
107
  "@vitest/coverage-v8": "^4.0.18",
107
108
  "drizzle-kit": "^0.30",
108
109
  "jsdom": "^28.1.0",
109
- "tailwindcss": "^4",
110
110
  "tsup": "^8.5",
111
- "typescript": "^5",
112
111
  "vitest": "^4.0.18"
113
112
  }
114
113
  }
Binary file
Binary file
@@ -2,13 +2,6 @@ import { NextResponse } from "next/server";
2
2
  import { clearAllData } from "@/lib/data/clear";
3
3
 
4
4
  export async function POST() {
5
- if (process.env.NODE_ENV === "production") {
6
- return NextResponse.json(
7
- { error: "This endpoint is disabled in production" },
8
- { status: 403 }
9
- );
10
- }
11
-
12
5
  try {
13
6
  const deleted = clearAllData();
14
7
  return NextResponse.json({ success: true, deleted });
@@ -2,13 +2,6 @@ import { NextResponse } from "next/server";
2
2
  import { seedSampleData } from "@/lib/data/seed";
3
3
 
4
4
  export async function POST() {
5
- if (process.env.NODE_ENV === "production") {
6
- return NextResponse.json(
7
- { error: "This endpoint is disabled in production" },
8
- { status: 403 }
9
- );
10
- }
11
-
12
5
  try {
13
6
  const seeded = await seedSampleData();
14
7
  return NextResponse.json({ success: true, seeded });
@@ -0,0 +1,109 @@
1
+ import { NextResponse } from "next/server";
2
+ import {
3
+ getContextHistory,
4
+ approveProposal,
5
+ rejectProposal,
6
+ rollbackToVersion,
7
+ addDirectContext,
8
+ checkContextSize,
9
+ } from "@/lib/agents/learned-context";
10
+
11
+ interface RouteParams {
12
+ params: Promise<{ id: string }>;
13
+ }
14
+
15
+ /** GET /api/profiles/[id]/context — version history + size info */
16
+ export async function GET(_request: Request, { params }: RouteParams) {
17
+ const { id: profileId } = await params;
18
+
19
+ const history = await getContextHistory(profileId);
20
+ const sizeInfo = checkContextSize(profileId);
21
+
22
+ return NextResponse.json({ history, ...sizeInfo });
23
+ }
24
+
25
+ /** POST /api/profiles/[id]/context — manual direct addition */
26
+ export async function POST(request: Request, { params }: RouteParams) {
27
+ const { id: profileId } = await params;
28
+
29
+ const body = await request.json();
30
+ const { additions } = body as { additions?: string };
31
+
32
+ if (!additions || typeof additions !== "string" || !additions.trim()) {
33
+ return NextResponse.json(
34
+ { error: "additions is required" },
35
+ { status: 400 }
36
+ );
37
+ }
38
+
39
+ await addDirectContext(profileId, additions.trim());
40
+
41
+ return NextResponse.json({ ok: true });
42
+ }
43
+
44
+ /** PATCH /api/profiles/[id]/context — approve / reject / rollback */
45
+ export async function PATCH(request: Request, { params }: RouteParams) {
46
+ const { id: profileId } = await params;
47
+
48
+ const body = await request.json();
49
+ const { action, notificationId, targetVersion, editedContent } = body as {
50
+ action?: string;
51
+ notificationId?: string;
52
+ targetVersion?: number;
53
+ editedContent?: string;
54
+ };
55
+
56
+ if (!action) {
57
+ return NextResponse.json(
58
+ { error: "action is required (approve | reject | rollback)" },
59
+ { status: 400 }
60
+ );
61
+ }
62
+
63
+ try {
64
+ switch (action) {
65
+ case "approve":
66
+ if (!notificationId) {
67
+ return NextResponse.json(
68
+ { error: "notificationId is required for approve" },
69
+ { status: 400 }
70
+ );
71
+ }
72
+ await approveProposal(notificationId, editedContent ?? undefined);
73
+ break;
74
+
75
+ case "reject":
76
+ if (!notificationId) {
77
+ return NextResponse.json(
78
+ { error: "notificationId is required for reject" },
79
+ { status: 400 }
80
+ );
81
+ }
82
+ await rejectProposal(notificationId);
83
+ break;
84
+
85
+ case "rollback":
86
+ if (targetVersion === undefined) {
87
+ return NextResponse.json(
88
+ { error: "targetVersion is required for rollback" },
89
+ { status: 400 }
90
+ );
91
+ }
92
+ await rollbackToVersion(profileId, targetVersion);
93
+ break;
94
+
95
+ default:
96
+ return NextResponse.json(
97
+ { error: `Unknown action: ${action}` },
98
+ { status: 400 }
99
+ );
100
+ }
101
+ } catch (error) {
102
+ return NextResponse.json(
103
+ { error: error instanceof Error ? error.message : String(error) },
104
+ { status: 400 }
105
+ );
106
+ }
107
+
108
+ return NextResponse.json({ ok: true });
109
+ }
@@ -0,0 +1,42 @@
1
+ import { render } from "@testing-library/react";
2
+
3
+ import { ActivityFeed } from "@/components/dashboard/activity-feed";
4
+ import { PriorityQueue } from "@/components/dashboard/priority-queue";
5
+
6
+ describe("dashboard accessibility surfaces", () => {
7
+ it("marks the priority queue updates as a polite live region", () => {
8
+ const { container } = render(
9
+ <PriorityQueue
10
+ tasks={[
11
+ {
12
+ id: "task-1",
13
+ title: "Fix runtime mismatch",
14
+ status: "failed",
15
+ priority: 0,
16
+ projectName: "Stagent",
17
+ },
18
+ ]}
19
+ />
20
+ );
21
+
22
+ expect(container.querySelector('.space-y-1[aria-live="polite"]')).not.toBeNull();
23
+ });
24
+
25
+ it("marks the activity feed updates as a polite live region", () => {
26
+ const { container } = render(
27
+ <ActivityFeed
28
+ entries={[
29
+ {
30
+ id: "entry-1",
31
+ event: "completed",
32
+ payload: "Wrote a summary",
33
+ timestamp: new Date("2026-03-12T09:00:00.000Z").toISOString(),
34
+ taskTitle: "Summarize roadmap",
35
+ },
36
+ ]}
37
+ />
38
+ );
39
+
40
+ expect(container.querySelector('.space-y-1[aria-live="polite"]')).not.toBeNull();
41
+ });
42
+ });
@@ -0,0 +1,46 @@
1
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
+ import { useRef, useState } from "react";
3
+
4
+ import { DocumentUploadDialog } from "@/components/documents/document-upload-dialog";
5
+
6
+ vi.mock("sonner", () => ({
7
+ toast: {
8
+ success: vi.fn(),
9
+ error: vi.fn(),
10
+ },
11
+ }));
12
+
13
+ function DocumentUploadHarness() {
14
+ const [open, setOpen] = useState(false);
15
+ const triggerRef = useRef<HTMLButtonElement>(null);
16
+
17
+ return (
18
+ <>
19
+ <button ref={triggerRef} type="button" onClick={() => setOpen(true)}>
20
+ Upload documents
21
+ </button>
22
+ <DocumentUploadDialog
23
+ open={open}
24
+ onClose={() => setOpen(false)}
25
+ onUploaded={() => {}}
26
+ restoreFocusElement={triggerRef.current}
27
+ />
28
+ </>
29
+ );
30
+ }
31
+
32
+ describe("document upload dialog accessibility", () => {
33
+ it("returns focus to the opener when the dialog closes", async () => {
34
+ render(<DocumentUploadHarness />);
35
+
36
+ const trigger = screen.getByRole("button", { name: "Upload documents" });
37
+ fireEvent.click(trigger);
38
+
39
+ const doneButton = await screen.findByRole("button", { name: "Done" });
40
+ fireEvent.click(doneButton);
41
+
42
+ await waitFor(() => {
43
+ expect(trigger).toHaveFocus();
44
+ });
45
+ });
46
+ });
@@ -0,0 +1,122 @@
1
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
+
3
+ import { PendingApprovalHost } from "@/components/notifications/pending-approval-host";
4
+
5
+ const { push } = vi.hoisted(() => ({
6
+ push: vi.fn(),
7
+ }));
8
+
9
+ vi.mock("next/navigation", () => ({
10
+ useRouter: () => ({
11
+ push,
12
+ }),
13
+ usePathname: () => "/dashboard",
14
+ }));
15
+
16
+ class EventSourceMock {
17
+ onerror: ((event: Event) => void) | null = null;
18
+ onmessage: ((event: MessageEvent) => void) | null = null;
19
+
20
+ close() {}
21
+ }
22
+
23
+ const approvals = [
24
+ {
25
+ channel: "in_app" as const,
26
+ notificationId: "notif-1",
27
+ taskId: "task-1",
28
+ workflowId: "workflow-1",
29
+ toolName: "Bash",
30
+ permissionLabel: "Bash",
31
+ compactSummary: "npm run build",
32
+ deepLink: "/tasks/task-1",
33
+ supportedActionIds: ["allow_once", "always_allow", "deny", "open_inbox"],
34
+ title: "Permission required",
35
+ body: "The agent wants to run the build before publishing.",
36
+ taskTitle: "Review workspace",
37
+ workflowName: "Workspace sync",
38
+ toolInput: { command: "npm run build" },
39
+ createdAt: "2026-03-12T15:00:00.000Z",
40
+ read: false,
41
+ },
42
+ {
43
+ channel: "in_app" as const,
44
+ notificationId: "notif-2",
45
+ taskId: "task-2",
46
+ workflowId: null,
47
+ toolName: "Write",
48
+ permissionLabel: "Write",
49
+ compactSummary: "/tmp/report.md",
50
+ deepLink: "/tasks/task-2",
51
+ supportedActionIds: ["allow_once", "always_allow", "deny", "open_inbox"],
52
+ title: "Permission required",
53
+ body: null,
54
+ taskTitle: "Write release brief",
55
+ workflowName: null,
56
+ toolInput: { path: "/tmp/report.md" },
57
+ createdAt: "2026-03-12T14:59:00.000Z",
58
+ read: false,
59
+ },
60
+ ];
61
+
62
+ describe("pending approval host", () => {
63
+ beforeEach(() => {
64
+ Object.defineProperty(window, "innerWidth", {
65
+ configurable: true,
66
+ value: 1280,
67
+ });
68
+
69
+ vi.stubGlobal(
70
+ "matchMedia",
71
+ vi.fn().mockImplementation(() => ({
72
+ matches: false,
73
+ addEventListener: vi.fn(),
74
+ removeEventListener: vi.fn(),
75
+ }))
76
+ );
77
+
78
+ vi.stubGlobal("EventSource", EventSourceMock);
79
+ vi.stubGlobal(
80
+ "fetch",
81
+ vi.fn().mockResolvedValue({
82
+ ok: true,
83
+ json: vi.fn().mockResolvedValue(approvals),
84
+ })
85
+ );
86
+ });
87
+
88
+ afterEach(() => {
89
+ vi.unstubAllGlobals();
90
+ vi.clearAllMocks();
91
+ });
92
+
93
+ it("shows the primary approval toast and exposes overflow requests in the detail dialog", async () => {
94
+ render(<PendingApprovalHost />);
95
+
96
+ await screen.findByText("Workspace sync · Review workspace");
97
+ expect(screen.getByText("+1 more")).toBeInTheDocument();
98
+ expect(screen.getByText("npm run build")).toBeInTheDocument();
99
+
100
+ const trigger = screen.getByRole("button", {
101
+ name: /Workspace sync · Review workspace/i,
102
+ });
103
+ fireEvent.click(trigger);
104
+
105
+ const dialog = await screen.findByRole("dialog");
106
+ expect(dialog).toHaveTextContent("Permission required");
107
+ expect(dialog).toHaveTextContent("Also pending");
108
+ expect(dialog).toHaveTextContent("Write release brief");
109
+
110
+ fireEvent.click(screen.getByRole("button", { name: /Write release brief/i }));
111
+
112
+ await waitFor(() => {
113
+ expect(dialog).toHaveTextContent("/tmp/report.md");
114
+ });
115
+
116
+ fireEvent.click(screen.getByRole("button", { name: "Close" }));
117
+
118
+ await waitFor(() => {
119
+ expect(trigger).toHaveFocus();
120
+ });
121
+ });
122
+ });
@@ -0,0 +1,79 @@
1
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
+
3
+ import { PermissionResponseActions } from "@/components/notifications/permission-response-actions";
4
+
5
+ const { toastError } = vi.hoisted(() => ({
6
+ toastError: vi.fn(),
7
+ }));
8
+
9
+ vi.mock("sonner", () => ({
10
+ toast: {
11
+ error: toastError,
12
+ },
13
+ }));
14
+
15
+ describe("permission response actions", () => {
16
+ beforeEach(() => {
17
+ vi.stubGlobal(
18
+ "fetch",
19
+ vi.fn().mockResolvedValue({
20
+ ok: true,
21
+ json: vi.fn().mockResolvedValue({ success: true }),
22
+ })
23
+ );
24
+ });
25
+
26
+ afterEach(() => {
27
+ vi.unstubAllGlobals();
28
+ vi.clearAllMocks();
29
+ });
30
+
31
+ it("sends the persisted permission pattern for always-allow approvals", async () => {
32
+ const onResponded = vi.fn();
33
+
34
+ render(
35
+ <PermissionResponseActions
36
+ taskId="task-1"
37
+ notificationId="notif-1"
38
+ toolName="Bash"
39
+ toolInput={{ command: "npm run build" }}
40
+ responded={false}
41
+ response={null}
42
+ onResponded={onResponded}
43
+ />
44
+ );
45
+
46
+ fireEvent.click(screen.getByRole("button", { name: "Always Allow" }));
47
+
48
+ await waitFor(() => {
49
+ expect(fetch).toHaveBeenCalledWith("/api/tasks/task-1/respond", {
50
+ method: "POST",
51
+ headers: { "Content-Type": "application/json" },
52
+ body: JSON.stringify({
53
+ notificationId: "notif-1",
54
+ behavior: "allow",
55
+ updatedInput: { command: "npm run build" },
56
+ message: undefined,
57
+ alwaysAllow: true,
58
+ permissionPattern: "Bash(command:npm *)",
59
+ }),
60
+ });
61
+ expect(onResponded).toHaveBeenCalled();
62
+ });
63
+ });
64
+
65
+ it("renders the resolved state label when a response already exists", () => {
66
+ render(
67
+ <PermissionResponseActions
68
+ taskId="task-1"
69
+ notificationId="notif-1"
70
+ toolName="Bash"
71
+ toolInput={{ command: "npm run build" }}
72
+ responded
73
+ response={JSON.stringify({ behavior: "allow", alwaysAllow: true })}
74
+ />
75
+ );
76
+
77
+ expect(screen.getByText("Always allowed")).toBeInTheDocument();
78
+ });
79
+ });
@@ -12,6 +12,7 @@ import {
12
12
  } from "lucide-react";
13
13
 
14
14
  import { PermissionResponseActions } from "@/components/notifications/permission-response-actions";
15
+ import { ContextProposalReview } from "@/components/profiles/context-proposal-review";
15
16
  import { Badge } from "@/components/ui/badge";
16
17
  import {
17
18
  Dialog,
@@ -136,23 +137,34 @@ function PendingApprovalDetail({
136
137
  </p>
137
138
  </div>
138
139
 
139
- <PermissionDetailFields
140
- toolName={selected.toolName}
141
- toolInput={selected.toolInput}
142
- />
143
-
144
- {selected.taskId && selected.toolName && selected.toolInput && (
145
- <PermissionResponseActions
146
- taskId={selected.taskId}
140
+ {selected.notificationType === "context_proposal" ? (
141
+ <ContextProposalReview
147
142
  notificationId={selected.notificationId}
148
- toolName={selected.toolName}
149
- toolInput={selected.toolInput}
150
- responded={false}
151
- response={null}
143
+ profileId={selected.toolName ?? ""}
144
+ proposedAdditions={selected.body ?? ""}
152
145
  onResponded={onResponded}
153
- buttonSize="default"
154
- layout="stacked"
155
146
  />
147
+ ) : (
148
+ <>
149
+ <PermissionDetailFields
150
+ toolName={selected.toolName}
151
+ toolInput={selected.toolInput}
152
+ />
153
+
154
+ {selected.taskId && selected.toolName && selected.toolInput && (
155
+ <PermissionResponseActions
156
+ taskId={selected.taskId}
157
+ notificationId={selected.notificationId}
158
+ toolName={selected.toolName}
159
+ toolInput={selected.toolInput}
160
+ responded={false}
161
+ response={null}
162
+ onResponded={onResponded}
163
+ buttonSize="default"
164
+ layout="stacked"
165
+ />
166
+ )}
167
+ </>
156
168
  )}
157
169
 
158
170
  <div className="flex flex-wrap items-center gap-2">
@@ -393,17 +405,29 @@ export function PendingApprovalHost() {
393
405
  </div>
394
406
  </button>
395
407
 
396
- {primary.taskId && primary.toolName && primary.toolInput && (
397
- <PermissionResponseActions
398
- taskId={primary.taskId}
399
- notificationId={primary.notificationId}
400
- toolName={primary.toolName}
401
- toolInput={primary.toolInput}
402
- responded={false}
403
- response={null}
404
- onResponded={() => removeNotification(primary.notificationId)}
405
- className="mt-3"
406
- />
408
+ {primary.notificationType === "context_proposal" ? (
409
+ <div className="mt-3">
410
+ <ContextProposalReview
411
+ notificationId={primary.notificationId}
412
+ profileId={primary.toolName ?? ""}
413
+ proposedAdditions={primary.body ?? ""}
414
+ onResponded={() => removeNotification(primary.notificationId)}
415
+ compact
416
+ />
417
+ </div>
418
+ ) : (
419
+ primary.taskId && primary.toolName && primary.toolInput && (
420
+ <PermissionResponseActions
421
+ taskId={primary.taskId}
422
+ notificationId={primary.notificationId}
423
+ toolName={primary.toolName}
424
+ toolInput={primary.toolInput}
425
+ responded={false}
426
+ response={null}
427
+ onResponded={() => removeNotification(primary.notificationId)}
428
+ className="mt-3"
429
+ />
430
+ )
407
431
  )}
408
432
 
409
433
  <div className="mt-3 flex items-center justify-between gap-3">