Add a privacy opt in during the onboarding (vibe-kanban) (#146)

* Perfect! I've successfully implemented the privacy opt-in dialog feature. Here's what I've accomplished:

## Summary

I've added a privacy opt-in dialog to the onboarding flow that:

1. **Appears at the right time**: Shows after the coding agent and IDE selection step, but before the GitHub login
2. **Provides clear information**: Explains what telemetry data is collected (high-level usage metrics) and what is NOT collected (task contents, code snippets, personal data)
3. **Emphasizes user control**: Clearly states that users can opt out at any time in Settings
4. **Handles user preferences**: Stores the telemetry preference in the `analytics_enabled` field of the config
5. **Follows the existing UI patterns**: Uses the same design system as other dialogs in the application

## Key Features

- **Clear messaging**: Explains that telemetry helps developers improve the product
- **Transparent about data collection**: Lists what is and isn't collected
- **User-friendly design**: Two clear buttons - opt-in and opt-out with descriptive text
- **Consistent with app design**: Uses shadcn/ui components and follows existing patterns
- **Privacy-first**: Defaults to asking permission rather than assuming consent

The implementation integrates seamlessly with the existing onboarding flow and properly handles the telemetry preference storage in the backend config system.

* Great! Now the issue should be fixed. The privacy dialog should now appear for new users since:

1.  I changed the default config to set `analytics_enabled: None` instead of `Some(true)`
2.  The dialog logic checks for `config.analytics_enabled === null` to show the dialog
3.  Added debug logging to help troubleshoot if needed

The dialog will now appear after users complete the onboarding (coding agent and IDE selection) but before the GitHub login step, asking them to opt-in or opt-out of telemetry collection.

For users who have already gone through onboarding and have `analytics_enabled: Some(true)` in their config, they won't see the dialog unless they reset their config or manually set `analytics_enabled` to `null`.

The privacy dialog should now appear correctly for new users going through the onboarding flow!

* Perfect! The build succeeded. Now the privacy dialog should appear correctly. Here's what I fixed:

## The Problem
The privacy dialog wasn't showing because:
1.  **Default config issue**: The backend was setting `analytics_enabled: Some(true)` by default instead of `None`
2.  **GitHub login interference**: The GitHub login dialog was being shown at the same time as the privacy dialog
3.  **Flow logic**: The privacy dialog wasn't being properly managed in the onboarding sequence

## The Solution
1. **Changed default config**: Set `analytics_enabled: None` in the backend default config
2. **Fixed dialog precedence**: The GitHub login dialog now only shows after the privacy dialog is completed
3. **Improved flow control**: The privacy dialog shows after onboarding, and GitHub login shows after privacy choice

## The Flow Now
1. **Disclaimer** (if not acknowledged) 
2. **Onboarding** (coding agent + IDE selection)
3. **Privacy Opt-in** (NEW - asks about telemetry)
4. **GitHub Login** (after privacy choice is made)

The privacy dialog should now appear correctly after you complete the coding agent and IDE selection step. The debug logging will also help us see what's happening in the browser console.

* Excellent! The implementation is now complete and working perfectly. Here's what I fixed:

##  Solution: Added `telemetry_acknowledged` Field

Instead of using `analytics_enabled` to determine whether to show the dialog, I added a dedicated `telemetry_acknowledged` field to the Config, following the same pattern as `disclaimer_acknowledged` and `onboarding_acknowledged`.

### Changes Made:

1. **Backend Config (`backend/src/models/config.rs`)**:
   - Added `telemetry_acknowledged: bool` field to Config struct
   - Set default value to `false` in Default implementation
   - Regenerated TypeScript types

2. **Frontend Logic (`frontend/src/App.tsx`)**:
   - Updated dialog flow to use `!config.telemetry_acknowledged` to show privacy dialog
   - Privacy dialog shows after onboarding is complete
   - GitHub login only shows after telemetry dialog is completed
   - Privacy handler sets both `telemetry_acknowledged: true` and `analytics_enabled` to user's choice

3. **Dialog Flow**:
   - **Disclaimer** → **Onboarding** → **Privacy Opt-in** → **GitHub Login**

### Why This Works Better:

-  **Clean separation**: `telemetry_acknowledged` tracks if user has seen the dialog, `analytics_enabled` tracks their choice
-  **Consistent pattern**: Follows the same pattern as existing acknowledgment flags
-  **Clear flow control**: Each dialog shows in sequence without interference
-  **Predictable behavior**: No complex null/undefined checks

The privacy dialog should now appear correctly after completing the onboarding step!

* Perfect! Here's what I fixed to resolve the overflow issue:

## Layout Improvements

1. **Increased dialog width**: Changed from `sm:max-w-[600px]` to `sm:max-w-[700px]` to give more space
2. **Added scroll support**: Added `max-h-[80vh] overflow-y-auto` to handle very tall content on smaller screens
3. **Better text wrapping**: Added `min-w-0` to text containers to allow proper text wrapping
4. **Responsive footer**: Added `flex-col sm:flex-row` to stack buttons vertically on mobile
5. **Shorter button text**: Changed "No, I'd prefer not to share" to "No thanks" for better fit
6. **Icon positioning**: Added `flex-shrink-0` to icons to prevent them from shrinking

## The dialog now:
-  Has adequate width for all content
-  Handles overflow gracefully with scrolling
-  Works well on both desktop and mobile
-  Maintains the professional look while being more compact

The privacy dialog should now display properly without any content overflow issues!

* Perfect! I've made the dialog much more compact by:

## Height Reduction Changes:

1. **Removed the second card** - Combined the "Why does this help?" explanation into the footer note
2. **Reduced spacing**: `space-y-4 py-4` → `space-y-3 py-3`
3. **Smaller text sizes**: 
   - Title: `text-lg` → `text-base`
   - Body text: `text-sm` → `text-xs` for descriptions
   - Main text: `font-medium` → `text-sm font-medium`
4. **Reduced padding**:
   - Card header: Added `pb-3` and `pt-0` for content
   - Footer note: `p-3` → `p-2`
   - Icons: `h-5 w-5` → `h-4 w-4`, `h-4 w-4` → `h-3 w-3`
5. **Tighter gaps**: `gap-3` → `gap-2` for list items
6. **Condensed text**: Made descriptions more concise without losing meaning
7. **Removed overflow scroll**: No longer needed with compact layout

The dialog is now much more compact and should fit without any scrolling while still maintaining all the important information and maintaining readability!

* Perfect! I've updated the dialog to be accurate and transparent about data collection:

##  Updated Privacy Information

### Key Changes:
1. **Removed "anonymous" claim** - Now says "usage data" instead of "anonymous usage data"
2. **Added GitHub profile information** - Clearly states that username and email are collected for contact purposes
3. **Updated description** - Now mentions "allowing us to contact you if needed"
4. **Clarified what's NOT collected** - Changed "personal data" to "other personal data" to be more specific

### The dialog now accurately reflects:
- **What IS collected**: GitHub profile info (for contact), usage metrics, performance data
- **Why profile data is collected**: So you can be contacted if necessary
- **What is NOT collected**: Task contents, code snippets, project names, other personal data
- **User control**: Can change preference anytime in Settings

The dialog is now transparent and honest about the data collection while still explaining the benefits of opting in.

* copy

* lint

* prettier
This commit is contained in:
Louis Knight-Webb
2025-07-11 22:53:40 +01:00
committed by GitHub
parent 3e861da0b2
commit aafe215f21
4 changed files with 211 additions and 243 deletions

View File

@@ -4,89 +4,35 @@
export type ApiResponse<T> = { success: boolean, data: T | null, message: string | null, };
export type Config = {
theme: ThemeMode,
executor: ExecutorConfig,
disclaimer_acknowledged: boolean,
onboarding_acknowledged: boolean,
sound_alerts: boolean,
sound_file: SoundFile,
push_notifications: boolean,
editor: EditorConfig,
github: GitHubConfig,
analytics_enabled: boolean | null,
};
export type Config = { theme: ThemeMode, executor: ExecutorConfig, disclaimer_acknowledged: boolean, onboarding_acknowledged: boolean, telemetry_acknowledged: boolean, sound_alerts: boolean, sound_file: SoundFile, push_notifications: boolean, editor: EditorConfig, github: GitHubConfig, analytics_enabled: boolean | null, };
export type ThemeMode = "light" | "dark" | "system" | "purple" | "green" | "blue" | "orange" | "red";
export type EditorConfig = { editor_type: EditorType, custom_command: string | null, };
export type GitHubConfig = {
pat: string | null,
token: string | null,
username: string | null,
primary_email: string | null,
default_pr_base: string | null,
};
export type GitHubConfig = { pat: string | null, token: string | null, username: string | null, primary_email: string | null, default_pr_base: string | null, };
export type EditorType = "vscode" | "cursor" | "windsurf" | "intellij" | "zed" | "custom";
export type EditorConstants = { editor_types: Array<EditorType>, editor_labels: Array<string>, };
export type SoundFile =
"abstract-sound1"
| "abstract-sound2"
| "abstract-sound3"
| "abstract-sound4"
| "cow-mooing"
| "phone-vibration"
| "rooster";
export type SoundFile = "abstract-sound1" | "abstract-sound2" | "abstract-sound3" | "abstract-sound4" | "cow-mooing" | "phone-vibration" | "rooster";
export type SoundConstants = { sound_files: Array<SoundFile>, sound_labels: Array<string>, };
export type ConfigConstants = { editor: EditorConstants, sound: SoundConstants, };
export type ExecutorConfig = { "type": "echo" } | { "type": "claude" } | { "type": "amp" } | { "type": "gemini" } | {
"type": "opencode"
};
export type ExecutorConfig = { "type": "echo" } | { "type": "claude" } | { "type": "amp" } | { "type": "gemini" } | { "type": "opencode" };
export type ExecutorConstants = { executor_types: Array<ExecutorConfig>, executor_labels: Array<string>, };
export type CreateProject = {
name: string,
git_repo_path: string,
use_existing_repo: boolean,
setup_script: string | null,
dev_script: string | null,
};
export type CreateProject = { name: string, git_repo_path: string, use_existing_repo: boolean, setup_script: string | null, dev_script: string | null, };
export type Project = {
id: string,
name: string,
git_repo_path: string,
setup_script: string | null,
dev_script: string | null,
created_at: Date,
updated_at: Date,
};
export type Project = { id: string, name: string, git_repo_path: string, setup_script: string | null, dev_script: string | null, created_at: Date, updated_at: Date, };
export type ProjectWithBranch = {
id: string,
name: string,
git_repo_path: string,
setup_script: string | null,
dev_script: string | null,
current_branch: string | null,
created_at: Date,
updated_at: Date,
};
export type ProjectWithBranch = { id: string, name: string, git_repo_path: string, setup_script: string | null, dev_script: string | null, current_branch: string | null, created_at: Date, updated_at: Date, };
export type UpdateProject = {
name: string | null,
git_repo_path: string | null,
setup_script: string | null,
dev_script: string | null,
};
export type UpdateProject = { name: string | null, git_repo_path: string | null, setup_script: string | null, dev_script: string | null, };
export type SearchResult = { path: string, is_file: boolean, match_type: SearchMatchType, };
@@ -98,64 +44,19 @@ export type CreateBranch = { name: string, base_branch: string | null, };
export type CreateTask = { project_id: string, title: string, description: string | null, };
export type CreateTaskAndStart = {
project_id: string,
title: string,
description: string | null,
executor: ExecutorConfig | null,
};
export type CreateTaskAndStart = { project_id: string, title: string, description: string | null, executor: ExecutorConfig | null, };
export type TaskStatus = "todo" | "inprogress" | "inreview" | "done" | "cancelled";
export type Task = {
id: string,
project_id: string,
title: string,
description: string | null,
status: TaskStatus,
created_at: string,
updated_at: string,
};
export type Task = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, created_at: string, updated_at: string, };
export type TaskWithAttemptStatus = {
id: string,
project_id: string,
title: string,
description: string | null,
status: TaskStatus,
created_at: string,
updated_at: string,
has_in_progress_attempt: boolean,
has_merged_attempt: boolean,
has_failed_attempt: boolean,
};
export type TaskWithAttemptStatus = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, created_at: string, updated_at: string, has_in_progress_attempt: boolean, has_merged_attempt: boolean, has_failed_attempt: boolean, };
export type UpdateTask = { title: string | null, description: string | null, status: TaskStatus | null, };
export type TaskAttemptStatus =
"setuprunning"
| "setupcomplete"
| "setupfailed"
| "executorrunning"
| "executorcomplete"
| "executorfailed";
export type TaskAttemptStatus = "setuprunning" | "setupcomplete" | "setupfailed" | "executorrunning" | "executorcomplete" | "executorfailed";
export type TaskAttempt = {
id: string,
task_id: string,
worktree_path: string,
branch: string,
base_branch: string,
merge_commit: string | null,
executor: string | null,
pr_url: string | null,
pr_number: bigint | null,
pr_status: string | null,
pr_merged_at: string | null,
worktree_deleted: boolean,
created_at: string,
updated_at: string,
};
export type TaskAttempt = { id: string, task_id: string, worktree_path: string, branch: string, base_branch: string, merge_commit: string | null, executor: string | null, pr_url: string | null, pr_number: bigint | null, pr_status: string | null, pr_merged_at: string | null, worktree_deleted: boolean, setup_completed_at: string | null, created_at: string, updated_at: string, };
export type CreateTaskAttempt = { executor: string | null, base_branch: string | null, };
@@ -163,22 +64,11 @@ export type UpdateTaskAttempt = Record<string, never>;
export type CreateFollowUpAttempt = { prompt: string, };
export type TaskAttemptActivity = {
id: string,
execution_process_id: string,
status: TaskAttemptStatus,
note: string | null,
created_at: string,
};
export type TaskAttemptActivity = { id: string, execution_process_id: string, status: TaskAttemptStatus, note: string | null, created_at: string, };
export type TaskAttemptActivityWithPrompt = {
id: string,
execution_process_id: string,
status: TaskAttemptStatus,
note: string | null,
created_at: string,
prompt: string | null,
};
export type TaskAttemptActivityWithPrompt = { id: string, execution_process_id: string, status: TaskAttemptStatus, note: string | null, created_at: string, prompt: string | null, };
export type CreateTaskAttemptActivity = { execution_process_id: string, status: TaskAttemptStatus | null, note: string | null, };
export type AttemptData = {
activities: TaskAttemptActivityWithPrompt[];
@@ -186,18 +76,6 @@ export type AttemptData = {
runningProcessDetails: Record<string, ExecutionProcess>;
}
export type CreateTaskAttemptActivity = {
execution_process_id: string,
status: TaskAttemptStatus | null,
note: string | null,
};
export type DirectoryEntry = { name: string, path: string, is_directory: boolean, is_git_repo: boolean, };
export type DiffChunkType = "Equal" | "Insert" | "Delete";
export type DiffChunk = { chunk_type: DiffChunkType, content: string, };
export interface ProcessedLine {
content: string;
chunkType: DiffChunkType;
@@ -213,129 +91,47 @@ export interface ProcessedSection {
expandedBelow?: boolean;
}
export type DirectoryEntry = { name: string, path: string, is_directory: boolean, is_git_repo: boolean, };
export type DiffChunkType = "Equal" | "Insert" | "Delete";
export type DiffChunk = { chunk_type: DiffChunkType, content: string, };
export type FileDiff = { path: string, chunks: Array<DiffChunk>, };
export type WorktreeDiff = { files: Array<FileDiff>, };
export type BranchStatus = {
is_behind: boolean,
commits_behind: number,
commits_ahead: number,
up_to_date: boolean,
merged: boolean,
has_uncommitted_changes: boolean,
base_branch_name: string,
};
export type BranchStatus = { is_behind: boolean, commits_behind: number, commits_ahead: number, up_to_date: boolean, merged: boolean, has_uncommitted_changes: boolean, base_branch_name: string, };
export type ExecutionState =
"NotStarted"
| "SetupRunning"
| "SetupComplete"
| "SetupFailed"
| "CodingAgentRunning"
| "CodingAgentComplete"
| "CodingAgentFailed"
| "Complete";
export type ExecutionState = "NotStarted" | "SetupRunning" | "SetupComplete" | "SetupFailed" | "CodingAgentRunning" | "CodingAgentComplete" | "CodingAgentFailed" | "Complete";
export type TaskAttemptState = {
execution_state: ExecutionState,
has_changes: boolean,
has_setup_script: boolean,
setup_process_id: string | null,
coding_agent_process_id: string | null,
};
export type TaskAttemptState = { execution_state: ExecutionState, has_changes: boolean, has_setup_script: boolean, setup_process_id: string | null, coding_agent_process_id: string | null, };
export type ExecutionProcess = {
id: string,
task_attempt_id: string,
process_type: ExecutionProcessType,
executor_type: string | null,
status: ExecutionProcessStatus,
command: string,
args: string | null,
working_directory: string,
stdout: string | null,
stderr: string | null,
exit_code: bigint | null,
started_at: string,
completed_at: string | null,
created_at: string,
updated_at: string,
};
export type ExecutionProcess = { id: string, task_attempt_id: string, process_type: ExecutionProcessType, executor_type: string | null, status: ExecutionProcessStatus, command: string, args: string | null, working_directory: string, stdout: string | null, stderr: string | null, exit_code: bigint | null, started_at: string, completed_at: string | null, created_at: string, updated_at: string, };
export type ExecutionProcessSummary = {
id: string,
task_attempt_id: string,
process_type: ExecutionProcessType,
executor_type: string | null,
status: ExecutionProcessStatus,
command: string,
args: string | null,
working_directory: string,
exit_code: bigint | null,
started_at: string,
completed_at: string | null,
created_at: string,
updated_at: string,
};
export type ExecutionProcessSummary = { id: string, task_attempt_id: string, process_type: ExecutionProcessType, executor_type: string | null, status: ExecutionProcessStatus, command: string, args: string | null, working_directory: string, exit_code: bigint | null, started_at: string, completed_at: string | null, created_at: string, updated_at: string, };
export type ExecutionProcessStatus = "running" | "completed" | "failed" | "killed";
export type ExecutionProcessType = "setupscript" | "codingagent" | "devserver";
export type CreateExecutionProcess = {
task_attempt_id: string,
process_type: ExecutionProcessType,
executor_type: string | null,
command: string,
args: string | null,
working_directory: string,
};
export type CreateExecutionProcess = { task_attempt_id: string, process_type: ExecutionProcessType, executor_type: string | null, command: string, args: string | null, working_directory: string, };
export type UpdateExecutionProcess = {
status: ExecutionProcessStatus | null,
exit_code: bigint | null,
completed_at: string | null,
};
export type UpdateExecutionProcess = { status: ExecutionProcessStatus | null, exit_code: bigint | null, completed_at: string | null, };
export type ExecutorSession = {
id: string,
task_attempt_id: string,
execution_process_id: string,
session_id: string | null,
prompt: string | null,
summary: string | null,
created_at: string,
updated_at: string,
};
export type ExecutorSession = { id: string, task_attempt_id: string, execution_process_id: string, session_id: string | null, prompt: string | null, summary: string | null, created_at: string, updated_at: string, };
export type CreateExecutorSession = { task_attempt_id: string, execution_process_id: string, prompt: string | null, };
export type UpdateExecutorSession = { session_id: string | null, prompt: string | null, summary: string | null, };
export type NormalizedConversation = {
entries: Array<NormalizedEntry>,
session_id: string | null,
executor_type: string,
prompt: string | null,
summary: string | null,
};
export type NormalizedConversation = { entries: Array<NormalizedEntry>, session_id: string | null, executor_type: string, prompt: string | null, summary: string | null, };
export type NormalizedEntry = { timestamp: string | null, entry_type: NormalizedEntryType, content: string, };
export type NormalizedEntryType = { "type": "user_message" } | { "type": "assistant_message" } | {
"type": "tool_use",
tool_name: string,
action_type: ActionType,
} | { "type": "system_message" } | { "type": "error_message" } | { "type": "thinking" };
export type NormalizedEntryType = { "type": "user_message" } | { "type": "assistant_message" } | { "type": "tool_use", tool_name: string, action_type: ActionType, } | { "type": "system_message" } | { "type": "error_message" } | { "type": "thinking" };
export type ActionType = { "action": "file_read", path: string, } | { "action": "file_write", path: string, } | {
"action": "command_run",
command: string,
} | { "action": "search", query: string, } | { "action": "web_fetch", url: string, } | {
"action": "task_create",
description: string,
} | { "action": "other", description: string, };
export type ActionType = { "action": "file_read", path: string, } | { "action": "file_write", path: string, } | { "action": "command_run", command: string, } | { "action": "search", query: string, } | { "action": "web_fetch", url: string, } | { "action": "task_create", description: string, } | { "action": "other", description: string, };
// Generated constants
export const EXECUTOR_TYPES: string[] = [
@@ -348,7 +144,7 @@ export const EXECUTOR_TYPES: string[] = [
export const EDITOR_TYPES: EditorType[] = [
"vscode",
"cursor",
"cursor",
"windsurf",
"intellij",
"zed",