Skip to content

[CP] feat(agent): track Agent GUI engagement#1082

Merged
chovy-ai merged 1 commit into
release/0704from
agent/cp-agent-gui-engagement-0704
Jul 14, 2026
Merged

[CP] feat(agent): track Agent GUI engagement#1082
chovy-ai merged 1 commit into
release/0704from
agent/cp-agent-gui-engagement-0704

Conversation

@chovy-ai

@chovy-ai chovy-ai commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Cherry-pick and release-branch adaptation of #1081.

  • add true Agent chat panel exposure tracking after 1 second at 50% visibility
  • track first composer focus with pointer, keyboard, or programmatic modality
  • track first privacy-safe content entry per panel visit
  • wire the analytics contract into the workspace Agent GUI surface
  • document ownership, exposure semantics, deduplication, and privacy constraints

Release branch adaptation

release/0704 does not contain the standalone Agent window present on main, 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:ts
  • pnpm --filter @tutti-os/agent-gui typecheck
  • pnpm --filter @tutti-os/desktop typecheck
  • Agent GUI analytics/editor tests: 62 passed
  • Desktop Agent GUI host tests: 32 passed
  • agent activity runtime boundary check
  • renderer feature boundary check
  • check:changed: 20/22 lanes passed

Known release/0704 baseline failures, outside this diff:

  • TestCodexAppServerAdapterCancelAfterTurnCompletedStillMarksChildrenCanceled in packages/agentactivity/daemon/runtime
  • desktop agent host api preserves frontend session UUIDs as canonical ids model-option assertion in createDesktopAgentHostApi.test.ts

Summary 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.

  • New Features
    • True chat panel exposure when active, document visible+focused, and ≥50% in viewport for 1s; previews excluded.
    • First composer focus captured with method (pointer, keyboard, programmatic).
    • First user-entered content per visit (text, large_text, image); ignores controlled prefill.
    • Events deduped and buffered until exposure; all share a panelVisitId.
    • Implemented optional engagementAnalytics in @tutti-os/agent-gui; desktop uses createAgentGUIEngagementAnalytics("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.

Review in cubic

Signed-off-by: chovy <chovy@chovydeMac-mini.local>
(cherry picked from commit 40ba47c)
@chovy-ai
chovy-ai marked this pull request as ready for review July 14, 2026 06:14

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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;

@cubic-dev-ai cubic-dev-ai Bot Jul 14, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Fix with cubic

agentSessionId: string | null;
agentTargetId: string | null;
composerReady: boolean;
contentType: string;

@cubic-dev-ai cubic-dev-ai Bot Jul 14, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Fix with cubic

return;
}
lastEmittedPromptRef.current = nextPrompt;
onUserContentChangeRef.current?.(nextPrompt);

@cubic-dev-ai cubic-dev-ai Bot Jul 14, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Fix with cubic

}
visit.timeoutId = null;
visit.exposed = true;
const eventContext = currentEventContext(contextRef.current, visit.id);

@cubic-dev-ai cubic-dev-ai Bot Jul 14, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Fix with cubic

| "pointer"
| "programmatic";

export type AgentGUIComposerContentType = "image" | "large_text" | "text";

@cubic-dev-ai cubic-dev-ai Bot Jul 14, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Fix with cubic


const focused = useCallback((focusMethod: AgentGUIComposerFocusMethod) => {
const visit = visitRef.current;
if (!visit || visit.pendingFocusMethod) {

@cubic-dev-ai cubic-dev-ai Bot Jul 14, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Fix with cubic

@chovy-ai
chovy-ai merged commit cd87751 into release/0704 Jul 14, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant