@agent-native/core 0.178.0 → 0.178.1-nightly-20260909221943

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 (88) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/.agents/skills/dictate/SKILL.md +1 -0
  3. package/corpus/templates/clips/DESIGN.md +10 -7
  4. package/corpus/templates/clips/actions/list-organization-state.ts +56 -3
  5. package/corpus/templates/clips/actions/list-recordings.ts +2 -0
  6. package/corpus/templates/clips/actions/search-dictations.ts +81 -0
  7. package/corpus/templates/clips/app/components/agent-destination-logos.tsx +50 -0
  8. package/corpus/templates/clips/app/components/bug-report/bug-report-dialog.tsx +60 -0
  9. package/corpus/templates/clips/app/components/bug-report/bug-report-form.tsx +206 -0
  10. package/corpus/templates/clips/app/components/clips-command-menu.tsx +653 -0
  11. package/corpus/templates/clips/app/components/library/empty-state.tsx +52 -17
  12. package/corpus/templates/clips/app/components/library/folder-card.tsx +34 -0
  13. package/corpus/templates/clips/app/components/library/folder-tree.tsx +109 -64
  14. package/corpus/templates/clips/app/components/library/library-action-hrefs.ts +27 -0
  15. package/corpus/templates/clips/app/components/library/library-grid.tsx +295 -103
  16. package/corpus/templates/clips/app/components/library/library-layout.tsx +381 -203
  17. package/corpus/templates/clips/app/components/library/library-primary-actions.tsx +48 -0
  18. package/corpus/templates/clips/app/components/library/page-header.tsx +48 -8
  19. package/corpus/templates/clips/app/components/library/recording-card.tsx +58 -55
  20. package/corpus/templates/clips/app/components/library/search-bar.tsx +5 -19
  21. package/corpus/templates/clips/app/components/library/sidebar-feedback-button.tsx +48 -0
  22. package/corpus/templates/clips/app/components/player/comments-panel.tsx +2 -2
  23. package/corpus/templates/clips/app/components/player/share-dialog.tsx +344 -260
  24. package/corpus/templates/clips/app/components/player/viewer-controls.tsx +2 -2
  25. package/corpus/templates/clips/app/components/recorder/camera-visualizer.tsx +0 -33
  26. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -1
  27. package/corpus/templates/clips/app/components/ui/tabs.tsx +1 -1
  28. package/corpus/templates/clips/app/hooks/use-command-search.ts +53 -0
  29. package/corpus/templates/clips/app/hooks/use-navigation-state.ts +32 -13
  30. package/corpus/templates/clips/app/hooks/use-upload-video-picker.tsx +1 -0
  31. package/corpus/templates/clips/app/i18n/en-US.ts +7 -0
  32. package/corpus/templates/clips/app/lib/agent-share.ts +16 -0
  33. package/corpus/templates/clips/app/lib/command-events.ts +7 -0
  34. package/corpus/templates/clips/app/root.tsx +6 -82
  35. package/corpus/templates/clips/app/routes/_app.dictate.tsx +25 -5
  36. package/corpus/templates/clips/app/routes/_app.library._index.tsx +2 -30
  37. package/corpus/templates/clips/app/routes/_app.library.folder.$folderId.tsx +13 -2
  38. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +4 -17
  39. package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +111 -114
  40. package/corpus/templates/clips/app/routes/_app.spaces.$spaceId.folder.$folderId.tsx +25 -2
  41. package/corpus/templates/clips/app/routes/_app.spaces.$spaceId.tsx +14 -61
  42. package/corpus/templates/clips/app/routes/_app.spaces._index.tsx +34 -24
  43. package/corpus/templates/clips/app/routes/_app.trash.tsx +1 -1
  44. package/corpus/templates/clips/app/routes/_app.tsx +2 -4
  45. package/corpus/templates/clips/app/routes/bug-report.tsx +11 -189
  46. package/corpus/templates/clips/app/routes/share.$shareId.tsx +148 -123
  47. package/corpus/templates/clips/changelog/2026-09-08-comments-and-desktop-window-controls.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-09-08-global-command-search.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-09-09-agent-sidebar-fits-below-toolbar.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-09-09-desktop-switches-stay-clear-in-dark-mode.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-09-09-recording-agent-uses-global-panel.md +6 -0
  52. package/corpus/templates/clips/desktop/src/app.tsx +2 -2
  53. package/corpus/templates/clips/desktop/src/components/MediaDeviceRow.tsx +5 -3
  54. package/corpus/templates/clips/desktop/src/components/SourceRow.tsx +1 -0
  55. package/corpus/templates/clips/desktop/src/components/Switch.tsx +2 -5
  56. package/corpus/templates/clips/desktop/src/components/ui/switch.tsx +21 -3
  57. package/corpus/templates/clips/desktop/src/hooks/useMediaDevices.ts +72 -52
  58. package/corpus/templates/clips/desktop/src/styles.css +18 -19
  59. package/corpus/templates/clips/desktop/src/tailwind.css +3 -0
  60. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +18 -10
  61. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +3 -5
  62. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +14 -3
  63. package/corpus/templates/design/AGENTS.md +2 -1
  64. package/corpus/templates/design/actions/export-png.ts +119 -0
  65. package/corpus/templates/design/actions/take-design-screenshot.ts +47 -13
  66. package/corpus/templates/design/server/plugins/agent-chat.ts +3 -1
  67. package/corpus/templates/factory/actions/dispatch-factory-item.ts +51 -22
  68. package/corpus/templates/factory/actions/list-triage-items.ts +21 -11
  69. package/corpus/templates/factory/actions/poll-slack-channel.ts +7 -2
  70. package/corpus/templates/factory/changelog/2026-09-09-slack-feedback-keeps-claimed-markers-after-poll.md +6 -0
  71. package/corpus/templates/factory/changelog/2026-09-09-slack-feedback-preserves-started-claimed-threads.md +6 -0
  72. package/corpus/templates/factory/changelog/2026-09-09-slack-feedback-skips-claimed-parents.md +6 -0
  73. package/corpus/templates/factory/server/lib/factory-automation-config.ts +1 -1
  74. package/corpus/templates/factory/server/lib/slack-feedback-prompt.ts +47 -0
  75. package/corpus/templates/factory/server/plugins/agent-chat.ts +5 -2
  76. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +5 -4
  77. package/corpus/templates/factory/server/triage/review-skill-alignment.ts +12 -7
  78. package/corpus/templates/factory/server/triage/slack-review-window.ts +39 -0
  79. package/dist/client/CommandMenu.js +13 -1
  80. package/dist/client/analytics.js +10 -0
  81. package/dist/collab/routes.d.ts +1 -1
  82. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  83. package/dist/notifications/routes.d.ts +2 -2
  84. package/dist/observability/routes.d.ts +5 -5
  85. package/dist/vite/client.js +25 -7
  86. package/dist/vite/sentry-source-maps.d.ts +12 -0
  87. package/dist/vite/sentry-source-maps.js +105 -0
  88. package/package.json +2 -1
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 9088
34
+ - template files: 9110
@@ -40,6 +40,7 @@ Dictate captures **mic only** — system audio is never recorded for dictations.
40
40
  | Action | What it does |
