[CP] feat(agent): track Agent GUI engagement#1082
Conversation
Signed-off-by: chovy <chovy@chovydeMac-mini.local> (cherry picked from commit 40ba47c)
There was a problem hiding this comment.
6 issues found across 26 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/agent/gui/agent-gui/agentGuiNode/AgentGUINode.tsx">
<violation number="1" location="packages/agent/gui/agent-gui/agentGuiNode/AgentGUINode.tsx:202">
P2: Analytics can remain disabled or report through a stale surface instance when `engagementAnalytics` changes without another prop change, because the custom `memo` comparator omits this new prop. Include `previous.engagementAnalytics === next.engagementAnalytics` in `areAgentGUINodePropsEqual`.</violation>
</file>
<file name="apps/desktop/src/renderer/src/features/analytics/reporters/agent-chat-input-content-entered/types.ts">
<violation number="1" location="apps/desktop/src/renderer/src/features/analytics/reporters/agent-chat-input-content-entered/types.ts:7">
P2: The exported reporter contract accepts any string as `contentType`, so future callers can accidentally emit raw content or unbounded analytics values despite this event's privacy-safe categorical contract. Narrowing this field to the three supported values preserves that guarantee at the desktop reporting boundary.</violation>
</file>
<file name="packages/agent/gui/agent-gui/agentGuiNode/agentRichText/AgentRichTextEditor.tsx">
<violation number="1" location="packages/agent/gui/agent-gui/agentGuiNode/agentRichText/AgentRichTextEditor.tsx:1043">
P2: Deleting from a prefilled draft is recorded as the visit's first content entry because every nonempty editor update reaches `contentEntered`. Please distinguish transactions that insert content from deletion-only changes before invoking `onUserContentChange`, so a deletion does not consume the per-visit event.</violation>
</file>
<file name="packages/agent/gui/agent-gui/agentGuiNode/agentGuiEngagementAnalytics.ts">
<violation number="1" location="packages/agent/gui/agent-gui/agentGuiNode/agentGuiEngagementAnalytics.ts:17">
P2: Large text is never reported as `large_text`; every rich-text user edit is currently classified as `text`. This leaves the advertised large-text engagement bucket empty, so the composer should classify the relevant paste/size case before calling `contentEntered`.</violation>
<violation number="2" location="packages/agent/gui/agent-gui/agentGuiNode/agentGuiEngagementAnalytics.ts:216">
P2: Buffered focus/content events can be attributed to the wrong session, target, provider, or `conversationState` if context changes during the 1-second dwell. Capturing context with each pending interaction would preserve the same event-time semantics used after exposure.</violation>
<violation number="3" location="packages/agent/gui/agent-gui/agentGuiNode/agentGuiEngagementAnalytics.ts:241">
P2: The first composer focus is lost when a user clicks directly into an inactive Agent window because the focus event can precede the passive effect that creates the visit. Buffering that pre-visit focus until activation/exposure would preserve the required first-focus event and its pointer modality.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| provider: AgentProvider; | ||
| references: readonly WorkspaceFileReference[]; | ||
| }) => void | Promise<void>; | ||
| engagementAnalytics?: AgentGUIEngagementAnalytics; |
There was a problem hiding this comment.
P2: Analytics can remain disabled or report through a stale surface instance when engagementAnalytics changes without another prop change, because the custom memo comparator omits this new prop. Include previous.engagementAnalytics === next.engagementAnalytics in areAgentGUINodePropsEqual.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent/gui/agent-gui/agentGuiNode/AgentGUINode.tsx, line 202:
<comment>Analytics can remain disabled or report through a stale surface instance when `engagementAnalytics` changes without another prop change, because the custom `memo` comparator omits this new prop. Include `previous.engagementAnalytics === next.engagementAnalytics` in `areAgentGUINodePropsEqual`.</comment>
<file context>
@@ -198,6 +199,7 @@ export interface AgentGUINodeProps {
provider: AgentProvider;
references: readonly WorkspaceFileReference[];
}) => void | Promise<void>;
+ engagementAnalytics?: AgentGUIEngagementAnalytics;
onOpenConversationWindow?: (agentSessionId: string) => void;
onClose: () => void;
</file context>
| agentSessionId: string | null; | ||
| agentTargetId: string | null; | ||
| composerReady: boolean; | ||
| contentType: string; |
There was a problem hiding this comment.
P2: The exported reporter contract accepts any string as contentType, so future callers can accidentally emit raw content or unbounded analytics values despite this event's privacy-safe categorical contract. Narrowing this field to the three supported values preserves that guarantee at the desktop reporting boundary.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/src/features/analytics/reporters/agent-chat-input-content-entered/types.ts, line 7:
<comment>The exported reporter contract accepts any string as `contentType`, so future callers can accidentally emit raw content or unbounded analytics values despite this event's privacy-safe categorical contract. Narrowing this field to the three supported values preserves that guarantee at the desktop reporting boundary.</comment>
<file context>
@@ -0,0 +1,13 @@
+ agentSessionId: string | null;
+ agentTargetId: string | null;
+ composerReady: boolean;
+ contentType: string;
+ conversationState: string;
+ hadPrefill: boolean;
</file context>
| return; | ||
| } | ||
| lastEmittedPromptRef.current = nextPrompt; | ||
| onUserContentChangeRef.current?.(nextPrompt); |
There was a problem hiding this comment.
P2: Deleting from a prefilled draft is recorded as the visit's first content entry because every nonempty editor update reaches contentEntered. Please distinguish transactions that insert content from deletion-only changes before invoking onUserContentChange, so a deletion does not consume the per-visit event.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent/gui/agent-gui/agentGuiNode/agentRichText/AgentRichTextEditor.tsx, line 1043:
<comment>Deleting from a prefilled draft is recorded as the visit's first content entry because every nonempty editor update reaches `contentEntered`. Please distinguish transactions that insert content from deletion-only changes before invoking `onUserContentChange`, so a deletion does not consume the per-visit event.</comment>
<file context>
@@ -1029,8 +1040,13 @@ export const AgentRichTextEditor = forwardRef<
return;
}
lastEmittedPromptRef.current = nextPrompt;
+ onUserContentChangeRef.current?.(nextPrompt);
onChangeRef.current(nextPrompt);
},
</file context>
| } | ||
| visit.timeoutId = null; | ||
| visit.exposed = true; | ||
| const eventContext = currentEventContext(contextRef.current, visit.id); |
There was a problem hiding this comment.
P2: Buffered focus/content events can be attributed to the wrong session, target, provider, or conversationState if context changes during the 1-second dwell. Capturing context with each pending interaction would preserve the same event-time semantics used after exposure.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent/gui/agent-gui/agentGuiNode/agentGuiEngagementAnalytics.ts, line 216:
<comment>Buffered focus/content events can be attributed to the wrong session, target, provider, or `conversationState` if context changes during the 1-second dwell. Capturing context with each pending interaction would preserve the same event-time semantics used after exposure.</comment>
<file context>
@@ -0,0 +1,324 @@
+ }
+ visit.timeoutId = null;
+ visit.exposed = true;
+ const eventContext = currentEventContext(contextRef.current, visit.id);
+ reportAnalyticsEvent(
+ analyticsRef.current?.onChatPanelExposed,
</file context>
| | "pointer" | ||
| | "programmatic"; | ||
|
|
||
| export type AgentGUIComposerContentType = "image" | "large_text" | "text"; |
There was a problem hiding this comment.
P2: Large text is never reported as large_text; every rich-text user edit is currently classified as text. This leaves the advertised large-text engagement bucket empty, so the composer should classify the relevant paste/size case before calling contentEntered.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent/gui/agent-gui/agentGuiNode/agentGuiEngagementAnalytics.ts, line 17:
<comment>Large text is never reported as `large_text`; every rich-text user edit is currently classified as `text`. This leaves the advertised large-text engagement bucket empty, so the composer should classify the relevant paste/size case before calling `contentEntered`.</comment>
<file context>
@@ -0,0 +1,324 @@
+ | "pointer"
+ | "programmatic";
+
+export type AgentGUIComposerContentType = "image" | "large_text" | "text";
+
+export interface AgentGUIEngagementContext {
</file context>
|
|
||
| const focused = useCallback((focusMethod: AgentGUIComposerFocusMethod) => { | ||
| const visit = visitRef.current; | ||
| if (!visit || visit.pendingFocusMethod) { |
There was a problem hiding this comment.
P2: The first composer focus is lost when a user clicks directly into an inactive Agent window because the focus event can precede the passive effect that creates the visit. Buffering that pre-visit focus until activation/exposure would preserve the required first-focus event and its pointer modality.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent/gui/agent-gui/agentGuiNode/agentGuiEngagementAnalytics.ts, line 241:
<comment>The first composer focus is lost when a user clicks directly into an inactive Agent window because the focus event can precede the passive effect that creates the visit. Buffering that pre-visit focus until activation/exposure would preserve the required first-focus event and its pointer modality.</comment>
<file context>
@@ -0,0 +1,324 @@
+
+ const focused = useCallback((focusMethod: AgentGUIComposerFocusMethod) => {
+ const visit = visitRef.current;
+ if (!visit || visit.pendingFocusMethod) {
+ return;
+ }
</file context>
Summary
Cherry-pick and release-branch adaptation of #1081.
Release branch adaptation
release/0704does not contain the standalone Agent window present onmain, so this PR keeps that file deleted and wires the available workspace Agent GUI surface only.Privacy
No prompt text, file names, file paths, mention URIs, or attachment payloads are collected.
Validation
Passed:
pnpm lint:tspnpm --filter @tutti-os/agent-gui typecheckpnpm --filter @tutti-os/desktop typecheckcheck:changed: 20/22 lanes passedKnown
release/0704baseline failures, outside this diff:TestCodexAppServerAdapterCancelAfterTurnCompletedStillMarksChildrenCanceledinpackages/agentactivity/daemon/runtimedesktop agent host api preserves frontend session UUIDs as canonical idsmodel-option assertion increateDesktopAgentHostApi.test.tsSummary by cubic
Adds privacy-safe engagement analytics to the Agent GUI: true chat panel exposure, first composer focus method, and first user-entered content per visit. Wired to the desktop workspace surface for
release/0704.panelVisitId.engagementAnalyticsin@tutti-os/agent-gui; desktop usescreateAgentGUIEngagementAnalytics("workspace")and reports via existing pipeline without prompt text, file names/paths, mention URIs, or attachment payloads.Written for commit fed21c2. Summary will update on new commits.