Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
197713b
chore: publish branch work in .agents/skills, .changeset/core-reusabl…
steve8708 Sep 10, 2026
0d7d907
chore: publish branch work in packages/core, packages/dispatch, packa…
steve8708 Sep 10, 2026
8f33a82
Merge origin/main into cursor/e46d0b20
steve8708 Sep 10, 2026
ae9e8b9
chore: publish branch work in packages/core (2 files)
steve8708 Sep 10, 2026
c12f468
chore: publish branch work in packages/core, packages/toolkit, templa…
steve8708 Sep 10, 2026
b021b00
chore: publish branch work in packages/core, packages/dispatch, packa…
steve8708 Sep 10, 2026
dcb698d
chore: publish branch work in packages/core, packages/dispatch, packa…
steve8708 Sep 10, 2026
505a7e2
chore: publish branch work in packages/core, packages/toolkit, templa…
steve8708 Sep 10, 2026
3dcc246
chore: publish branch work in templates/mail (1 files)
steve8708 Sep 10, 2026
6c0494c
chore: publish branch work in .changeset/core-reusable-sidebar-and-al…
steve8708 Sep 10, 2026
5922538
chore: publish branch work in packages/core, packages/dispatch, packa…
steve8708 Sep 10, 2026
914c93c
Merge remote-tracking branch 'origin/main' into cursor/e46d0b20
steve8708 Sep 10, 2026
802b14f
chore: publish branch work in templates/clips (1 files)
steve8708 Sep 10, 2026
2f9a92f
chore: publish branch work in packages/core, packages/dispatch, templ…
steve8708 Sep 10, 2026
68b7c5f
chore: publish branch work in scripts/qa-standalone-chat-dev-smoke.ts…
steve8708 Sep 10, 2026
bf8dfeb
chore: publish branch work in packages/core, templates/brain (2 files)
steve8708 Sep 10, 2026
9ece78c
Merge remote-tracking branch 'origin/main' into cursor/e46d0b20
steve8708 Sep 10, 2026
7098bcc
chore: publish branch work in packages/core (1 files)
steve8708 Sep 10, 2026
b409517
chore: publish branch work in packages/core (1 files)
steve8708 Sep 10, 2026
59ff32e
chore: publish branch work in packages/core, packages/dispatch, templ…
steve8708 Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/core-reusable-sidebar-and-alpha-badge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@agent-native/core": patch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Release the toolkit alongside the new AppSidebar export

This changeset bumps @agent-native/core, but not @agent-native/toolkit, despite adding the AppSidebar export in packages/toolkit/src/app-shell/index.ts. Core now imports that symbol from @agent-native/toolkit/app-shell; a consumer installing the patched Core with the currently published toolkit can therefore fail at module initialization. Add a patch changeset for @agent-native/toolkit so the export and Core release are published together.

Fix in Builder

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: changeset now includes a patch bump for @agent-native/toolkit alongside core/dispatch so the new AppSidebar export publishes with Core.

"@agent-native/dispatch": patch
"@agent-native/toolkit": patch
---

Add reusable AppSidebar in toolkit and core, support top-left configurable alpha badges, and update app layouts to match the new sidebar design.
52 changes: 52 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,58 @@
"3197"
],
"port": 3197
},
{
"name": "dispatch-vault-verify",
"runtimeExecutable": "pnpm",
"runtimeArgs": [
"exec",
"tsx",
"scripts/claude-launch.ts",
"--name",
"dispatch-vault-verify",
"--dir",
"templates/dispatch",
"--env",
"DATABASE_URL=",
"--env",
"SLACK_BOT_TOKEN=",
"--env",
"SLACK_SIGNING_SECRET=",
"--env",
"TELEGRAM_BOT_TOKEN=",
"--env",
"SENDGRID_API_KEY=",
"--env",
"WORKSPACE_GATEWAY_URL=",
"--env",
"AGENT_NATIVE_ENABLE_RECURRING_JOBS=0",
"--env",
"SENTRY_DSN=",
"--env",
"SENTRY_SERVER_DSN=",
"--env",
"SENTRY_CLIENT_DSN=",
"--env",
"VITE_SENTRY_CLIENT_DSN=",
"--env",
"AGENT_NATIVE_ANALYTICS_ENDPOINT=",
"--env",
"VITE_AGENT_NATIVE_ANALYTICS_ENDPOINT=",
"--env",
"AMPLITUDE_API_KEY=",
"--env",
"VITE_AMPLITUDE_API_KEY=",
"--env",
"VITE_AGENT_NATIVE_SESSION_REPLAY_ENABLED=0",
"--env",
"AGENT_NATIVE_BUILDER_SSO_SECRET=",
"--",
"dev",
"--port",
"3190"
],
"port": 3190
}
]
}
12 changes: 7 additions & 5 deletions packages/core/docs/content/template-clips-anonymous-intake.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ The intake URL is a narrow upload capability. It is not a share link, a library

