Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/quiet-comments-route.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@agent-native/core": patch
---

Add public-safe review reads and client gating, root-thread agent routing, durable resolution notes, and capability-aware review panels.
18 changes: 16 additions & 2 deletions packages/core/docs/content/toolkit-comments-review.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ status such as draft, review, approved, or needs changes.
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `registerReviewableResource()` | Register a resource type and optional access resolver for review actions. |
| Comment actions | `list-review-comments`, `create-review-comment`, `reply-review-comment`, `resolve-review-thread`, and `delete-review-comment`. |
| Feedback actions | `get-review-feedback` and `consume-review-feedback` for agent-readable review queues. |
| Feedback actions | `get-review-feedback`, `send-review-thread-to-agent`, and `consume-review-feedback` for root-thread agent queues and human handoffs. |
| Review status action | `set-review-status` for draft, in review, approved, and changes requested states. |
| `ReviewThreadPanel` | Drop-in React panel with comment composer, replies, resolve, and delete controls. |
| `ReviewThreadPanel` | Drop-in React panel with capability-gated composer, reply, resolve, delete, and custom thread-action controls. |
| `ReviewStatusBadge` | Controlled status badge for app headers, sidebars, and review panels. |
| Review hooks | `useReviewComments()`, `useCreateReviewComment()`, `useReplyReviewComment()`, `useResolveReviewThread()`, `useDeleteReviewComment()`, and `useSetReviewStatus()`. |
| Mentions helpers | `extractReviewMentions()` and `normalizeReviewMentions()` for `@[Label](mailto:[email protected])` tokens. |
Expand All @@ -42,6 +42,13 @@ specialize anchor shapes, but the common model preserves:
- `parentCommentId` for replies
- optional `mentions`
- optional `anchor` or `resolutionTarget`
- optional `resolutionNote` on a resolved root thread

`list-review-comments` supports anonymous reads for resources whose access
resolver grants public viewer access. Public viewer responses redact ownership,
organization, email, and actor metadata while preserving safe display names and
the caller-specific `canDelete` capability. Its `summary` reports unpaginated
open-root and agent-queue counts even when the returned comment page is bounded.

## Register A Resource {#register-resource}

Expand Down Expand Up @@ -76,6 +83,9 @@ export function DocumentReview({ documentId }: { documentId: string }) {
resourceType="document"
resourceId={documentId}
targetId="intro-section"
canReply
canResolve={false}
canDeleteComment={(comment) => comment.canDelete === true}
/>
);
}
Expand All @@ -93,6 +103,10 @@ actions and thread UI.
- Agents can read open review feedback before changing code or content.
- `consumedAt` and `resolved` are separate. An agent can consume a piece of
feedback without closing the reviewer-visible thread.
- The root comment owns routing. A human-targeted root is absent from the agent
queue; sending it back to the agent clears its previous consumption marker.
- Agents can pass `resolutionNote` to `resolve-review-thread` so verification
evidence remains attached to the resolved root.

## What's next

Expand Down
2 changes: 2 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
"./review/actions/consume-review-feedback": "./dist/review/actions/consume-review-feedback.js",
"./review/actions/get-review-feedback": "./dist/review/actions/get-review-feedback.js",
"./review/actions/set-review-status": "./dist/review/actions/set-review-status.js",
"./review/actions/send-review-thread-to-agent": "./dist/review/actions/send-review-thread-to-agent.js",
"./comments-review/actions/list-review-comments": "./dist/review/actions/list-review-comments.js",
"./comments-review/actions/create-review-comment": "./dist/review/actions/create-review-comment.js",
"./comments-review/actions/reply-review-comment": "./dist/review/actions/reply-review-comment.js",
Expand All @@ -164,6 +165,7 @@
"./comments-review/actions/consume-review-feedback": "./dist/review/actions/consume-review-feedback.js",
"./comments-review/actions/get-review-feedback": "./dist/review/actions/get-review-feedback.js",
"./comments-review/actions/set-review-status": "./dist/review/actions/set-review-status.js",
"./comments-review/actions/send-review-thread-to-agent": "./dist/review/actions/send-review-thread-to-agent.js",
"./sharing": "./dist/sharing/index.js",
"./sharing/actions/share-resource": "./dist/sharing/actions/share-resource.js",
"./sharing/actions/unshare-resource": "./dist/sharing/actions/unshare-resource.js",
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/client/analytics.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,21 @@ describe("browser analytics pageviews", () => {
});
});