41
41
  | -------------------- | ------------------------------------------------------------------------------------------- |
42
42
  | `list-dictations` | Past dictations, scoped via `accessFilter` |
43
+ | `search-dictations` | Search native or cleaned dictation text, with matching snippets |
43
44
  | `cleanup-dictation` | Polish a single dictation's text (writes `cleanedText`) |
44
45
  | `cleanup-transcript` | Shared cleanup pipeline (also used by Clips + Meetings); resolves credentials per the order below |
45
46
 
@@ -54,10 +54,11 @@ transcript, agent, insights, and settings.
54
54
 
55
55
  ## Progressive disclosure
56
56
 
57
- The viewer presents jobs, not inventories. Sharing starts with one header-level
58
- copy-link action, invitations, and current access. Social destinations and
59
- embed publishing replace the body as focused secondary views; embed
60
- configuration and agent context links stay collapsed until requested. The
57
+ The viewer presents jobs, not inventories. Human sharing and agent continuity
58
+ are separate jobs: Share owns invitations, durable access policy, password,
59
+ expiry, social destinations, and embed publishing; a quiet adjacent Send to
60
+ agent action owns ephemeral handoff to an agent destination. Secondary sharing
61
+ destinations replace the Share body as focused views. The
61
62
  overflow leads with recording cleanup actions, while maintenance and
