Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/cli/src/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export const logger = {
console.log("");
},
};

1 change: 1 addition & 0 deletions packages/provider-amp/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,4 @@
- feat: add Amp SDK provider with undo and follow-up support
- Updated dependencies
- react-grab@0.0.81

1 change: 1 addition & 0 deletions packages/provider-amp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ import "@react-grab/amp/client";
- **Undo**: Undo the last change made by Amp
- **Streaming**: Real-time status updates during execution
- **Tool calls**: See tool usage as it happens

1 change: 1 addition & 0 deletions packages/provider-amp/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
},
"include": ["src/**/*"]
}

Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ export const validateCode = (code: string): ValidationResult => {

return { isValid: true, sanitizedCode };
};

8 changes: 5 additions & 3 deletions packages/provider-visual-edit/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,10 @@ export const createVisualEditAgentProvider = (
return { requestId };
};

const onStart = (session: AgentSession, element: Element | undefined) => {
const onStart = (session: AgentSession, elements: Element[]) => {
const requestId = (session.context.options as RequestContext | undefined)
?.requestId;
const element = elements[0];
if (!requestId || !element) return;

const html = buildAncestorContext(element);
Expand Down Expand Up @@ -445,7 +446,7 @@ export const createVisualEditAgentProvider = (

const onComplete = async (
session: AgentSession,
element: Element | undefined,
elements: Element[],
): Promise<AgentCompleteResult | void> => {
const requestId = (session.context.options as RequestContext | undefined)
?.requestId;
Expand All @@ -455,6 +456,7 @@ export const createVisualEditAgentProvider = (
if (!rawCode) return;
const code = rawCode.trim();

const element = elements[0];
if (!element) {
cleanup(requestId);
return { error: "Failed to edit: element not found" };
Expand Down Expand Up @@ -515,7 +517,7 @@ export const createVisualEditAgentProvider = (
cleanup(requestId);
};

const onUndo = () => {
const onUndo = (_session: AgentSession, _elements: Element[]) => {
// HACK: Undo logic is handled by provider.undo, this callback is for session restoration in core.tsx
};

Expand Down
65 changes: 40 additions & 25 deletions packages/react-grab/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { getNearestComponentName } from "./context.js";
import { RECENT_THRESHOLD_MS } from "./constants.js";

interface StartSessionParams {
element: Element;
elements: Element[];
prompt: string;
position: { x: number; y: number };
selectionBounds?: OverlayBounds;
Expand All @@ -35,6 +35,7 @@ interface SessionOperations {
retry: (sessionId: string) => void;
undo: (sessionId: string) => void;
getElement: (sessionId: string) => Element | undefined;
getElements: (sessionId: string) => Element[];
tryResume: () => void;
acknowledgeError: (sessionId: string) => string | undefined;
}
Expand Down Expand Up @@ -69,7 +70,7 @@ export const createAgentManager = (
const [canUndo, setCanUndo] = createSignal(false);
const [canRedo, setCanRedo] = createSignal(false);
const abortControllers = new Map<string, AbortController>();
const sessionElements = new Map<string, Element>();
const sessionElements = new Map<string, Element[]>();

let agentOptions = initialAgentOptions;

Expand Down Expand Up @@ -128,10 +129,10 @@ export const createAgentManager = (
storage,
);
setSessions((prev) => new Map(prev).set(session.id, completedSession));
const element = sessionElements.get(session.id);
const elements = sessionElements.get(session.id) ?? [];
const result = await agentOptions?.onComplete?.(
completedSession,
element,
elements,
);
updateUndoRedoState();
if (result?.error) {
Expand All @@ -149,8 +150,8 @@ export const createAgentManager = (
if (error instanceof Error && error.name === "AbortError") {
wasAborted = true;
if (currentSession) {
const element = sessionElements.get(session.id);
agentOptions?.onAbort?.(currentSession, element);
const elements = sessionElements.get(session.id) ?? [];
agentOptions?.onAbort?.(currentSession, elements);
}
} else {
const errorMessage =
Expand Down Expand Up @@ -196,7 +197,8 @@ export const createAgentManager = (
const element = document.elementFromPoint(centerX, centerY);
if (!element) return undefined;

if (tagName && element.tagName.toLowerCase() !== tagName) {
const isValidHtmlTagName = tagName && !tagName.includes(" ");
if (isValidHtmlTagName && element.tagName.toLowerCase() !== tagName) {
return undefined;
}

Expand Down Expand Up @@ -247,7 +249,7 @@ export const createAgentManager = (
for (const existingSession of resumableSessions) {
const reacquiredElement = tryReacquireElement(existingSession);
if (reacquiredElement) {
sessionElements.set(existingSession.id, reacquiredElement);
sessionElements.set(existingSession.id, [reacquiredElement]);
}

const sessionWithResumeStatus = {
Expand Down Expand Up @@ -278,19 +280,20 @@ export const createAgentManager = (
};

const startSession = async (params: StartSessionParams) => {
const { element, prompt, position, selectionBounds, sessionId } = params;
const { elements, prompt, position, selectionBounds, sessionId } = params;
const storage = agentOptions?.storage;

if (!agentOptions?.provider) {
if (!agentOptions?.provider || elements.length === 0) {
return;
}

const firstElement = elements[0];
const existingSession = sessionId ? sessions().get(sessionId) : undefined;
const isFollowUp = Boolean(sessionId);

const content = existingSession
? existingSession.context.content
: await generateSnippet([element], { maxLines: Infinity });
: await generateSnippet(elements, { maxLines: Infinity });

const context: AgentContext = {
content,
Expand All @@ -311,9 +314,14 @@ export const createAgentManager = (
storage,
);
} else {
const tagName = (element.tagName || "").toLowerCase() || undefined;
const tagName =
elements.length > 1
? `${elements.length} elements`
: (firstElement.tagName || "").toLowerCase() || undefined;
const componentName =
(await getNearestComponentName(element)) || undefined;
elements.length > 1
? undefined
: (await getNearestComponentName(firstElement)) || undefined;

session = createSession(
context,
Expand All @@ -323,12 +331,12 @@ export const createAgentManager = (
componentName,
);
session.lastStatus = "Thinking…";
sessionElements.set(session.id, element);
}

sessionElements.set(session.id, elements);
setSessions((prev) => new Map(prev).set(session.id, session));
saveSessionById(session, storage);
agentOptions.onStart?.(session, element);
agentOptions.onStart?.(session, elements);

const abortController = new AbortController();
abortControllers.set(session.id, abortController);
Expand Down Expand Up @@ -362,9 +370,9 @@ export const createAgentManager = (
const dismissSession = (sessionId: string) => {
const currentSessions = sessions();
const session = currentSessions.get(sessionId);
const element = sessionElements.get(sessionId);
if (session && element) {
agentOptions?.onDismiss?.(session, element);
const elements = sessionElements.get(sessionId) ?? [];
if (session && elements.length > 0) {
agentOptions?.onDismiss?.(session, elements);
}
const storage = agentOptions?.storage;
sessionElements.delete(sessionId);
Expand All @@ -380,8 +388,8 @@ export const createAgentManager = (
const currentSessions = sessions();
const session = currentSessions.get(sessionId);
if (session) {
const element = sessionElements.get(sessionId);
agentOptions?.onUndo?.(session, element);
const elements = sessionElements.get(sessionId) ?? [];
agentOptions?.onUndo?.(session, elements);
void agentOptions?.provider?.undo?.();
}
dismissSession(sessionId);
Expand Down Expand Up @@ -412,7 +420,7 @@ export const createAgentManager = (
if (!session || !agentOptions?.provider) return;

const storage = agentOptions.storage;
const element = sessionElements.get(sessionId);
const elements = sessionElements.get(sessionId) ?? [];

const retriedSession = updateSession(
session,
Expand All @@ -427,8 +435,8 @@ export const createAgentManager = (
setSessions((prev) => new Map(prev).set(sessionId, retriedSession));
saveSessionById(retriedSession, storage);

if (element) {
agentOptions.onStart?.(retriedSession, element);
if (elements.length > 0) {
agentOptions.onStart?.(retriedSession, elements);
}

const abortController = new AbortController();
Expand All @@ -454,7 +462,8 @@ export const createAgentManager = (
let didUpdate = false;

for (const [sessionId, session] of currentSessions) {
const element = sessionElements.get(sessionId);
const elements = sessionElements.get(sessionId) ?? [];
const element = elements[0];

if (element && document.contains(element)) {
const newBounds = createElementBounds(element);
Expand Down Expand Up @@ -485,7 +494,12 @@ export const createAgentManager = (
};

const getSessionElement = (sessionId: string): Element | undefined => {
return sessionElements.get(sessionId);
const elements = sessionElements.get(sessionId);
return elements?.[0];
};

const getSessionElements = (sessionId: string): Element[] => {
return sessionElements.get(sessionId) ?? [];
};

return {
Expand All @@ -500,6 +514,7 @@ export const createAgentManager = (
retry: retrySession,
undo: undoSession,
getElement: getSessionElement,
getElements: getSessionElements,
tryResume: tryResumeSessions,
acknowledgeError: acknowledgeSessionError,
},
Expand Down
1 change: 1 addition & 0 deletions packages/react-grab/src/components/icon-retry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ export const IconRetry: Component<IconRetryProps> = (props) => {
</svg>
);
};

29 changes: 22 additions & 7 deletions packages/react-grab/src/components/renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,29 @@ export const ReactGrabRenderer: Component<ReactGrabRendererProps> = (props) => {
props.agentSessions ? Array.from(props.agentSessions.values()) : [],
);

const selectionBoundsList = createMemo(() => {
if (
props.selectionBoundsMultiple &&
props.selectionBoundsMultiple.length > 0
) {
return props.selectionBoundsMultiple;
}
return props.selectionBounds ? [props.selectionBounds] : [];
});

return (
<>
<Show when={props.selectionVisible && props.selectionBounds}>
<SelectionBox
variant="selection"
bounds={props.selectionBounds!}
visible={props.selectionVisible}
isFading={props.selectionLabelStatus === "fading"}
/>
<Show when={props.selectionVisible}>
<For each={selectionBoundsList()}>
{(bounds) => (
<SelectionBox
variant="selection"
bounds={bounds}
visible={props.selectionVisible}
isFading={props.selectionLabelStatus === "fading"}
/>
)}
</For>
</Show>

<Show
Expand Down Expand Up @@ -118,6 +132,7 @@ export const ReactGrabRenderer: Component<ReactGrabRendererProps> = (props) => {
<SelectionLabel
tagName={props.selectionTagName}
componentName={props.selectionComponentName}
elementsCount={props.selectionElementsCount}
selectionBounds={props.selectionBounds}
mouseX={props.mouseX}
visible={props.selectionLabelVisible}
Expand Down
4 changes: 4 additions & 0 deletions packages/react-grab/src/components/selection-label/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export const SelectionLabel: Component<SelectionLabelProps> = (props) => {
// old cached width, causing the label to be offset incorrectly.
void props.tagName;
void props.componentName;
void props.elementsCount;
void props.statusText;
void props.inputValue;
void props.hasAgent;
Expand Down Expand Up @@ -242,6 +243,9 @@ export const SelectionLabel: Component<SelectionLabelProps> = (props) => {
};

const tagDisplay = () => {
if (props.elementsCount && props.elementsCount > 1) {
return `${props.elementsCount} elements`;
}
if (props.componentName && props.tagName) {
return `${props.componentName}.${props.tagName}`;
}
Expand Down
Loading