it("can skip the authenticated engine-status probe on public routes", async () => {
installBrowser("https://design.agent-native.com/present/public-design");
const { fetchMock } = installFetch();
const { configureTracking } = await freshAnalytics();

configureTracking({ llmConnectionStatus: false });
await tick();

expect(
fetchMock.mock.calls.some(([url]) =>
String(url).includes("/_agent-native/agent-engine/status"),
),
).toBe(false);
});

it("keeps sanitized tracking but disables content capture on local Plan routes", async () => {
const { gtag } = installBrowser(
"https://plan.agent-native.com/local-plans/local#bridge=secret",
Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/client/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ export type ConfigureTrackingOptions = {
contentCapture?: boolean;
/** Resolve content capture synchronously for each browser pathname. */
contentCaptureForPath?: (pathname: string) => boolean;
/**
* Whether tracking may read the authenticated agent-engine status endpoint.
* Disable this on anonymous/public routes to avoid an expected 401 request.
*/
llmConnectionStatus?: boolean;
sessionReplay?: boolean | SessionReplayOptions;
/**
* First-party, Sentry-style error capture. Auto-captures uncaught errors
Expand Down Expand Up @@ -1042,7 +1047,9 @@ export function configureTracking(options: ConfigureTrackingOptions): void {
ensureSentry();
ensureAmplitude();
captureFirstTouchAttribution();
installLlmConnectionRefresh();
if (options.llmConnectionStatus !== false) {
installLlmConnectionRefresh();
}
installTrackingAuthSessionRefresh();
installPageviewTracking();
maybeInstallSessionReplay(
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/client/guided-questions.flow.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,20 @@ describe("useGuidedQuestionFlow scoped reads", () => {
expect(requestedKeys).not.toContain("guided-questions:undefined");
});

it("does not read application state when disabled", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);

const result = await renderFlow({
enabled: false,
stateKey: "guided-questions",
queryKey: ["guided-questions"],
});

expect(result.current().questions).toBeNull();
expect(fetchMock).not.toHaveBeenCalled();
});

it("refetches on a key-specific DB-sync wakeup without fixed polling", async () => {
let hasQuestion = false;
const fetchMock = vi.fn(
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/client/guided-questions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,8 @@ function normalizeBrowserTabId(browserTabId?: string): string | undefined {
}

export interface UseGuidedQuestionFlowOptions {
/** Disable application-state reads for signed-out or otherwise inactive surfaces. */
enabled?: boolean;
stateKey?: string;
/**
* The current browser tab id. Agent actions that write the guided-questions
Expand All @@ -980,6 +982,7 @@ export interface UseGuidedQuestionFlowOptions {
}

export function useGuidedQuestionFlow({
enabled = true,
stateKey = "show-questions",
browserTabId,
queryKey = ["show-questions"],
Expand Down Expand Up @@ -1030,6 +1033,7 @@ export function useGuidedQuestionFlow({

const { data } = useQuery({
queryKey: resolvedQueryKey,
enabled,
queryFn: async () => {
const read = async (key: string) => {
const res = await fetch(endpointFor(key));
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/client/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ export function AgentNativeI18nProvider({
}, []);

useEffect(() => {
if (!persistPreference) return;
void setClientAppState(
"localization",
{
Expand All @@ -424,7 +425,7 @@ export function AgentNativeI18nProvider({
).catch(() => {
// Public/anonymous pages cannot write app-state; localization still works.
});
}, [locale, preference]);
}, [locale, persistPreference, preference]);

const setPreference = useCallback(
async (next: LocalePreference) => {
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,7 @@ export {
type VersionHistoryPanelProps,
} from "./history/index.js";
export {
ReviewCommentComposer,
ReviewStatusBadge,
ReviewThreadPanel,
buildReviewThreads,
Expand All @@ -874,6 +875,7 @@ export {
useResolveReviewThread,
useReviewComments,
useReviewFeedback,
useSendReviewThreadToAgent,
useSetReviewStatus,
type ConsumeReviewFeedbackInput,
type CreateReviewCommentInput,
Expand All @@ -885,9 +887,11 @@ export {
type ReplyReviewCommentInput,
type ResolveReviewThreadInput,
type ReviewStatusBadgeProps,
type ReviewCommentComposerProps,
type ReviewThread,
type ReviewThreadPanelProps,
type SetReviewStatusInput,
type SendReviewThreadToAgentInput,
} from "./review/index.js";
export type {
AppToFrameMessage,
Expand Down
108 changes: 108 additions & 0 deletions packages/core/src/client/review/ReviewCommentComposer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Button } from "@agent-native/toolkit/ui/button";
import { Spinner } from "@agent-native/toolkit/ui/spinner";
import { Textarea } from "@agent-native/toolkit/ui/textarea";
import { IconMessageCircle, IconSend } from "@tabler/icons-react";

import type { ReviewResolutionTarget } from "../../review/types.js";
import { cn } from "../utils.js";

export interface ReviewCommentComposerProps {
value: string;
onChange: (value: string) => void;
onSubmit: (resolutionTarget: ReviewResolutionTarget) => void;
submittingTarget?: ReviewResolutionTarget | null;
disabled?: boolean;
showAgentAction?: boolean;
placeholder?: string;
commentLabel?: string;
agentLabel?: string;
autoFocus?: boolean;
submitOnEnter?: boolean;
onEscape?: () => void;
className?: string;
}

export function ReviewCommentComposer({
value,
onChange,
onSubmit,
submittingTarget = null,
disabled = false,
showAgentAction = false,
placeholder = "Add a comment...",
commentLabel = "Comment",
agentLabel = "Send to agent",
autoFocus = false,
submitOnEnter = false,
onEscape,
className,
}: ReviewCommentComposerProps) {
const canSubmit = Boolean(value.trim()) && !disabled;
const submit = (resolutionTarget: ReviewResolutionTarget) => {
if (!canSubmit) return;
onSubmit(resolutionTarget);
};

return (
<form
className={cn("@container/review", className)}
onSubmit={(event) => {
event.preventDefault();
submit("human");
}}
>
<Textarea
autoFocus={autoFocus}
value={value}
disabled={disabled}
onChange={(event) => onChange(event.currentTarget.value)}
placeholder={placeholder}
className="min-h-16 resize-none text-sm"
onKeyDown={(event) => {
if (event.key === "Escape" && onEscape) {
event.stopPropagation();
event.preventDefault();
onEscape();
return;
}
if (submitOnEnter && event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
submit("human");
}
}}
/>
<div className="mt-2 flex flex-col items-stretch justify-end gap-2 @2xs/review:flex-row @2xs/review:items-center">
<Button
type="submit"
size="sm"
disabled={!canSubmit}
className="h-8 w-full min-w-0 gap-1.5 @2xs/review:w-auto"
>
{submittingTarget === "human" ? (
<Spinner className="size-3.5" />
) : (
<IconMessageCircle className="size-3.5" />
)}
<span className="truncate">{commentLabel}</span>
</Button>
{showAgentAction ? (
<Button
type="button"
size="sm"
variant="outline"
disabled={!canSubmit}
className="h-8 w-full min-w-0 gap-1.5 @2xs/review:w-auto"
onClick={() => submit("agent")}
>
{submittingTarget === "agent" ? (
<Spinner className="size-3.5" />
) : (
<IconSend className="size-3.5" />
)}
<span className="truncate">{agentLabel}</span>
</Button>
) : null}
</div>
</form>
);
}
Loading
Loading