62
63
  document-generation commands live in named submenus. A generic AI-tools
63
64
  launcher does not compete with Share. The editor opens in transcript mode and
@@ -65,9 +66,11 @@ reveals the precision timeline as a peer mode instead of stacking both
65
66
  workspaces under the player.
66
67
 
67
68
  Share is the viewer toolbar's sole labeled primary action and the product-led
68
- growth entry point. Copy link is the first action inside Share, never a competing
69
- toolbar button. Edit, download, and overflow use equal compact icon controls
70
- with accessible names and tooltips; their visual weight must not rival Share.
69
+ growth entry point. Send to agent is an accessible secondary icon action that
70
+ opens one compact handoff menu; it must not become a second sharing/settings dialog. Copy
71
+ link stays inside Share, never as a competing toolbar button. Edit, download,
72
+ and overflow use equal compact icon controls with accessible names and tooltips;
73
+ their visual weight must not rival Share.
71
74
 
72
75
  Viewer identity uses one avatar grammar everywhere. People use profile images or
73
76
  initials; agents use the same circular avatar shape with the assistant mark.
@@ -16,10 +16,24 @@ import {
16
16
  } from "@agent-native/core/org";
17
17
  import { isEmailDerivedName } from "@agent-native/core/user-profile";
18
18
  import { getUserProfiles } from "@agent-native/core/user-profile/server";
19
- import { and, asc, desc, eq, isNotNull, or } from "drizzle-orm";
19
+ import {
20
+ and,
21
+ asc,
22
+ desc,
23
+ eq,
24
+ isNotNull,
25
+ isNull,
26
+ notInArray,
27
+ or,
28
+ sql,
29
+ } from "drizzle-orm";
20
30
  import { z } from "zod";
21
31
 
22
32
  import { getDb, schema } from "../server/db/index.js";
33
+ import {
34
+ agentRecordingAccessFilter,
35
+ isAgentRecordingCaller,
36
+ } from "../server/lib/agent-recording-access.js";
23
37
  import {
24
38
  getCurrentOwnerEmail,
25
39
  ownerEmailMatches,
@@ -38,7 +52,7 @@ export default defineAction({
38
52
  ),
39
53
  }),
40
54
  http: { method: "GET" },