## Capability boundaries

| Capability | Allows | Does not allow | Lifetime |
| --- | --- | --- | --- |
| Intake URL | Create one private recording, upload its chunks, finalize it, or abort it | List recordings, read media, open the library, or use normal agent APIs | One hour by default. Four hours maximum |
| Agent context link | Read the completed recording through the normal scoped Clips agent surface after an authenticated host-side exchange | Be minted by the anonymous intake bearer, read any other recording, or act as the recording owner | Seven days for bug-report completions |
| Clips session | Mint intake URLs and use the normal authenticated Clips UI | Nothing is added to the anonymous visitor's browser | Controlled by normal Clips auth |
<!-- i18n-copy-ignore: oxfmt table column alignment only; wording unchanged. -->

| Capability | Allows | Does not allow | Lifetime |
| ------------------ | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------- |
| Intake URL | Create one private recording, upload its chunks, finalize it, or abort it | List recordings, read media, open the library, or use normal agent APIs | One hour by default. Four hours maximum |
| Agent context link | Read the completed recording through the normal scoped Clips agent surface after an authenticated host-side exchange | Be minted by the anonymous intake bearer, read any other recording, or act as the recording owner | Seven days for bug-report completions |
| Clips session | Mint intake URLs and use the normal authenticated Clips UI | Nothing is added to the anonymous visitor's browser | Controlled by normal Clips auth |

The intake token is signed with the existing scoped-token mechanism. The token is never stored in SQL. The database stores only the intake id, owner and organization binding, recording id, status, and expiry.

Expand Down
2 changes: 1 addition & 1 deletion packages/core/docs/content/template-clips-developers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ Two things live outside this schema on purpose: organization/member/invitation r
| `recording_ctas` | Call-to-action buttons (label, url, color, placement) overlaid on a recording |
| `recording_comments` | Threaded, timestamped comments with emoji-reaction map and resolved flag |
| `recording_bug_reports` | Redacted host-product bug report metadata keyed by `recording_id` |
| `clips_intake_sessions` | One-use anonymous intake capabilities bound to an owner, organization, and recording |
| `clips_intake_sessions` | One-use anonymous intake capabilities bound to an owner, organization, and recording |
| `recording_reactions` | Emoji reactions pinned to a video timestamp (anonymous viewers allowed) |
| `recording_viewers` / `recording_events` | View analytics: per-viewer watch time and completion, plus granular events (view-start, watch-progress, seek, pause, cta-click, reaction) |
| `recording_views` | Append-only per-view log (who viewed, when) — backs the owner-facing "Viewed by" popover, kept separate from the aggregate counters above |
Expand Down
83 changes: 50 additions & 33 deletions packages/core/src/client/EnvironmentBadge.render.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe("EnvironmentBadge render", () => {
vi.clearAllMocks();
});