41
- run: async (args) => {
55
+ run: async (args, ctx) => {
42
56
  const db = getDb();
43
57
  const ownerEmail = getCurrentOwnerEmail();
44
58
 
@@ -127,7 +141,12 @@ export default defineAction({
127
141
  createdAt: Number(i.createdAt),
128
142
  }));
129
143
 
130
- const [spaces, folders] = await Promise.all([
144
+ const resolvedDb = await Promise.resolve(db);
145
+ const meetingRecordingIds = resolvedDb
146
+ .select({ id: schema.meetings.recordingId })
147
+ .from(schema.meetings)
148
+ .where(isNotNull(schema.meetings.recordingId));
149
+ const [spaces, folders, folderRecordingCountRows] = await Promise.all([
131
150
  db
132
151
  .select()
133
152
  .from(schema.spaces)
@@ -146,7 +165,39 @@ export default defineAction({
146
165
  ),
147
166
  )
148
167
  .orderBy(asc(schema.folders.position)),
168
+ resolvedDb
169
+ .select({
170
+ folderId: schema.recordings.folderId,
171
+ recordingCount: sql<number>`COUNT(1)`,
172
+ })
173
+ .from(schema.recordings)
174
+ .where(
175
+ and(
176
+ agentRecordingAccessFilter(
177
+ schema.recordings,
178
+ schema.recordingShares,
179
+ schema.recordingViewers,
180
+ {
181
+ agentOnly: isAgentRecordingCaller(ctx?.caller),
182
+ userEmail: ctx?.userEmail,
183
+ },
184
+ ),
185
+ eq(schema.recordings.organizationId, organizationId),
186
+ isNotNull(schema.recordings.folderId),
187
+ isNull(schema.recordings.archivedAt),
188
+ isNull(schema.recordings.trashedAt),
189
+ notInArray(schema.recordings.id, meetingRecordingIds),
190
+ ),
191
+ )
192
+ .groupBy(schema.recordings.folderId),
149
193
  ]);
194
+ const recordingCountByFolder = new Map(
195
+ folderRecordingCountRows.flatMap((row) =>
196
+ row.folderId
197
+ ? [[row.folderId, Number(row.recordingCount ?? 0)] as const]
198
+ : [],
199
+ ),
200
+ );
150
201
 
151
202
  return {
152
203
  currentUserEmail: ownerEmail,
@@ -173,6 +224,7 @@ export default defineAction({
173
224
  spaceId: f.spaceId,
174
225
  ownerEmail: f.ownerEmail,
175
226
  position: f.position,
227
+ recordingCount: recordingCountByFolder.get(f.id) ?? 0,
176
228
  })),
177
229
  personalFolders: folders
178
230
  .filter((f) => f.spaceId === null)
@@ -180,6 +232,7 @@ export default defineAction({
180
232
  id: f.id,
181
233
  name: f.name,
182
234
  parentId: f.parentId,
235
+ recordingCount: recordingCountByFolder.get(f.id) ?? 0,
183
236
  })),
184
237
  invitations,
185
238
  };
@@ -228,6 +228,8 @@ export default defineAction({
228
228
  if (args.view === "library" || args.view === "space") {
229
229
  if (args.folderId !== undefined && args.folderId !== null) {
230
230
  whereClauses.push(eq(schema.recordings.folderId, args.folderId));
231
+ } else {
232
+ whereClauses.push(isNull(schema.recordings.folderId));
231
233
  }
232
234
  }
233
235
 
@@ -0,0 +1,81 @@
1
+ /** Search dictation history by its native or cleaned transcript text. */
2
+
3
+ import { defineAction } from "@agent-native/core/action";
4
+ import { buildDeepLink } from "@agent-native/core/server";
5
+ import { accessFilter } from "@agent-native/core/sharing";
6
+ import { and, desc, sql } from "drizzle-orm";
7
+ import { z } from "zod";
8
+
9
+ import { getDb, schema } from "../server/db/index.js";
10
+ import { buildCaseInsensitiveSearchPattern } from "./search-recordings-utils.js";
11
+
12
+ const SNIPPET_RADIUS = 80;
13
+
14
+ function buildSnippet(text: string, query: string): string | null {
15
+ const index = text.toLowerCase().indexOf(query.toLowerCase());
16
+ if (index === -1) return null;
17
+ const start = Math.max(0, index - SNIPPET_RADIUS);
18
+ const end = Math.min(text.length, index + query.length + SNIPPET_RADIUS);
19
+ return `${start > 0 ? "…" : ""}${text.slice(start, end).replace(/\s+/g, " ").trim()}${end < text.length ? "…" : ""}`;
20
+ }
21
+
22
+ export default defineAction({
23
+ description:
24
+ "Search dictations by their native or cleaned transcript text. Results are scoped to dictations the current user can access and include a short matching snippet.",
25
+ schema: z.object({
26
+ query: z.string().min(1).describe("Search text"),
27
+ limit: z.coerce.number().int().min(1).max(100).default(30),
28
+ }),
29
+ http: { method: "GET" },
30
+ run: async (args) => {
31
+ const db = getDb();
32
+ const pattern = buildCaseInsensitiveSearchPattern(args.query);
33
+ const rows = await db
34
+ .select({
35
+ id: schema.dictations.id,
36
+ fullText: schema.dictations.fullText,
37
+ cleanedText: schema.dictations.cleanedText,
38
+ durationMs: schema.dictations.durationMs,
39
+ source: schema.dictations.source,
40
+ targetApp: schema.dictations.targetApp,
41
+ startedAt: schema.dictations.startedAt,
42
+ createdAt: schema.dictations.createdAt,
43
+ })
44
+ .from(schema.dictations)
45
+ .where(
46
+ and(
47
+ accessFilter(schema.dictations, schema.dictationShares),
48
+ sql`(lower(${schema.dictations.fullText}) LIKE ${pattern} ESCAPE '\\' OR lower(coalesce(${schema.dictations.cleanedText}, '')) LIKE ${pattern} ESCAPE '\\')`,
49
+ ),
50
+ )
51
+ .orderBy(desc(schema.dictations.startedAt))
52
+ .limit(args.limit);
53
+
54
+ return {
55
+ query: args.query,
56
+ dictations: rows.map((dictation) => ({
57
+ ...dictation,
58
+ snippet:
59
+ buildSnippet(dictation.cleanedText ?? "", args.query) ??
60
+ buildSnippet(dictation.fullText, args.query),
61
+ })),
62
+ };
63
+ },
64
+ link: ({ result }) => {
65
+ if (!result || typeof result !== "object") return null;
66
+ const dictations = (result as { dictations?: unknown }).dictations;
67
+ if (!Array.isArray(dictations) || dictations.length === 0) return null;
68
+ const first = dictations[0] as { id?: string };
69
+ if (!first.id) return null;
70
+ return {
71
+ url: buildDeepLink({
72
+ app: "clips",
73
+ view: "dictate",
74
+ params: { dictationId: first.id },
75
+ to: "/dictate",
76
+ }),
77
+ label: "Open Dictate in Clips",
78
+ view: "dictate",
79
+ };
80
+ },
81
+ });
@@ -0,0 +1,50 @@
1
+ import { IconBrandOpenai } from "@tabler/icons-react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ interface AgentDestinationLogoProps {
6
+ className?: string;
7
+ }
8
+
9
+ export function ClaudeLogo({ className }: AgentDestinationLogoProps) {
10
+ return (
11
+ <svg
12
+ aria-hidden="true"
13
+ className={cn("shrink-0", className)}
14
+ fill="currentColor"
15
+ viewBox="0 0 100 100"
16
+ xmlns="http://www.w3.org/2000/svg"
17
+ >
18
+ <path d="m19.6 66.5 19.7-11 .3-1-.3-.5h-1l-3.3-.2-11.2-.3L14 53l-9.5-.5-2.4-.5L0 49l.2-1.5 2-1.3 2.9.2 6.3.5 9.5.6 6.9.4L38 49.1h1.6l.2-.7-.5-.4-.4-.4L29 41l-10.6-7-5.6-4.1-3-2-1.5-2-.6-4.2 2.7-3 3.7.3.9.2 3.7 2.9 8 6.1L37 36l1.5 1.2.6-.4.1-.3-.7-1.1L33 25l-6-10.4-2.7-4.3-.7-2.6c-.3-1-.4-2-.4-3l3-4.2L28 0l4.2.6L33.8 2l2.6 6 4.1 9.3L47 29.9l2 3.8 1 3.4.3 1h.7v-.5l.5-7.2 1-8.7 1-11.2.3-3.2 1.6-3.8 3-2L61 2.6l2 2.9-.3 1.8-1.1 7.7L59 27.1l-1.5 8.2h.9l1-1.1 4.1-5.4 6.9-8.6 3-3.5L77 13l2.3-1.8h4.3l3.1 4.7-1.4 4.9-4.4 5.6-3.7 4.7-5.3 7.1-3.2 5.7.3.4h.7l12-2.6 6.4-1.1 7.6-1.3 3.5 1.6.4 1.6-1.4 3.4-8.2 2-9.6 2-14.3 3.3-.2.1.2.3 6.4.6 2.8.2h6.8l12.6 1 3.3 2 1.9 2.7-.3 2-5.1 2.6-6.8-1.6-16-3.8-5.4-1.3h-.8v.4l4.6 4.5 8.3 7.5L89 80.1l.5 2.4-1.3 2-1.4-.2-9.2-7-3.6-3-8-6.8h-.5v.7l1.8 2.7 9.8 14.7.5 4.5-.7 1.4-2.6 1-2.7-.6-5.8-8-6-9-4.7-8.2-.5.4-2.9 30.2-1.3 1.5-3 1.2-2.5-2-1.4-3 1.4-6.2 1.6-8 1.3-6.4 1.2-7.9.7-2.6v-.2H49L43 72l-9 12.3-7.2 7.6-1.7.7-3-1.5.3-2.8L24 86l10-12.8 6-7.9 4-4.6-.1-.5h-.3L17.2 77.4l-4.7.6-2-2 .2-3 1-1 8-5.5Z" />
19
+ </svg>
20
+ );
21
+ }
22
+
23
+ export function ClaudeCodeLogo({ className }: AgentDestinationLogoProps) {
24
+ return (
25
+ <svg
26
+ aria-hidden="true"
27
+ className={cn("shrink-0", className)}
28
+ fill="none"
29
+ viewBox="0 0 20 20"
30
+ xmlns="http://www.w3.org/2000/svg"
31
+ >
32
+ <g
33
+ stroke="currentColor"
34
+ strokeLinecap="round"
35
+ strokeLinejoin="round"
36
+ strokeWidth="1.2"
37
+ >
38
+ <path d="m13.75 13.5 3.5-3.5-3.5-3.5" />
39
+ <path d="M9 16 11 4" />
40
+ <path d="m6.25 13.5-3.5-3.5 3.5-3.5" />
41
+ </g>
42
+ </svg>
43
+ );
44
+ }
45
+
46
+ export function CodexLogo({ className }: AgentDestinationLogoProps) {
47
+ return (
48
+ <IconBrandOpenai aria-hidden="true" className={cn("shrink-0", className)} />
49
+ );
50
+ }
@@ -0,0 +1,60 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
2
+ import type { BugReportContext } from "@shared/bug-report";
3
+ import { useEffect, useState } from "react";
4
+
5
+ import { BugReportForm } from "@/components/bug-report/bug-report-form";
6
+ import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
7
+ import { OPEN_BUG_REPORT_EVENT } from "@/lib/command-events";
8
+
9
+ function currentBugReportContext(): BugReportContext {
10
+ return {
11
+ projectId: null,
12
+ title: null,
13
+ description: null,
14
+ severity: "normal",
15
+ sourceUrl: window.location.href,
16
+ pageTitle: document.title || null,
17
+ appVersion: null,
18
+ environment: null,
19
+ reporterEmail: null,
20
+ reporterName: null,
21
+ reporterId: null,
22
+ metadata: null,
23
+ returnUrl: window.location.href,
24
+ };
25
+ }
26
+
27
+ export function BugReportDialog() {
28
+ const t = useT();
29
+ const [open, setOpen] = useState(false);
30
+ const [initialContext, setInitialContext] = useState<BugReportContext | null>(
31
+ null,
32
+ );
33
+
34
+ useEffect(() => {
35
+ const handleOpen = () => {
36
+ setInitialContext(currentBugReportContext());
37
+ setOpen(true);
38
+ };
39
+ window.addEventListener(OPEN_BUG_REPORT_EVENT, handleOpen);
40
+ return () => window.removeEventListener(OPEN_BUG_REPORT_EVENT, handleOpen);
41
+ }, []);
42
+
43
+ return (
44
+ <Dialog open={open} onOpenChange={setOpen}>
45
+ <DialogContent className="w-[calc(100%-2rem)] max-w-2xl gap-0 p-0">
46
+ <DialogTitle className="sr-only">
47
+ {t("bugReportRoute.title")}
48
+ </DialogTitle>
49
+ {open ? (
50
+ <div className="max-h-[min(760px,calc(100vh-2rem))] overflow-y-auto p-5 sm:p-6">
51
+ <BugReportForm
52
+ initialContext={initialContext}
53
+ onRecordingStarted={() => setOpen(false)}
54
+ />
55
+ </div>
56
+ ) : null}
57
+ </DialogContent>
58
+ </Dialog>
59
+ );
60
+ }
@@ -0,0 +1,206 @@
1
+ import { appBasePath } from "@agent-native/core/client/api-path";
2
+ import { useT } from "@agent-native/core/client/i18n";
3
+ import {
4
+ bugReportContextToSearchParams,
5
+ type BugReportContext,
6
+ type BugReportSeverity,
7
+ } from "@shared/bug-report";
8
+ import { IconBug, IconShieldCheck } from "@tabler/icons-react";
9
+ import { useEffect, useState } from "react";
10
+
11
+ import { Button } from "@/components/ui/button";
12
+ import { Input } from "@/components/ui/input";
13
+ import { Label } from "@/components/ui/label";
14
+ import {
15
+ Select,
16
+ SelectContent,
17
+ SelectItem,
18
+ SelectTrigger,
19
+ SelectValue,
20
+ } from "@/components/ui/select";
21
+ import { Textarea } from "@/components/ui/textarea";
22
+ import { cn } from "@/lib/utils";
23
+
24
+ interface BugReportFormProps {
25
+ className?: string;
26
+ initialContext?: BugReportContext | null;
27
+ onRecorderOpened?: (recorderWindow: Window | null) => void;
28
+ onRecordingStarted?: () => void;
29
+ }
30
+
31
+ function openRecorder(url: string): Window | null {
32
+ const opened = window.open(
33
+ url,
34
+ "agent-native-clips-bug-report",
35
+ "popup,width=1120,height=820",
36
+ );
37
+ if (!opened) {
38
+ window.location.href = url;
39
+ return null;
40
+ }
41
+ opened.focus();
42
+ return opened;
43
+ }
44
+
45
+ export function BugReportForm({
46
+ className,
47
+ initialContext,
48
+ onRecorderOpened,
49
+ onRecordingStarted,
50
+ }: BugReportFormProps) {
51
+ const t = useT();
52
+ const [title, setTitle] = useState(initialContext?.title ?? "");
53
+ const [description, setDescription] = useState(
54
+ initialContext?.description ?? "",
55
+ );
56
+ const [reporterEmail, setReporterEmail] = useState(
57
+ initialContext?.reporterEmail ?? "",
58
+ );
59
+ const [severity, setSeverity] = useState<BugReportSeverity>(
60
+ initialContext?.severity ?? "normal",
61
+ );
62
+ const [referrer, setReferrer] = useState<string | null>(null);
63
+
64
+ useEffect(() => {
65
+ setReferrer(document.referrer || null);
66
+ }, []);
67
+
68
+ const sourceUrl = initialContext?.sourceUrl ?? referrer;
69
+ const pageTitle = initialContext?.pageTitle ?? null;
70
+ const sourceLabel =
71
+ pageTitle || sourceUrl || t("bugReportRoute.sourceUnknown");
72
+
73
+ const startRecording = () => {
74
+ const context: BugReportContext = {
75
+ projectId: initialContext?.projectId ?? null,
76
+ title: title.trim() || initialContext?.title || null,
77
+ description: description.trim() || initialContext?.description || null,
78
+ severity,
79
+ sourceUrl,
80
+ pageTitle,
81
+ appVersion: initialContext?.appVersion ?? null,
82
+ environment: initialContext?.environment ?? null,
83
+ reporterEmail:
84
+ reporterEmail.trim() || initialContext?.reporterEmail || null,
85
+ reporterName: initialContext?.reporterName ?? null,
86
+ reporterId: initialContext?.reporterId ?? null,
87
+ metadata: initialContext?.metadata ?? null,
88
+ returnUrl: initialContext?.returnUrl ?? sourceUrl,
89
+ };
90
+ const params = bugReportContextToSearchParams(context);
91
+ params.set("intent", "bug-report");
92
+ params.set("mode", "screen");
93
+ params.set("surface", "browser");
94
+ const recorderWindow = openRecorder(
95
+ `${appBasePath()}/record?${params.toString()}`,
96
+ );
97
+ onRecorderOpened?.(recorderWindow);
98
+ onRecordingStarted?.();
99
+ };
100
+
101
+ return (
102
+ <div className={cn("space-y-4", className)}>
103
+ <div className="mb-5 flex items-start gap-3">
104
+ <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-primary text-primary-foreground">
105
+ <IconBug size={20} />
106
+ </div>
107
+ <div className="min-w-0">
108
+ <h2 className="text-xl font-semibold leading-tight">
109
+ {t("bugReportRoute.title")}
110
+ </h2>
111
+ <p className="mt-2 text-sm leading-6 text-muted-foreground">
112
+ {t("bugReportRoute.description")}
113
+ </p>
114
+ </div>
115
+ </div>
116
+
117
+ <div className="space-y-4">
118
+ <div className="space-y-2">
119
+ <Label htmlFor="bug-title">
120
+ {t("bugReportRoute.issueTitleLabel")}
121
+ </Label>
122
+ <Input
123
+ id="bug-title"
124
+ value={title}
125
+ onChange={(event) => setTitle(event.target.value)}
126
+ placeholder={t("bugReportRoute.issueTitlePlaceholder")}
127
+ />
128
+ </div>
129
+
130
+ <div className="space-y-2">
131
+ <Label htmlFor="bug-details">
132
+ {t("bugReportRoute.detailsLabel")}
133
+ </Label>
134
+ <Textarea
135
+ id="bug-details"
136
+ value={description}
137
+ onChange={(event) => setDescription(event.target.value)}
138
+ placeholder={t("bugReportRoute.detailsPlaceholder")}
139
+ className="min-h-24 resize-none"
140
+ />
141
+ </div>
142
+
143
+ <div className="grid gap-4 sm:grid-cols-[1fr_180px]">
144
+ <div className="space-y-2">
145
+ <Label htmlFor="bug-email">{t("bugReportRoute.emailLabel")}</Label>
146
+ <Input
147
+ id="bug-email"
148
+ type="email"
149
+ value={reporterEmail}
150
+ onChange={(event) => setReporterEmail(event.target.value)}
151
+ placeholder={t("bugReportRoute.emailPlaceholder")}
152
+ />
153
+ </div>
154
+ <div className="space-y-2">
155
+ <Label>{t("bugReportRoute.severityLabel")}</Label>
156
+ <Select
157
+ value={severity}
158
+ onValueChange={(value) => setSeverity(value as BugReportSeverity)}
159
+ >
160
+ <SelectTrigger>
161
+ <SelectValue />
162
+ </SelectTrigger>
163
+ <SelectContent>
164
+ <SelectItem value="low">
165
+ {t("bugReportRoute.severityLow")}
166
+ </SelectItem>
167
+ <SelectItem value="normal">
168
+ {t("bugReportRoute.severityNormal")}
169
+ </SelectItem>
170
+ <SelectItem value="high">
171
+ {t("bugReportRoute.severityHigh")}
172
+ </SelectItem>
173
+ <SelectItem value="urgent">
174
+ {t("bugReportRoute.severityUrgent")}
175
+ </SelectItem>
176
+ </SelectContent>
177
+ </Select>
178
+ </div>
179
+ </div>
180
+
181
+ <div className="rounded-md border bg-muted/40 px-3 py-2">
182
+ <p className="text-xs font-medium text-muted-foreground">
183
+ {t("bugReportRoute.sourceLabel")}
184
+ </p>
185
+ <p
186
+ className={cn(
187
+ "mt-1 truncate text-sm",
188
+ !sourceUrl && "text-muted-foreground",
189
+ )}
190
+ >
191
+ {sourceLabel}
192
+ </p>
193
+ </div>
194
+
195
+ <Button className="h-11 w-full" onClick={startRecording}>
196
+ {t("bugReportRoute.startRecording")}
197
+ </Button>
198
+ </div>
199
+
200
+ <div className="mt-5 flex items-start gap-2 rounded-md bg-muted/50 p-3 text-xs leading-5 text-muted-foreground">
201
+ <IconShieldCheck size={16} className="mt-0.5 shrink-0" />
202
+ <p>{t("bugReportRoute.privacyNote")}</p>
203
+ </div>
204
+ </div>
205
+ );
206
+ }