it("renders a non-navigating dev pill for configured local development", () => {
it("renders a non-navigating alpha pill for configured local development", () => {
Object.defineProperty(window, "location", {
configurable: true,
value: {
Expand All @@ -77,7 +77,7 @@ describe("EnvironmentBadge render", () => {
act(() => root.render(<EnvironmentBadge />));

const badge = container.querySelector('[role="status"]');
expect(badge?.textContent).toBe("dev");
expect(badge?.textContent).toBe("alpha");
expect(badge?.getAttribute("aria-label")).toBe(
"Local development environment",
);
Expand All @@ -87,7 +87,7 @@ describe("EnvironmentBadge render", () => {
expect(container.querySelector("a")).toBeNull();
});

it("supports an inline dev pill for app-owned brand slots", () => {
it("supports an inline alpha pill for app-owned brand slots", () => {
Object.defineProperty(window, "location", {
configurable: true,
value: {
Expand All @@ -107,14 +107,14 @@ describe("EnvironmentBadge render", () => {
act(() => root.render(<EnvironmentBadge placement="inline" />));

const badge = container.querySelector('[role="status"]');
expect(badge?.textContent).toBe("dev");
expect(badge?.textContent).toBe("alpha");
expect(badge?.className).toContain("inline-flex");
expect(badge?.className).toContain("h-5");
expect(badge?.className).not.toContain("fixed");
expect(badge?.className).not.toContain("bottom-3");
});

it("defers the dev pill to a post-mount effect so the first client commit matches SSR's null output", async () => {
it("defers the alpha pill to a post-mount effect so the first client commit matches SSR's null output", async () => {
Object.defineProperty(window, "location", {
configurable: true,
value: {
Expand All @@ -141,7 +141,9 @@ describe("EnvironmentBadge render", () => {

await act(async () => {});

expect(container.querySelector('[role="status"]')?.textContent).toBe("dev");
expect(container.querySelector('[role="status"]')?.textContent).toBe(
"alpha",
);
});

it.each([
Expand All @@ -162,47 +164,47 @@ describe("EnvironmentBadge render", () => {
expect(container.innerHTML).toBe("");
});

it("renders the beta chip for signed-out visitors", () => {
it("renders the badge for signed-out visitors without interaction", () => {
useSessionMock.mockReturnValue({
session: null,
status: "unauthenticated",
});

act(() => root.render(<EnvironmentBadge />));

const trigger = container.querySelector("button");
expect(trigger?.textContent).toContain("beta");
expect(trigger?.className).toContain("border-primary/80");
expect(trigger?.className).toContain("bottom-3");
expect(trigger?.className).toContain("left-3");
expect(trigger?.className).not.toContain("top-3");
expect(trigger?.className).not.toContain("right-3");
expect(trigger?.className).not.toContain("bg-background/95");
expect(container.textContent).toContain("beta");
const badge = container.querySelector('[role="status"]');
expect(badge?.textContent).toBe("alpha");
expect(badge?.className).toContain("border-primary/80");
expect(badge?.className).toContain("bottom-3");
expect(badge?.className).toContain("left-3");
expect(container.querySelector("button")).toBeNull();
expect(container.textContent).toContain("alpha");
});

it("renders the beta chip for non-builder users", () => {
it("renders the badge for non-builder users without interaction", () => {
useSessionMock.mockReturnValue({
session: { email: "person@example.com" },
status: "authenticated",
});

act(() => root.render(<EnvironmentBadge />));

expect(container.querySelector("button")?.textContent).toContain("beta");
expect(container.textContent).toContain("beta");
expect(container.querySelector("button")).toBeNull();
const badge = container.querySelector('[role="status"]');
expect(badge?.textContent).toBe("alpha");
expect(container.textContent).toContain("alpha");
});

it("opens an inline beta chip below an app-owned brand slot", () => {
it("opens an inline chip popover for @builder.io employees", () => {
useSessionMock.mockReturnValue({
session: null,
status: "unauthenticated",
session: { email: "shawn@builder.io" },
status: "authenticated",
});

act(() => root.render(<EnvironmentBadge placement="inline" />));

const trigger = container.querySelector("button");
expect(trigger?.textContent).toContain("beta");
expect(trigger?.textContent).toBe("alpha");
expect(trigger?.className).toContain("inline-flex");
expect(trigger?.className).not.toContain("fixed");

Expand All @@ -216,10 +218,10 @@ describe("EnvironmentBadge render", () => {
expect(document.body.querySelector('[data-side="bottom"]')).not.toBeNull();
});

it("keeps the beta chip linked to production for every visitor", () => {
it("keeps the chip linked to production for builder visitors", () => {
useSessionMock.mockReturnValue({
session: null,
status: "unauthenticated",
session: { email: "steve@builder.io" },
status: "authenticated",
});

act(() => root.render(<EnvironmentBadge />));
Expand Down Expand Up @@ -251,8 +253,8 @@ describe("EnvironmentBadge render", () => {

it("hides the badge for the current page without persisting the choice", () => {
useSessionMock.mockReturnValue({
session: null,
status: "unauthenticated",
session: { email: "steve@builder.io" },
status: "authenticated",
});

act(() => root.render(<EnvironmentBadge />));
Expand All @@ -278,13 +280,28 @@ describe("EnvironmentBadge render", () => {
act(() => hideButton?.click());

expect(container.innerHTML).toBe("");
expect(document.body.querySelector('[data-side="top"]')).toBeNull();
expect(window.sessionStorage.getItem("agent-native:force-production")).toBe(
null,
);
expect(
window.localStorage?.getItem("agent-native:beta-opt-out-until"),
).toBeNull();
});

it("supports configurable badgeText prop and config", () => {
useSessionMock.mockReturnValue({
session: { email: "steve@builder.io" },
status: "authenticated",
});

act(() => root.render(<EnvironmentBadge badgeText="beta" />));
expect(container.querySelector("button")?.textContent).toBe("beta");

injectedAgentNativeConfigMock.mockReturnValue({
deployment: { badgeText: "custom" },
});
act(() => root.render(<EnvironmentBadge key="custom" />));
expect(container.querySelector("button")?.textContent).toBe("custom");
});

it("hides the production chip for non-employee sessions", () => {
it("renders non-interactive chip on production for non-employee sessions", () => {
Object.defineProperty(window, "location", {
configurable: true,
value: {
Expand Down
Loading
Loading