Showing preview only (2,364K chars total). Download the full file or copy to clipboard to get everything.
Repository: heyito/ito
Branch: main
Commit: ab9d158b61dd
Files: 400
Total size: 2.2 MB
Directory structure:
gitextract_bmhz2bpu/
├── .claude/
│ └── settings.local.json
├── .cursor/
│ └── rules/
│ ├── always.mdc
│ ├── code-conventions.mdc
│ ├── react.mdc
│ └── typescript.mdc
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── app-deploy.yml
│ ├── autolink-pr-to-issue.yml
│ ├── build-image.yml
│ ├── build.yml
│ ├── ci-controller.yml
│ ├── deploy-server.yml
│ ├── infra-deploy.yml
│ ├── native-build-check.yml
│ └── test-runner.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .vscode/
│ └── settings.json
├── AGENTS.md
├── CLAUDE.md
├── LICENSE
├── README.md
├── app/
│ ├── app.tsx
│ ├── assets/
│ │ ├── .gitignore
│ │ ├── accesssibility.webm
│ │ └── microphone.webm
│ ├── components/
│ │ ├── analytics/
│ │ │ └── index.ts
│ │ ├── auth/
│ │ │ ├── Auth0Provider.tsx
│ │ │ └── useAuth.ts
│ │ ├── home/
│ │ │ ├── HomeKit.tsx
│ │ │ ├── ProUpgradeDialog.tsx
│ │ │ └── contents/
│ │ │ ├── AboutContent.tsx
│ │ │ ├── DictionaryContent.tsx
│ │ │ ├── HomeContent.tsx
│ │ │ ├── NotesContent.tsx
│ │ │ ├── SettingsContent.tsx
│ │ │ ├── activityMessages.ts
│ │ │ └── settings/
│ │ │ ├── AccountSettingsContent.tsx
│ │ │ ├── AdvancedSettingsContent.tsx
│ │ │ ├── AudioSettingsContent.tsx
│ │ │ ├── GeneralSettingsContent.tsx
│ │ │ ├── KeyboardSettingsContent.tsx
│ │ │ └── PricingBillingSettingsContent.tsx
│ │ ├── icons/
│ │ │ ├── AppleIcon.tsx
│ │ │ ├── AppleNotesIcon.tsx
│ │ │ ├── AsterikIcon.tsx
│ │ │ ├── AudioIcon.tsx
│ │ │ ├── AvatarIcon.tsx
│ │ │ ├── ChatGPTIcon.tsx
│ │ │ ├── ClaudeIcon.tsx
│ │ │ ├── CodeWindowIcon.tsx
│ │ │ ├── ColorSchemeIcon.tsx
│ │ │ ├── CursorIcon.tsx
│ │ │ ├── DiscordIcon.tsx
│ │ │ ├── FanIcon.tsx
│ │ │ ├── GitHubIcon.tsx
│ │ │ ├── GmailIcon.tsx
│ │ │ ├── GoogleIcon.tsx
│ │ │ ├── IMessageIcon.tsx
│ │ │ ├── ItoIcon.tsx
│ │ │ ├── MicrosoftIcon.tsx
│ │ │ ├── NotionIcon.tsx
│ │ │ ├── SlackIcon.tsx
│ │ │ ├── SpeedIcon.tsx
│ │ │ ├── TotalWordsIcon.tsx
│ │ │ ├── VSCodeIcon.tsx
│ │ │ └── XIcon.tsx
│ │ ├── pill/
│ │ │ ├── Pill.tsx
│ │ │ └── contents/
│ │ │ ├── AudioBars.tsx
│ │ │ ├── AudioBarsBase.tsx
│ │ │ ├── LoadingAnimation.tsx
│ │ │ ├── PreviewAudioBars.tsx
│ │ │ └── TooltipButton.tsx
│ │ ├── ui/
│ │ │ ├── animated-checkmark.tsx
│ │ │ ├── app-orbit-image.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── button.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ ├── keyboard-key.tsx
│ │ │ ├── keyboard-shortcut-editor.tsx
│ │ │ ├── microphone-selector.tsx
│ │ │ ├── multi-shortcut-editor.tsx
│ │ │ ├── nav-item.tsx
│ │ │ ├── note.tsx
│ │ │ ├── spinner.tsx
│ │ │ ├── status-indicator.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── tip.tsx
│ │ │ └── tooltip.tsx
│ │ ├── welcome/
│ │ │ ├── WelcomeKit.tsx
│ │ │ ├── contents/
│ │ │ │ ├── AnyAppContent.tsx
│ │ │ │ ├── CheckEmailContent.tsx
│ │ │ │ ├── CreateAccountContent.tsx
│ │ │ │ ├── DataControlContent.tsx
│ │ │ │ ├── EmailLoginContent.tsx
│ │ │ │ ├── EmailSignupContent.tsx
│ │ │ │ ├── GoodToGoContent.tsx
│ │ │ │ ├── IntroducingIntelligentModeContent.tsx
│ │ │ │ ├── KeyboardTestContext.tsx
│ │ │ │ ├── MicrophoneTestContent.tsx
│ │ │ │ ├── PermissionsContent.tsx
│ │ │ │ ├── ReferralContent.tsx
│ │ │ │ ├── SignInContent.tsx
│ │ │ │ └── TryItOutContent.tsx
│ │ │ └── styles.css
│ │ └── window/
│ │ ├── OnboardingTitlebar.tsx
│ │ ├── Titlebar.tsx
│ │ ├── TitlebarContext.tsx
│ │ └── WindowContext.tsx
│ ├── generated/
│ │ ├── buf/
│ │ │ └── validate/
│ │ │ └── validate_pb.ts
│ │ ├── ito_connect.ts
│ │ └── ito_pb.ts
│ ├── hooks/
│ │ ├── useBillingState.test.ts
│ │ ├── useBillingState.ts
│ │ ├── useDeviceChangeListener.ts
│ │ └── usePlatform.ts
│ ├── index.d.ts
│ ├── index.html
│ ├── media/
│ │ └── microphone.ts
│ ├── renderer.tsx
│ ├── sentry.ts
│ ├── store/
│ │ ├── useAdvancedSettingsStore.ts
│ │ ├── useAudioStore.ts
│ │ ├── useAuthStore.ts
│ │ ├── useDictionaryStore.ts
│ │ ├── useMainStore.ts
│ │ ├── useNotesStore.ts
│ │ ├── useOnboardingStore.ts
│ │ ├── usePermissionsStore.ts
│ │ ├── useSettingsStore.ts
│ │ ├── useShortcutEditingStore.ts
│ │ └── useUserMetadataStore.ts
│ ├── styles/
│ │ ├── app.css
│ │ ├── globals.css
│ │ └── window.css
│ └── utils/
│ ├── audioUtils.ts
│ ├── healthCheck.test.ts
│ ├── healthCheck.ts
│ ├── keyboard.test.ts
│ ├── keyboard.ts
│ └── utils.ts
├── build/
│ ├── entitlements.mac.inherit.plist
│ └── entitlements.mac.plist
├── build-app.sh
├── build-binaries.sh
├── commitlint.config.js
├── components.json
├── dev-app-update.yml
├── electron-builder.config.js
├── electron.vite.config.ts
├── eslint.config.mjs
├── lib/
│ ├── __tests__/
│ │ ├── fixtures/
│ │ │ ├── auth.ts
│ │ │ └── database.ts
│ │ ├── helpers/
│ │ │ └── testUtils.ts
│ │ ├── mocks/
│ │ │ ├── electron.ts
│ │ │ └── sqlite.ts
│ │ └── setup.ts
│ ├── auth/
│ │ ├── config.test.ts
│ │ ├── config.ts
│ │ ├── events.test.ts
│ │ └── events.ts
│ ├── clients/
│ │ ├── grpcClient.test.ts
│ │ ├── grpcClient.ts
│ │ └── itoHttpClient.ts
│ ├── constants/
│ │ ├── external-links.ts
│ │ ├── generated-defaults.ts
│ │ ├── keyboard-defaults.ts
│ │ ├── store-keys.test.ts
│ │ └── store-keys.ts
│ ├── main/
│ │ ├── app.ts
│ │ ├── appNap.ts
│ │ ├── audio/
│ │ │ ├── AudioStreamManager.test.ts
│ │ │ └── AudioStreamManager.ts
│ │ ├── autoUpdaterWrapper.ts
│ │ ├── context/
│ │ │ └── ContextGrabber.ts
│ │ ├── env.ts
│ │ ├── grammar/
│ │ │ ├── GrammarRulesService.test.ts
│ │ │ └── GrammarRulesService.ts
│ │ ├── index.d.ts
│ │ ├── interactions/
│ │ │ ├── InteractionManager.test.ts
│ │ │ └── InteractionManager.ts
│ │ ├── itoSessionManager.test.ts
│ │ ├── itoSessionManager.ts
│ │ ├── itoStreamController.test.ts
│ │ ├── itoStreamController.ts
│ │ ├── logger.ts
│ │ ├── main.ts
│ │ ├── recordingStateNotifier.ts
│ │ ├── sentry.ts
│ │ ├── sqlite/
│ │ │ ├── db.test.ts
│ │ │ ├── db.ts
│ │ │ ├── migrations.ts
│ │ │ ├── models.ts
│ │ │ ├── repo.test.ts
│ │ │ ├── repo.ts
│ │ │ ├── schema.ts
│ │ │ └── utils.ts
│ │ ├── store.test.ts
│ │ ├── store.ts
│ │ ├── syncService.test.ts
│ │ ├── syncService.ts
│ │ ├── teardown.ts
│ │ ├── text/
│ │ │ ├── TextInserter.test.ts
│ │ │ └── TextInserter.ts
│ │ ├── timing/
│ │ │ ├── TimingCollector.test.ts
│ │ │ └── TimingCollector.ts
│ │ ├── tray.ts
│ │ ├── voiceInputService.test.ts
│ │ └── voiceInputService.ts
│ ├── media/
│ │ ├── IAccessibilityContextProvider.ts
│ │ ├── active-application.test.ts
│ │ ├── active-application.ts
│ │ ├── audio.test.ts
│ │ ├── audio.ts
│ │ ├── keyboard.test.ts
│ │ ├── keyboard.ts
│ │ ├── macOSAccessibilityContextProvider.ts
│ │ ├── microphoneSetUp.ts
│ │ ├── native-interface.test.ts
│ │ ├── native-interface.ts
│ │ ├── selected-text-reader.test.ts
│ │ ├── selected-text-reader.ts
│ │ ├── systemAudio.ts
│ │ └── text-writer.ts
│ ├── preload/
│ │ ├── api.test.ts
│ │ ├── api.ts
│ │ ├── index.d.ts
│ │ └── preload.ts
│ ├── protocol/
│ │ └── index.ts
│ ├── types/
│ │ ├── cursorContext.ts
│ │ ├── ipc.ts
│ │ └── keyboard.ts
│ ├── utils/
│ │ ├── applicationDetection.ts
│ │ ├── crossPlatform.ts
│ │ ├── settings.test.ts
│ │ └── settings.ts
│ ├── utils.ts
│ └── window/
│ ├── index.ts
│ ├── ipcDev.ts
│ ├── ipcEvents.test.ts
│ └── ipcEvents.ts
├── native/
│ ├── Cargo.toml
│ ├── active-application/
│ │ ├── Cargo.toml
│ │ ├── active-application.manifest
│ │ ├── build.rs
│ │ └── src/
│ │ └── main.rs
│ ├── audio-recorder/
│ │ ├── Cargo.toml
│ │ ├── audio-recorder.manifest
│ │ ├── build.rs
│ │ └── src/
│ │ └── main.rs
│ ├── clippy.toml
│ ├── cursor-context/
│ │ ├── Package.resolved
│ │ ├── Package.swift
│ │ └── Sources/
│ │ └── cursor-context/
│ │ ├── CLI.swift
│ │ └── main.swift
│ ├── global-key-listener/
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── build.rs
│ │ ├── global-key-listener.manifest
│ │ └── src/
│ │ ├── key_codes.rs
│ │ └── main.rs
│ ├── macos-text/
│ │ ├── .gitignore
│ │ ├── Package.swift
│ │ └── Sources/
│ │ └── focused-text-reader/
│ │ └── main.swift
│ ├── rustfmt.toml
│ ├── selected-text-reader/
│ │ ├── Cargo.toml
│ │ ├── build.rs
│ │ ├── selected-text-reader.manifest
│ │ └── src/
│ │ ├── macos.rs
│ │ ├── main.rs
│ │ └── windows.rs
│ └── text-writer/
│ ├── Cargo.toml
│ ├── build.rs
│ ├── src/
│ │ ├── macos_writer.rs
│ │ ├── main.rs
│ │ └── windows_writer.rs
│ └── text-writer.manifest
├── package.json
├── resources/
│ └── build/
│ ├── entitlements.mac.plist
│ └── icon.icns
├── scripts/
│ ├── clean-app-data.js
│ └── generate-constants.js
├── server/
│ ├── .dockerignore
│ ├── Dockerfile
│ ├── README.md
│ ├── buf.gen.yaml
│ ├── buf.yaml
│ ├── docker-compose.yml
│ ├── infra/
│ │ ├── .gitignore
│ │ ├── .npmignore
│ │ ├── README.md
│ │ ├── bin/
│ │ │ └── infra.ts
│ │ ├── cdk.context.json
│ │ ├── cdk.json
│ │ ├── jest.config.js
│ │ ├── lambdas/
│ │ │ ├── firehose-transform.ts
│ │ │ ├── opensearch-bootstrap.ts
│ │ │ ├── run-migration.ts
│ │ │ └── timing-merger.ts
│ │ ├── lib/
│ │ │ ├── cicd-stack.ts
│ │ │ ├── constants.ts
│ │ │ ├── helpers.ts
│ │ │ ├── network-stack.ts
│ │ │ ├── observability-stack.ts
│ │ │ ├── platform-stack.ts
│ │ │ ├── security-stack.ts
│ │ │ ├── service/
│ │ │ │ ├── fargate-task.ts
│ │ │ │ ├── firehose-config.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── log-groups.ts
│ │ │ │ ├── migration-lambda.ts
│ │ │ │ └── opensearch-bootstrap.ts
│ │ │ ├── service-stack.ts
│ │ │ └── timing-config.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── package.json
│ ├── scripts/
│ │ ├── migrate-audio-to-s3.ts
│ │ ├── migrate.sh
│ │ └── setup-minio.sh
│ ├── src/
│ │ ├── auth/
│ │ │ ├── auth0Helpers.ts
│ │ │ └── userContext.ts
│ │ ├── clients/
│ │ │ ├── asrConfig.ts
│ │ │ ├── cerebrasClient.ts
│ │ │ ├── errors.ts
│ │ │ ├── groqClient.test.ts
│ │ │ ├── groqClient.ts
│ │ │ ├── intentTranscriptionConfig.ts
│ │ │ ├── llmProvider.ts
│ │ │ ├── providerUtils.ts
│ │ │ ├── providers.ts
│ │ │ └── s3storageClient.ts
│ │ ├── constants/
│ │ │ ├── generated-defaults.ts
│ │ │ ├── markers.ts
│ │ │ └── storage.ts
│ │ ├── db/
│ │ │ ├── models.ts
│ │ │ └── repo.ts
│ │ ├── db.ts
│ │ ├── generated/
│ │ │ ├── buf/
│ │ │ │ └── validate/
│ │ │ │ └── validate_pb.ts
│ │ │ ├── ito_connect.ts
│ │ │ └── ito_pb.ts
│ │ ├── index.ts
│ │ ├── ito.proto
│ │ ├── migrations/
│ │ │ ├── 1722889955000_initial_schema.js
│ │ │ ├── 1752006262324_add-raw-audio-column.js
│ │ │ ├── 1752099660683_add-duration-ms.js
│ │ │ ├── 1753297915000_add-advanced-settings.js
│ │ │ ├── 1754938499581_update-advanced-settings.js
│ │ │ ├── 1756922843670_raw-audio-reference.js
│ │ │ ├── 1760496947939_add-temporary-analytics-token.js
│ │ │ ├── 1761765111646_add-user-trials.js
│ │ │ ├── 1761778190395_add-user-subscriptions.js
│ │ │ ├── 1762468699097_add-subscription-end-at.js
│ │ │ ├── 1763753112000_make-llm-settings-nullable.js
│ │ │ └── schema/
│ │ │ └── initial.js
│ │ ├── prompts/
│ │ │ ├── transcription.test.ts
│ │ │ └── transcription.ts
│ │ ├── server.ts
│ │ ├── services/
│ │ │ ├── __tests__/
│ │ │ │ └── helpers.ts
│ │ │ ├── auth0.ts
│ │ │ ├── billing.test.ts
│ │ │ ├── billing.ts
│ │ │ ├── cloudWatchLogger.ts
│ │ │ ├── errorInterceptor.ts
│ │ │ ├── ito/
│ │ │ │ ├── audioUtils.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── helpers.ts
│ │ │ │ ├── itoService.ts
│ │ │ │ ├── timingService.ts
│ │ │ │ ├── transcribeStreamHandler.ts
│ │ │ │ ├── transcribeStreamV2Handler.ts
│ │ │ │ └── types.ts
│ │ │ ├── logging.test.ts
│ │ │ ├── logging.ts
│ │ │ ├── loggingInterceptor.ts
│ │ │ ├── stripeWebhook.test.ts
│ │ │ ├── stripeWebhook.ts
│ │ │ ├── timing/
│ │ │ │ └── ServerTimingCollector.ts
│ │ │ ├── trial.test.ts
│ │ │ ├── trial.ts
│ │ │ ├── validationInterceptor.test.ts
│ │ │ └── validationInterceptor.ts
│ │ ├── utils/
│ │ │ ├── abortUtils.ts
│ │ │ ├── audio.ts
│ │ │ ├── audioProcessing.ts
│ │ │ └── renderCallback.ts
│ │ └── validation/
│ │ ├── HeaderValidator.test.ts
│ │ ├── HeaderValidator.ts
│ │ ├── schemas.test.ts
│ │ └── schemas.ts
│ ├── test-client.ts
│ └── tsconfig.json
├── shared-constants.js
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.web.json
└── vite-env.d.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .claude/settings.local.json
================================================
{
"permissions": {
"allow": [
"WebFetch(domain:github.com)",
"WebFetch(domain:docs.microsoft.com)",
"Bash(bun build:rust:win:*)",
"Bash(bun runTest:*)",
"Bash(bun test:*)",
"Bash(cargo build:*)",
"Bash(git show:*)",
"WebFetch(domain:stackoverflow.com)",
"WebFetch(domain:www.electronjs.org)",
"WebFetch(domain:www.electron.build)",
"Bash(where:*)",
"Bash(cargo test)",
"Bash(cargo test:*)",
"Bash(bun:*)",
"Bash(cargo clippy:*)",
"Bash(cargo clean:*)",
"Bash(awk:*)",
"WebFetch(domain:connectrpc.com)",
"Bash(git log:*)"
],
"deny": [],
"ask": []
}
}
================================================
FILE: .cursor/rules/always.mdc
================================================
---
description:
globs:
alwaysApply: true
---
## Development cycle
1. Before writing any code, come up with an extremely good plan, review the plan, and then ask the user for permission to execute the plan.
2. Always rely on the user for manually testing
3. Always use bun as your preferred package manager.
4. Always rely on the user to install new packages / update new packages
5. Prefer code that is easy to read and self-documenting. Use comments sparingly.
6. Never use empty catch statements.
================================================
FILE: .cursor/rules/code-conventions.mdc
================================================
---
description:
globs: **/*.ts,**/*.tsx
alwaysApply: false
---
## Technologies used
- **React** - Build interactive UIs with React components
- **Tailwind CSS** - Utility-first CSS framework for rapid UI development
- **TypeScript** - Full type safety across all packages
- **PNPM Workspaces** - Fast, disk-efficient package management
- **ESLint & Prettier** - Code quality tools configured and ready to use
- **Auth** - We use Auth0
- **DB** - We use postgres in the server and SQLite in the app
- **Package manager** We use Bun as our preferred package manager. Please use bun whenever applicable.
# Code Conventions
When writing or modifying code in this project, please adhere to the following conventions:
1. **TypeScript Best Practices**: Follow standard, idiomatic TypeScript coding practices for structure, naming, and types, unless otherwise overridden.
2. **Minimal Comments**: Avoid adding comments unless they explain complex logic or non-obvious decisions. Well-written, self-explanatory code is preferred. Do not add comments that merely restate what the code does.
3. **Tests as Documentation**: Rely on comprehensive tests (which will be added later if not present) to document the behavior and usage of the code, rather than extensive comments within the code itself.
4. **File naming conventions**: Use kebab-case when naming directories, TypeScript, and other files.
Only make the exact changes I request—do not modify, remove, or alter any other code, styling, or page elements unless explicitly instructed. If my request conflicts with existing code, styling, or functionality, or if you anticipate any issues, pause execution and notify me for confirmation before proceeding. Always follow this rule for every modification. If in doubt, ask before making any change.
================================================
FILE: .cursor/rules/react.mdc
================================================
---
description:
globs: **/*.tsx
alwaysApply: false
---
React Naming Conventions:
- Use kebab-case for files and directories.
Components:
- DO not use 'use client' or 'use server' statements
- Favor named exports for components
- Ensure components are modular, reusable, and maintain a clear separation of concerns.
- Always split React components out so there is only ever one per file
- Keep logic as low as possible.
UI and Styling:
- Implement responsive design with Tailwind CSS; use a mobile-first approach
================================================
FILE: .cursor/rules/typescript.mdc
================================================
---
description:
globs: **/*.ts,**/*.tsx
alwaysApply: false
---
# TypeScript Best Practices
## Type System
- Prefer interfaces over types for object definitions
- Use type for unions, intersections, and mapped types
- NEVER use `any` or `as any` types or coercion
- Use strict TypeScript configuration
- Leverage TypeScript's built-in utility types
- Use generics for reusable type patterns
## Naming Conventions
- Use PascalCase for type names and interfaces
- Use camelCase for variables and functions
- Use UPPER_CASE for constants
- Use descriptive names with auxiliary verbs (e.g., isLoading, hasError)
- Prefix interfaces for React props with 'Props' (e.g., ButtonProps)
## Code Organization
- Keep type definitions close to where they're used
- Export types and interfaces from dedicated type files when shared
- Use barrel exports (index.ts) for organizing exports
- Place shared types in a `types.ts` file
- Co-locate component props with their components
## Functions
- Use explicit return types for public functions
- Use arrow functions for callbacks and methods
- Implement proper error handling with custom error types
- Use function overloads for complex type scenarios
- Prefer async/await over Promises
- Prefer function declarations over function expressions.
- Prefer functional programming over classes.
## Best Practices
- Enable strict mode in tsconfig.json
- Use readonly for immutable properties
- Leverage discriminated unions for type safety
- Use type guards for runtime type checking
- Implement proper null checking
- Avoid type assertions unless necessary
## Error Handling
- Do not proactively add error handling
- Handle Promise rejections properly
================================================
FILE: .gitattributes
================================================
# Mark generated protobuf/buf code as generated
# This makes GitHub collapse these files by default in PRs
app/generated/** linguist-generated=true
server/src/generated/** linguist-generated=true
================================================
FILE: .github/workflows/app-deploy.yml
================================================
# .github/workflows/app-deploy.yml
name: Run Migrations and Deploy App
on:
workflow_call:
inputs:
environment:
type: string
required: true
description: 'The environment to build for'
workflow_dispatch:
inputs:
environment:
type: string
required: true
description: 'The environment to build for'
permissions:
contents: read
id-token: write
jobs:
migrate-and-deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install jq
run: sudo apt-get update && sudo apt-get install -y jq
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::287641434880:role/ItoGitHubCiCdRole
aws-region: us-west-2
- name: Run migrations
run: |
aws lambda invoke \
--function-name ${{ vars.AWS_STAGE }}-ItoDb-migration \
--payload '{}' \
out.json
cat out.json
if jq -e '.FunctionError' out.json > /dev/null; then
echo "❌ Lambda execution failed:"
echo "Message: $(jq -r '.errorMessage' out.json)"
exit 1
fi
if jq -e '.errorType' out.json > /dev/null; then
echo "❌ Migration failed:"
echo "Error: $(jq -r '.errorType' out.json)"
echo "Message: $(jq -r '.errorMessage' out.json)"
exit 1
fi
- name: Force new ECS deployment
run: |
aws ecs update-service \
--cluster ${{ vars.AWS_STAGE }}-ito-cluster \
--service ${{ vars.AWS_STAGE }}-ito-service \
--force-new-deployment
================================================
FILE: .github/workflows/autolink-pr-to-issue.yml
================================================
name: Auto-link PR to Issue
on:
pull_request:
types: [opened]
jobs:
autolink:
runs-on: ubuntu-latest
steps:
- name: Extract issue number from branch name
id: extract_issue
run: |
BRANCH_NAME="${{ github.head_ref }}"
echo "Branch name: $BRANCH_NAME"
# Extract issue number from branch name pattern ito-XXX
if [[ $BRANCH_NAME =~ ^ito-([0-9]+) ]]; then
ISSUE_NUMBER="${BASH_REMATCH[1]}"
echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT
echo "Found issue number: $ISSUE_NUMBER"
else
echo "No issue number found in branch name"
echo "issue_number=" >> $GITHUB_OUTPUT
fi
- name: Link PR to Issue
if: steps.extract_issue.outputs.issue_number != ''
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_NUMBER: ${{ steps.extract_issue.outputs.issue_number }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
# Check if issue exists
ISSUE_EXISTS=$(gh api repos/heyito/ito/issues/$ISSUE_NUMBER --jq '.number' 2>/dev/null || echo "")
if [ -n "$ISSUE_EXISTS" ]; then
echo "Linking PR #$PR_NUMBER to issue #$ISSUE_NUMBER"
# Add comment to PR mentioning the issue (creates automatic link)
gh api repos/heyito/ito/issues/$PR_NUMBER/comments \
--method POST \
--field body="Resolves #$ISSUE_NUMBER"
echo "Successfully linked PR #$PR_NUMBER to issue #$ISSUE_NUMBER"
else
echo "Issue #$ISSUE_NUMBER does not exist, skipping autolink"
fi
================================================
FILE: .github/workflows/build-image.yml
================================================
name: Build and Push Docker Image
on:
workflow_call:
inputs:
environment:
type: string
required: true
description: 'The environment to build for'
workflow_dispatch:
inputs:
environment:
type: string
required: true
description: 'The environment to build for'
permissions:
contents: read
id-token: write
jobs:
build-and-push:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::287641434880:role/ItoGitHubCiCdRole
aws-region: us-west-2
- name: Login to ECR
run: |
aws ecr get-login-password \
| docker login \
--username AWS \
--password-stdin 287641434880.dkr.ecr.us-west-2.amazonaws.com
- name: Build & push multi-arch image
working-directory: server
run: |
docker buildx create --use
docker buildx build \
--platform linux/arm64,linux/amd64 \
--tag 287641434880.dkr.ecr.us-west-2.amazonaws.com/${{ vars.AWS_STAGE }}-ito-server:latest \
--push .
================================================
FILE: .github/workflows/build.yml
================================================
name: Build and Release
on:
release:
types: [published]
permissions:
contents: write
id-token: write # required for OIDC role assumption
jobs:
build-mac:
runs-on: macos-latest
environment: ${{ github.event.release.target_commitish == 'main' && 'production' || 'develop' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Bun
uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: 'x86_64-apple-darwin,aarch64-apple-darwin'
- name: Set up environment
run: |
echo "VITE_AUTH0_DOMAIN=\"${{ secrets.VITE_AUTH0_DOMAIN }}\"" >> .env
echo "VITE_AUTH0_CLIENT_ID=\"${{ secrets.VITE_AUTH0_CLIENT_ID }}\"" >> .env
echo "VITE_AUTH0_AUDIENCE=\"${{ secrets.VITE_AUTH0_AUDIENCE }}\"" >> .env
echo "VITE_POSTHOG_API_KEY=\"${{ secrets.VITE_POSTHOG_API_KEY }}\"" >> .env
echo "VITE_POSTHOG_HOST=\"${{ secrets.VITE_POSTHOG_HOST }}\"" >> .env
echo "VITE_GRPC_BASE_URL=\"${{ vars.VITE_GRPC_BASE_URL }}\"" >> .env
echo "VITE_UPDATER_BUCKET=\"${{ vars.VITE_UPDATER_BUCKET }}\"" >> .env
echo "VITE_SENTRY_DSN=\"${{ vars.VITE_SENTRY_DSN }}\"" >> .env
echo "VITE_SENTRY_ENV=\"${{ vars.VITE_SENTRY_ENV }}\"" >> .env
echo "VITE_SENTRY_TRACES_SAMPLE_RATE=\"${{ vars.VITE_SENTRY_TRACES_SAMPLE_RATE }}\"" >> .env
echo "VITE_SENTRY_PROFILES_SAMPLE_RATE=\"${{ vars.VITE_SENTRY_PROFILES_SAMPLE_RATE }}\"" >> .env
echo "VITE_ITO_VERSION=\"${GITHUB_REF#refs/tags/v}\"" >> .env
echo "ITO_ENV=\"${{ github.event.release.target_commitish == 'main' && 'prod' || 'dev' }}\"" >> .env
echo "VITE_ITO_ENV=\"${{ github.event.release.target_commitish == 'main' && 'prod' || 'dev' }}\"" >> .env
echo "APPLE_ID=\"${{ secrets.APPLE_ID }}\"" >> .env
echo "APPLE_TEAM_ID=\"${{ secrets.APPLE_TEAM_ID }}\"" >> .env
echo "APPLE_APP_SPECIFIC_PASSWORD=\"${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}\"" >> .env
echo "CSC_LINK=\"release.p12\"" >> .env
echo "CSC_KEY_PASSWORD=\"${{ secrets.MACOS_CERT_PASSWORD }}\"" >> .env
echo "GH_TOKEN=\"${{ secrets.GITHUB_TOKEN }}\"" >> .env
echo "GRPC_BASE_URL=\"${{ vars.GRPC_BASE_URL }}\"" >> .env
echo "Created .env file:"
cat .env
- name: Decode and install certificate
run: |
echo "${{ secrets.MACOS_CERT_BASE64 }}" | base64 --decode > release.p12
- name: Build and package macOS application
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: ./build-app.sh mac
- name: Upload Mac Installer DMG
uses: actions/upload-artifact@v4
with:
name: Ito-Mac-Installer
path: dist/Ito-${{ github.event.release.target_commitish == 'main' && 'Installer' || 'dev-Installer' }}.dmg
- name: Upload Mac Build Artifacts
uses: actions/upload-artifact@v4
with:
name: Mac-Build-Artifacts
path: |
dist/*.yml
dist/*universal-mac.zip
dist/*universal-mac.zip.blockmap
dist/*.dmg
dist/*.dmg.blockmap
build-windows-rust:
runs-on: windows-latest
environment: ${{ github.event.release.target_commitish == 'main' && 'production' || 'develop' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Rust with MSVC toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable-x86_64-pc-windows-msvc
targets: 'x86_64-pc-windows-msvc'
- name: Build Rust native modules with MSVC
shell: pwsh
run: |
# Find Visual Studio installation (GitHub Actions uses Enterprise)
$vsPath = if (Test-Path "C:\Program Files\Microsoft Visual Studio\2022\Enterprise") {
"C:\Program Files\Microsoft Visual Studio\2022\Enterprise"
} elseif (Test-Path "C:\Program Files\Microsoft Visual Studio\2022\Community") {
"C:\Program Files\Microsoft Visual Studio\2022\Community"
} else {
throw "Visual Studio 2022 not found"
}
Write-Host "Using Visual Studio at: $vsPath"
# Launch VS Developer PowerShell and build
& "$vsPath\Common7\Tools\Launch-VsDevShell.ps1" -Arch amd64
# Build each native module
$modules = @("global-key-listener", "audio-recorder", "text-writer", "active-application", "selected-text-reader")
foreach ($module in $modules) {
Write-Host "Building $module with MSVC..."
Set-Location "native\$module"
cargo build --release --target x86_64-pc-windows-msvc
Set-Location ..\..
}
- name: Prepare artifacts directory
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path rust-binaries
$modules = @("global-key-listener", "audio-recorder", "text-writer", "active-application", "selected-text-reader")
foreach ($module in $modules) {
$sourcePath = "native\target\x86_64-pc-windows-msvc\release\$module.exe"
$destPath = "rust-binaries\$module.exe"
if (Test-Path $sourcePath) {
Copy-Item $sourcePath $destPath
Write-Host "Copied $module.exe"
} else {
Write-Error "$sourcePath not found!"
exit 1
}
}
- name: Upload Rust binaries
uses: actions/upload-artifact@v4
with:
name: Windows-Rust-Binaries
path: rust-binaries/*.exe
build-windows:
runs-on: ubuntu-latest
needs: build-windows-rust
environment: ${{ github.event.release.target_commitish == 'main' && 'production' || 'develop' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download Rust binaries built with MSVC
uses: actions/download-artifact@v4
with:
name: Windows-Rust-Binaries
path: rust-binaries-msvc
- name: Place Rust binaries in expected locations
run: |
# Place binaries where electron-builder expects them
for module in global-key-listener audio-recorder text-writer active-application selected-text-reader; do
mkdir -p "native/target/x86_64-pc-windows-msvc/release"
cp "rust-binaries-msvc/$module.exe" "native/target/x86_64-pc-windows-msvc/release/"
echo "Placed $module.exe"
done
- name: Set up Bun
uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install
- name: Set up environment
run: |
echo "VITE_AUTH0_DOMAIN=\"${{ secrets.VITE_AUTH0_DOMAIN }}\"" >> .env
echo "VITE_AUTH0_CLIENT_ID=\"${{ secrets.VITE_AUTH0_CLIENT_ID }}\"" >> .env
echo "VITE_AUTH0_AUDIENCE=\"${{ secrets.VITE_AUTH0_AUDIENCE }}\"" >> .env
echo "VITE_POSTHOG_API_KEY=\"${{ secrets.VITE_POSTHOG_API_KEY }}\"" >> .env
echo "VITE_POSTHOG_HOST=\"${{ secrets.VITE_POSTHOG_HOST }}\"" >> .env
echo "VITE_GRPC_BASE_URL=\"${{ vars.VITE_GRPC_BASE_URL }}\"" >> .env
echo "VITE_UPDATER_BUCKET=\"${{ vars.VITE_UPDATER_BUCKET }}\"" >> .env
echo "VITE_SENTRY_DSN=\"${{ vars.VITE_SENTRY_DSN }}\"" >> .env
echo "VITE_SENTRY_ENV=\"${{ vars.VITE_SENTRY_ENV }}\"" >> .env
echo "VITE_SENTRY_TRACES_SAMPLE_RATE=\"${{ vars.VITE_SENTRY_TRACES_SAMPLE_RATE }}\"" >> .env
echo "VITE_SENTRY_PROFILES_SAMPLE_RATE=\"${{ vars.VITE_SENTRY_PROFILES_SAMPLE_RATE }}\"" >> .env
echo "VITE_ITO_VERSION=\"${GITHUB_REF#refs/tags/v}\"" >> .env
echo "ITO_ENV=\"${{ github.event.release.target_commitish == 'main' && 'prod' || 'dev' }}\"" >> .env
echo "VITE_ITO_ENV=\"${{ github.event.release.target_commitish == 'main' && 'prod' || 'dev' }}\"" >> .env
echo "GH_TOKEN=\"${{ secrets.GITHUB_TOKEN }}\"" >> .env
echo "GRPC_BASE_URL=\"${{ vars.GRPC_BASE_URL }}\"" >> .env
echo "Created .env file:"
cat .env
- name: Build and package Windows application (unsigned)
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: ./build-app.sh windows --skip-binaries
- name: Upload unsigned Windows artifacts for signing
uses: actions/upload-artifact@v4
with:
name: Windows-Unsigned-Artifacts
path: |
dist/*.exe
dist/*.yml
dist/*.nsis.7z
dist/*.zip
dist/*.blockmap
sign-windows:
runs-on: windows-latest
needs: build-windows
environment: ${{ github.event.release.target_commitish == 'main' && 'production' || 'develop' }}
steps:
- name: Download unsigned Windows artifacts
uses: actions/download-artifact@v4
with:
name: Windows-Unsigned-Artifacts
path: dist
- name: Sign Windows executable with Azure Trusted Signing
uses: Azure/trusted-signing-action@v0
with:
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
endpoint: https://eus.codesigning.azure.net/
trusted-signing-account-name: ${{ secrets.AZURE_CODE_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ secrets.AZURE_CODE_SIGNING_CERTIFICATE_PROFILE_NAME }}
files-folder: dist
files-folder-filter: exe
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
- name: Regenerate metadata after signing
shell: bash
run: |
# Find the signed executable file in the dist directory
# This searches for any file matching the pattern "Ito-*.exe"
SIGNED_EXE=$(find dist -name "Ito-*.exe" | head -1)
if [ -z "$SIGNED_EXE" ]; then
echo "Error: No signed executable found"
exit 1
fi
echo "Found signed executable: $SIGNED_EXE"
# Calculate the SHA512 checksum of the signed file
# cut -d' ' -f1 extracts just the hash part (before the filename)
NEW_CHECKSUM=$(sha512sum "$SIGNED_EXE" | cut -d' ' -f1)
# Convert hex checksum to base64 format (required by electron-updater)
# xxd -r -p converts hex string to raw bytes
# base64 -w 0 encodes to base64 without line wrapping
NEW_CHECKSUM_B64=$(echo -n "$NEW_CHECKSUM" | xxd -r -p | base64 -w 0)
# Get the file size in bytes using stat
FILE_SIZE=$(stat -c%s "$SIGNED_EXE")
# Extract just the filename from the full path
FILENAME=$(basename "$SIGNED_EXE")
echo "New checksum (hex): $NEW_CHECKSUM"
echo "New checksum (base64): $NEW_CHECKSUM_B64"
echo "File size: $FILE_SIZE"
echo "Filename: $FILENAME"
# Update the auto-updater metadata file with the signed file's information
# This is critical because signing changes the file's checksum
if [ -f "dist/latest.yml" ]; then
echo "Updating latest.yml with new checksum after signing"
# Update each field in latest.yml using sed (stream editor)
# The | delimiter is used instead of / to avoid conflicts with filenames
sed -i "s|url: .*|url: $FILENAME|" dist/latest.yml # Update download URL
sed -i "s|sha512: .*|sha512: $NEW_CHECKSUM_B64|" dist/latest.yml # Update checksum
sed -i "s|size: .*|size: $FILE_SIZE|" dist/latest.yml # Update file size
sed -i "s|path: .*|path: $FILENAME|" dist/latest.yml # Update path
echo "Updated latest.yml contents:"
cat dist/latest.yml
else
echo "Warning: latest.yml not found in dist directory"
ls -la dist/
fi
- name: Upload signed Windows artifacts
uses: actions/upload-artifact@v4
with:
name: Windows-Build-Artifacts
path: |
dist/*.yml
dist/*.exe
dist/*.nsis.7z
dist/*.zip
dist/*.blockmap
upload-to-s3:
runs-on: ubuntu-latest
needs: [build-mac, sign-windows]
environment: ${{ github.event.release.target_commitish == 'main' && 'production' || 'develop' }}
steps:
- name: Download Mac Build Artifacts
uses: actions/download-artifact@v4
with:
name: Mac-Build-Artifacts
path: mac-dist
- name: Download Windows Build Artifacts
uses: actions/download-artifact@v4
with:
name: Windows-Build-Artifacts
path: windows-dist
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::287641434880:role/ItoGitHubCiCdRole
aws-region: us-west-2
- name: Upload Build Output to S3
run: |
echo "Deploying version ${{ github.ref_name }} to S3"
BUCKET=s3://${{ vars.AWS_STAGE }}-ito-releases/releases
echo "Listing existing files in root of releases/"
aws s3 ls "$BUCKET/" | grep -vE '/$' | awk '{print $4}' > existing_files.txt
echo "Combining Mac and Windows artifacts"
mkdir -p combined-dist
# Copy Mac artifacts
if [ -d "mac-dist" ]; then
cp -r mac-dist/* combined-dist/
fi
# Copy Windows artifacts
if [ -d "windows-dist" ]; then
cp -r windows-dist/* combined-dist/
fi
echo "Files to upload:"
ls -la combined-dist/
echo "Identifying which files to delete post-upload"
find combined-dist -maxdepth 1 \( \
-name '*.yml' -o \
-name '*universal-mac.zip' -o \
-name '*universal-mac.zip.blockmap' -o \
-name '*.dmg' -o \
-name '*.dmg.blockmap' -o \
-name '*.exe' -o \
-name '*.nsis.7z' -o \
-name '*.zip' -o \
-name '*.blockmap' \
\) | xargs -I{} basename {} > uploaded_root_files.txt
echo "Uploading full combined dist to versioned folder: $BUCKET/${{ github.ref_name }}/"
aws s3 cp combined-dist $BUCKET/${{ github.ref_name }}/ --recursive
echo "Uploading selected files to root of releases/"
for FILE in \
$(find combined-dist -maxdepth 1 -name '*.yml') \
$(find combined-dist -maxdepth 1 -name '*universal-mac.zip') \
$(find combined-dist -maxdepth 1 -name '*universal-mac.zip.blockmap') \
$(find combined-dist -maxdepth 1 -name '*.dmg') \
$(find combined-dist -maxdepth 1 -name '*.dmg.blockmap') \
$(find combined-dist -maxdepth 1 -name '*.exe') \
$(find combined-dist -maxdepth 1 -name '*.nsis.7z') \
$(find combined-dist -maxdepth 1 -name '*.zip') \
$(find combined-dist -maxdepth 1 -name '*.blockmap')
do
if [ -f "$FILE" ]; then
aws s3 cp "$FILE" $BUCKET/
fi
done
# Compare and find stale files (exist in bucket but not in upload list)
# Exclude .blockmap files from deletion to preserve differential update capability
echo "Existing files in bucket root:"
cat existing_files.txt
echo ""
echo "Files uploaded to bucket root:"
cat uploaded_root_files.txt
echo ""
echo "Computing stale files (existing - uploaded)..."
comm -23 <(sort existing_files.txt) <(sort uploaded_root_files.txt) > all_stale_files.txt
echo "All stale files before filtering:"
cat all_stale_files.txt || echo "(none)"
echo ""
echo "Filtering out .blockmap files..."
grep -v '\.blockmap$' all_stale_files.txt > stale_files.txt || echo "(no non-blockmap stale files found)"
echo "Stale files identified for deletion (excluding .blockmap files for differential updates):"
cat stale_files.txt || echo "(none)"
# Delete each stale file (but preserve blockmaps)
while IFS= read -r file; do
echo "Deleting stale file: $file"
aws s3 rm "$BUCKET/$file"
done < stale_files.txt
- name: Invalidate CDN Cache for Release Files
run: |
echo "Invalidating cache for release files on distribution: ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }}. Updated files should be available within 15 minutes."
aws cloudfront create-invalidation \
--distribution-id "${{ vars.CLOUDFRONT_DISTRIBUTION_ID }}" \
--paths "/*.yml" "/*.dmg" "/*.exe" "/*.zip" "/*.blockmap"
- name: Upload installers to GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ github.ref_name }}
files: |
mac-dist/*.dmg
windows-dist/*.exe
================================================
FILE: .github/workflows/ci-controller.yml
================================================
name: CI Controller
on:
push:
branches:
- 'main'
- 'dev'
pull_request:
branches:
- '**'
jobs:
run-tests:
uses: ./.github/workflows/test-runner.yml
native-build-check:
uses: ./.github/workflows/native-build-check.yml
# This job ONLY runs for pushes to 'dev' and 'main'
deploy-server:
needs: run-tests
if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'dev')
uses: ./.github/workflows/deploy-server.yml
with:
# Dynamically set the environment based on the branch
environment: ${{ github.ref_name == 'main' && 'production' || 'develop' }}
secrets: inherit
================================================
FILE: .github/workflows/deploy-server.yml
================================================
name: Deploy Server Worfklow
on:
workflow_call:
inputs:
environment:
type: string
required: true
jobs:
paths-check:
runs-on: ubuntu-latest
outputs:
server_changed: ${{ steps.filter.outputs.server }}
infra_changed: ${{ steps.filter.outputs.infra }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch full history to allow comparing against previous commits
- name: Detect changed areas
id: filter
uses: dorny/paths-filter@v2
with:
base: ${{ github.event.before }}
filters: |
server:
- server/src/**
- server/Dockerfile
- server/package.json
infra:
- server/infra/**
list-files: 'none'
# Step 1: Build and push Docker image first (if server code changed)
# This ensures :latest tag is updated before CDK references it
build-image:
needs: paths-check
if: ${{ needs.paths-check.outputs.server_changed == 'true' }}
uses: ./.github/workflows/build-image.yml
with:
environment: ${{ inputs.environment }}
secrets: inherit
# Step 2: Deploy infrastructure (always runs if anything changed)
# If task definition changes, ECS will automatically restart with new :latest
deploy-infra:
needs:
- paths-check
- build-image
if: ${{ always() && (needs.paths-check.outputs.infra_changed == 'true' || needs.paths-check.outputs.server_changed == 'true') && (needs.build-image.result == 'success' || needs.build-image.result == 'skipped') }}
uses: ./.github/workflows/infra-deploy.yml
with:
environment: ${{ inputs.environment }}
secrets: inherit
# Step 3: Run migrations and force ECS deployment (only if server changed)
# Migration + force-new-deployment ensures ECS picks up new image even if task def didn't change
deploy-app:
needs:
- paths-check
- build-image
- deploy-infra
if: ${{ needs.paths-check.outputs.server_changed == 'true' }}
uses: ./.github/workflows/app-deploy.yml
with:
environment: ${{ inputs.environment }}
secrets: inherit
================================================
FILE: .github/workflows/infra-deploy.yml
================================================
name: Infra CDK
on:
workflow_call:
inputs:
environment:
type: string
required: true
description: 'The environment to build for'
workflow_dispatch:
inputs:
environment:
type: string
required: true
description: 'The environment to build for'
permissions:
contents: read
id-token: write # for OIDC
jobs:
cdk-deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Checkout infra
uses: actions/checkout@v3
- name: Setup Node.js & CDK
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install infra dependencies
working-directory: server/infra
run: npm install
- name: Verify CDK version
working-directory: server/infra
run: npx cdk --version
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::287641434880:role/ItoGitHubCiCdRole
aws-region: us-west-2
- name: Synthesize CDK template
working-directory: server/infra
run: npx cdk synth
- name: Show CDK diff
working-directory: server/infra
run: npx cdk diff
- name: Refresh AWS credentials before deploy
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::287641434880:role/ItoGitHubCiCdRole
aws-region: us-west-2
- name: Deploy to AWS
working-directory: server/infra
env:
AUTH0_DOMAIN: '${{ secrets.VITE_AUTH0_DOMAIN }}'
AUTH0_AUDIENCE: '${{ secrets.VITE_AUTH0_AUDIENCE }}'
AUTH0_CLIENT_ID: '${{ secrets.VITE_AUTH0_CLIENT_ID }}'
AUTH0_MGMT_CLIENT_ID: '${{ secrets.AUTH0_MGMT_CLIENT_ID }}'
AUTH0_MGMT_CLIENT_SECRET: '${{ secrets.AUTH0_MGMT_CLIENT_SECRET }}'
STRIPE_SECRET_KEY: '${{ secrets.STRIPE_SECRET_KEY }}'
STRIPE_WEBHOOK_SECRET: '${{ secrets.STRIPE_WEBHOOK_SECRET }}'
STRIPE_PRICE_ID: '${{ secrets.STRIPE_PRICE_ID }}'
APP_PROTOCOL: '${{ vars.APP_PROTOCOL }}'
STRIPE_PUBLIC_BASE_URL: '${{ vars.STRIPE_PUBLIC_BASE_URL }}'
run: npx cdk deploy "Ito-${{ vars.AWS_STAGE }}/*" --require-approval never
================================================
FILE: .github/workflows/native-build-check.yml
================================================
name: Native Build Check
on:
workflow_call:
jobs:
build-check-mac:
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: 'x86_64-apple-darwin,aarch64-apple-darwin'
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
native/target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Verify native binaries compile (x86_64)
working-directory: native
run: cargo build --workspace --release --target x86_64-apple-darwin
- name: Verify native binaries compile (aarch64)
working-directory: native
run: cargo build --workspace --release --target aarch64-apple-darwin
build-check-windows:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y mingw-w64
- name: Set up Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: 'x86_64-pc-windows-gnu'
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
native/target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Verify native binaries compile (Windows)
working-directory: native
run: cargo build --workspace --release --target x86_64-pc-windows-gnu
================================================
FILE: .github/workflows/test-runner.yml
================================================
name: Test Runner
on:
workflow_call:
jobs:
test:
runs-on: macos-latest
env:
ITO_ENV: dev
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Bun
uses: oven-sh/setup-bun@v1
- name: Set ITO_ENV based on branch
run: |
if [[ "${GITHUB_REF_NAME}" == "main" || "${GITHUB_BASE_REF}" == "main" ]]; then
echo "ITO_ENV=prod" >> "$GITHUB_ENV"
else
echo "ITO_ENV=dev" >> "$GITHUB_ENV"
fi
- name: Install dependencies
run: bun install && cd server && bun install && cd ..
- name: Run tests
run: bun runAllTests
- name: Check code formatting
run: bun format:app
- name: Check code linting
run: bun lint:app
- name: Check Rust formatting
run: bun format:native
- name: Check Rust linting
run: bun lint:native
================================================
FILE: .gitignore
================================================
out/*
release/*
node_modules
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
*/dist/*
dist/*
.DS_Store
native/**/target
native/target-rust-analyzer
native/**/.build
out.json
tsconfig.node.tsbuildinfo
server/deploy-dev.sh
# Sentry Config File
.env.sentry-build-plugin
.electron-builder-cache
.npm-cache
.cache
================================================
FILE: .prettierignore
================================================
node_modules/
dist/
build/
.next/
coverage/
out/
# Generated proto files
**/generated/
lib/generated/
app/generated/
server/src/generated/
# CDK outputs
server/infra/cdk.out/
server/infra/*.d.ts
server/infra/*.js
*.assets.json
*.template.json
# Build artifacts
*.min.js
*.bundle.js
# Dependencies
.pnpm-store/
bun.lockb
pnpm-lock.yaml
yarn.lock
package-lock.json
# Test coverage
.nyc_output/
# IDE and system files
.vscode/
.idea/
.DS_Store
*.swp
*.swo
# Logs
*.log
logs/
# Environment files
.env
.env.*
# Electron
.vite/
# Binary files
resources/binaries/
*.exe
*.dll
*.so
*.dylib
# Native build artifacts
native/**/.build/**
native/**/build/**
native/**/*.json
================================================
FILE: .prettierrc.json
================================================
{
"semi": false,
"singleQuote": true,
"arrowParens": "avoid",
"tabWidth": 2
}
================================================
FILE: .vscode/settings.json
================================================
{
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"rust-analyzer.linkedProjects": [
"./native/selected-text-reader/Cargo.toml",
"./native/text-writer/Cargo.toml",
"./native/audio-recorder/Cargo.toml",
"./native/active-application/Cargo.toml",
"./native/global-key-listener/Cargo.toml"
],
"rust-analyzer.cargo.targetDir": "${workspaceFolder}/native/target-rust-analyzer"
}
================================================
FILE: AGENTS.md
================================================
# Repository Guidelines
## Project Structure & Module Organization
- `app/` hosts the Electron renderer (React + Tailwind); `lib/` contains shared TypeScript modules, preload logic, and unit tests.
- `server/` holds the Bun-based transcription API, database migrations, and infrastructure scripts.
- `native/` includes Rust crates for keyboard, audio, and text bridges; rebuild via scripts as needed.
- `resources/` provides packaging assets (icons, updater configs), while `build/` and `out/` house generated installers; avoid editing build outputs.
- Use `scripts/` for automation and keep generated proto/constants under `lib/generated` (created by build steps).
## Build, Test, and Development Commands
- `bun install` aligns dependencies after pulls.
- `bun run dev` starts the Electron shell with live reload; `bun run dev:rust` rebuilds native bridges via `./build-binaries.sh` before launching.
- Packaging: `bun run build:app` for cross-platform; use `bun run build:mac` / `bun run build:win` for platform installers.
- Backend (`server/`): `bun install`, `bun run local-db-up` (Postgres), `bun run db:migrate`, `bun run dev`.
## Coding Style & Naming Conventions
- TypeScript + React across app/lib; server targets modern ECMAScript on Bun.
- Prettier (2-space indent) and ESLint enforce formatting. Run `bun run format` and `bun run lint` (or `*:app` variants) before submitting.
- Components/classes use `PascalCase`, hooks/utilities `camelCase`, constants `SCREAMING_SNAKE_CASE`. Co-locate Tailwind styles with components and reuse tokens via `lib/constants`.
- Always prefer console commands over log commands. E.g. use `console.log` instead of `log.info`.
## Testing Guidelines
- Unit tests run with Bun. Renderer/shared specs live in `lib/__tests__`; server tests reside under `server/src/**`. Name files with `.test.ts`.
- Run `bun run runLibTests`, `bun run runServerTests`, or `bun run runAllTests`; paste output in PR notes.
- Seed backend tests with `bun run local-db-up` and avoid hitting external services—mock microphone, OS, and network dependencies.
## Commit & Pull Request Guidelines
- Conventional commits are enforced via commitlint (example: `feat(app): add dictation overlay`). Scope by top-level folder.
- PRs need a summary, linked issue, test commands, and screenshots/GIFs for UI work. Call out schema/config updates and refresh `.env.example` files.
## Environment & Security Notes
- Copy `.env.example` (root) and `server/.env.example`; never commit credentials.
- After modifying protobufs or constants, run `bun run generate:constants` so `lib/generated` stays in sync with the app bundle.
================================================
FILE: CLAUDE.md
================================================
# Claude Context for ITO Project
## Project Overview
This is the ITO project - an AI assistant application with both client and server components.
## Project Structure
- `app/` - Client application code
- `server/` - Server-side code with gRPC services
- `server/src/ito.proto` - Protocol buffer definitions
- `server/src/clients/` - Various client implementations (Groq, LLM providers, etc.)
## Branch
Main development branch: `dev`
## Development Commands
- Dev: `bun dev` (starts electron-vite dev with watch)
- Server: `docker compose up --build` (run from server directory)
- Build: `bun build:app:mac` or `bun build:app:windows`
- Test: `bun runAllTests` (runs lib, server, and native tests)
- Lib tests: `bun runLibTests`
- Server tests: `bun runServerTests`
- Native tests: `bun runNativeTests` (or see "Native Binary Tests" section)
- Lint:
- TypeScript: `bun lint` (check) or `bun lint:fix` (fix)
- Rust: `bun lint:native` (check) or `bun lint:fix:native` (fix)
- Type check: `bun type-check`
- Format:
- TypeScript: `bun format` (check) or `bun format:fix` (fix)
- Rust: `bun format:native` (check) or `bun format:fix:native` (fix)
## Native Binary Tests
The `native/` directory contains Rust binaries that power the app's core functionality. The modules are organized as a Cargo workspace, allowing you to test and build all modules with a single command.
### Running Tests
Test all native modules:
```bash
cd native
cargo test --workspace
```
Or use the npm script:
```bash
bun runNativeTests
```
Test a single module:
```bash
cd native/global-key-listener
cargo test
```
### Native Modules
- `global-key-listener` - Keyboard event capture and hotkey management
- `audio-recorder` - Audio recording with sample rate conversion
- `text-writer` - Cross-platform text input simulation
- `active-application` - Active window detection
- `selected-text-reader` - Selected text extraction
### Linting and Formatting
Rust code follows standard formatting and linting rules defined in `native/`:
- **rustfmt.toml** - Code formatting configuration (100 char width, Unix line endings)
- **clippy.toml** - Linter configuration (cognitive complexity threshold)
- **Cargo.toml** - Workspace-level lint rules (pedantic + nursery warnings)
Run checks locally:
```bash
# Check formatting
bun format:native
# Auto-fix formatting
bun format:fix:native
# Check lints
bun lint:native
# Auto-fix lints (where possible)
bun lint:fix:native
```
### CI/CD
Native tests and builds are integrated into the existing CI workflows:
**Tests** (`.github/workflows/test-runner.yml`):
- Unit tests run on macOS runner (OS-agnostic tests)
- Runs automatically via `bun runAllTests` on all pushes and PRs
- Executed as part of the main CI controller workflow
**Compilation Checks** (`.github/workflows/native-build-check.yml`):
- macOS: Verifies compilation for x86_64 and aarch64 architectures
- Windows: Verifies cross-compilation for x86_64-pc-windows-gnu
- Runs automatically on all pushes and PRs via the CI controller
- Ensures binaries compile correctly for both platforms before merging
**Release Builds** (`.github/workflows/build.yml`):
- Full release compilation happens during tagged releases
- Also includes compilation verification before packaging
## Code Style Preferences
- Keep code as simple as possible
- Don't create overly long files
- Group related code into useful, well-named functions
- Prefer clean, readable code over complex solutions
- Follow existing patterns and conventions in the codebase
- Always prefer console commands over log commands. E.g. use `console.log` instead of `log.info`.
## Tech Stack
- TypeScript
- bun
- gRPC with Protocol Buffers
- React (for UI components)
- Various LLM providers (Groq, etc.)
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2024 Demox Labs
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
================================================
FILE: README.md
================================================
# [DEPRECATED] - This project is no longer maintained
# Ito
<div align="center">
<img src="resources/build/icon.png" width="128" height="128" alt="Ito Logo" />
<h3>Smart dictation. Everywhere you want.</h3>
<p>
<strong>Ito</strong> is an intelligent voice assistant that brings seamless voice dictation to any application on your computer. Simply hold down your trigger key, speak naturally, and watch your words appear instantly in any text field.
</p>
<p>
<img alt="macOS" src="https://img.shields.io/badge/macOS-supported-blue?logo=apple&logoColor=white">
<img alt="Windows" src="https://img.shields.io/badge/Windows-supported-blue?logo=windows&logoColor=white">
<img alt="Version" src="https://img.shields.io/badge/version-0.2.0-green">
<img alt="License" src="https://img.shields.io/badge/license-GPL-blue">
</p>
</div>
---
## ✨ Features
### 🎙️ **Universal Voice Dictation**
- **Works in any app**: Emails, documents, chat applications, web browsers, code editors
- **Global keyboard shortcuts**: Customizable trigger keys that work system-wide
- **Real-time transcription**: High-accuracy speech-to-text powered by advanced AI models
- **Instant text insertion**: Automatically types transcribed text into the focused text field
### 🧠 **Smart & Adaptive**
- **Custom dictionary**: Add technical terms, names, and specialized vocabulary
- **Context awareness**: Learns from your usage patterns to improve accuracy
- **Multi-language support**: Transcribe in multiple languages
- **Intelligent punctuation**: Automatically adds appropriate punctuation
### ⚙️ **Powerful Customization**
- **Flexible shortcuts**: Configure any key combination as your trigger
- **Audio preferences**: Choose your preferred microphone
- **Privacy controls**: Local processing options and data control settings
- **Seamless integration**: Works with any application
### 💾 **Data Management**
- **Notes system**: Automatically save transcriptions for later reference
- **Interaction history**: Track your dictation sessions and improve over time
- **Cloud sync**: Keep your settings and data synchronized across devices
- **Export capabilities**: Export your notes and interaction data
---
## 🚀 Quick Start
### Prerequisites
- **macOS 10.15+** or **Windows 10+**
- **Node.js 20+** and **Bun** (for development)
- **Rust toolchain** (for building native components)
- **Microphone access** and **Accessibility permissions**
### Installation
1. **Download the latest release** from [heyito.ai](https://www.heyito.ai/) or the [GitHub releases page](https://github.com/heyito/ito/releases)
2. **Install the application**:
- **macOS**: Open the `.dmg` file and drag Ito to Applications
- **Windows**: Run the `.exe` installer and follow the setup wizard
3. **Grant permissions** when prompted:
- **Microphone access**: Required for voice input
- **Accessibility access**: Required for global keyboard shortcuts and text insertion
4. **Set up authentication**:
- Sign in with Google, Apple, Github through Auth0 or create a local account
- Complete the guided onboarding process
### First Use
1. **Configure your trigger key**: Choose a comfortable keyboard shortcut (default: `Fn + Space`)
2. **Test your microphone**: Ensure clear audio input during the setup process
3. **Try it out**: Hold your trigger key and speak into any text field
4. **Customize settings**: Adjust voice sensitivity, shortcuts, and preferences
---
## 🛠️ Development
### Building from Source
> **Important**: Ito requires a local transcription server for voice processing. See [server/README.md](server/README.md) for detailed server setup instructions.
```bash
# Clone the repository
git clone https://github.com/heyito/ito.git
cd ito
# Install dependencies
bun install
# Set up environment variables
cp .env.example .env
# Build native components (Rust binaries)
./build-binaries.sh
# Set up and start the server (required for transcription)
cd server
cp .env.example .env # Edit with your API keys
bun install
bun run local-db-up # Start PostgreSQL database
bun run db:migrate # Run database migrations
bun run dev # Start development server
cd ..
# Start the Electron app (in a new terminal)
bun run dev
```
### Build Requirements
#### All Platforms
- **Rust**: Install via [rustup.rs](https://rustup.rs/)
- **Windows users**: See Windows-specific instructions below for GNU toolchain setup
- **macOS/Linux users**: Default installation is sufficient
#### macOS
- **Xcode Command Line Tools**: `xcode-select --install`
#### Windows
**Required Setup:**
This setup uses git bash for shell operations. Download from [git](https://git-scm.com/downloads)
1. **Install Docker Desktop**: Download from [docker.com](https://www.docker.com/products/docker-desktop/) and ensure it's running
2. **Install Rust** (with GNU target)
Download and run the official [Rust installer for Windows](https://rustup.rs/).
This installs `rustup` and the MSVC toolchain by default.
Add the GNU target (needed for our native components):
rustup toolchain install stable-x86_64-pc-windows-gnu
rustup target add x86_64-pc-windows-gnu
---
3. **Install 7-Zip**
winget install 7zip.7zip
---
4. **Install GCC & MinGW-w64 via MSYS2**
Install [MSYS2](https://www.msys2.org/).
Open the **MSYS2 MinGW x64** shell (from the Start Menu).
Update and install the toolchain:
pacman -Syu # run twice if asked to restart
pacman -S --needed mingw-w64-x86_64-toolchain
Verify the tools exist:
ls /mingw64/bin/gcc.exe /mingw64/bin/dlltool.exe
---
5. **Use the MinGW tools when building** (Git Bash)
You normally develop and build in **Git Bash**. Before building, prepend the MinGW path:
export PATH="/c/msys64/mingw64/bin:$PATH"
export DLLTOOL="/c/msys64/mingw64/bin/dlltool.exe"
export CC_x86_64_pc_windows_gnu="/c/msys64/mingw64/bin/x86_64-w64-mingw32-gcc.exe"
export AR_x86_64_pc_windows_gnu="/c/msys64/mingw64/bin/ar.exe"
export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER="/c/msys64/mingw64/bin/x86_64-w64-mingw32-gcc.exe"
Check you’re picking up the right ones:
which gcc # -> /c/msys64/mingw64/bin/gcc.exe
which dlltool # -> /c/msys64/mingw64/bin/dlltool.exe
⚠️ **Do not add `C:\msys64\ucrt64\bin` to PATH.** That’s the wrong runtime and will break linking.
💡 To avoid running these exports every session, add the lines above to your Git Bash `~/.bashrc` file. They will be applied automatically whenever you open a new Git Bash window.
---
6. **Restart Git Bash if you update MSYS2**
Whenever you update MSYS2 packages with `pacman -Syu`, restart Git Bash so the changes take effect.
> **Note**: Windows builds use Docker for cross-compilation to ensure consistent builds. The Docker container handles the Windows build environment automatically.
### Project Structure
```
ito/
├── app/ # Electron renderer (React frontend)
│ ├── components/ # React components
│ ├── store/ # Zustand state management
│ └── styles/ # TailwindCSS styles
├── lib/ # Shared library code
│ ├── main/ # Electron main process
│ ├── preload/ # Preload scripts & IPC
│ └── media/ # Audio/keyboard native interfaces
├── native/ # Native components (Rust/Swift)
│ ├── audio-recorder/ # Audio capture (Rust)
│ ├── global-key-listener/ # Keyboard events (Rust)
│ ├── text-writer/ # Text insertion (Rust)
│ └── active-application/ # Get the active application for context (Rust)
├── server/ # gRPC transcription server
│ ├── src/ # Server implementation
│ └── infra/ # AWS infrastructure (CDK)
└── resources/ # Build resources & assets
```
### Available Scripts
```bash
# Development
bun run dev # Start with hot reload
bun run dev:rust # Build Rust components and start dev
# Building Native Components
bun run build:rust # Build for current platform
bun run build:rust:mac # Build for macOS (with universal binary)
bun run build:rust:win # Build for Windows
# Building Application
bun run build:mac # Build for macOS
bun run build:win # Build for Windows
./build-app.sh mac # Build macOS using build script
./build-app.sh windows # Build Windows using build script (requires Docker)
# Code Quality
bun run lint # Run ESLint
bun run format # Run Prettier
bun run lint:fix # Fix linting issues
```
---
## 🏗️ Architecture
### Client Architecture
**Ito** is built as a modern Electron application with a sophisticated multi-process architecture:
- **Main Process**: Handles system integration, permissions, and native component coordination
- **Renderer Process**: React-based UI with real-time audio visualization
- **Preload Scripts**: Secure IPC bridge between main and renderer processes
- **Native Components**: High-performance Rust binaries for audio capture and keyboard handling
### Technology Stack
**Frontend:**
- **Electron** - Cross-platform desktop framework
- **React 19** - Modern UI library with concurrent features
- **TypeScript** - Type-safe development
- **TailwindCSS** - Utility-first styling
- **Zustand** - Lightweight state management
- **Framer Motion** - Smooth animations
**Backend:**
- **Node.js** - Runtime environment
- **gRPC** - High-performance RPC for transcription services
- **SQLite** - Local data storage
- **Protocol Buffers** - Efficient data serialization
**Native Components:**
- **Rust** - System-level audio recording and keyboard event handling
- **Swift** - macOS-specific text manipulation and accessibility features
- **cpal** - Cross-platform audio library
- **enigo** - Cross-platform input simulation
**Infrastructure:**
- **AWS CDK** - Infrastructure as code
- **Docker** - Containerized deployments
- **Auth0** - Authentication and user management
### Communication Flow
```mermaid
graph TD
A[User Holds Trigger Key] --> B[Global Key Listener]
B --> C[Main Process]
C --> D[Audio Recorder Service]
D --> E[gRPC Transcription Service]
E --> F[AI Transcription Model]
F --> G[Transcribed Text]
G --> H[Text Writer Service]
H --> I[Active Text Field]
```
---
## 🔧 Configuration
### Keyboard Shortcuts
Customize your trigger keys in **Settings > Keyboard**:
- **Single key**: `Space`, `Fn`, etc.
- **Key combinations**: `Cmd + Space`, `Ctrl + Shift + V`, etc.
- **Complex shortcuts**: `Fn + Cmd + Space` for advanced workflows
### Audio Settings
Fine-tune audio capture in **Settings > Audio**:
- **Microphone selection**: Choose from available input devices
- **Sensitivity adjustment**: Optimize for your voice and environment
- **Noise reduction**: Filter background noise automatically
- **Audio feedback**: Enable/disable sound effects
### Privacy & Data
Control your data in **Settings > General**:
- **Local processing**: Keep voice data on your device
- **Cloud sync**: Synchronize settings across devices
- **Analytics**: Share anonymous usage data (optional)
- **Data export**: Download your notes and interaction history
---
## 🔒 Privacy & Security
### Data Handling
- **Local-enabled**: Voice processing can be done entirely on your device or using our cloud
- **Encrypted transmission**: All network communication uses TLS encryption
- **Minimal data collection**: Only essential data is processed and stored
- **User control**: Full control and transparency over data retention and deletion
### Permissions
**Ito** requires specific system permissions to function:
- **Microphone Access**: To capture your voice for transcription
- **Accessibility Access**: To detect keyboard shortcuts and insert text
- **Network Access**: For cloud features and updates (optional)
### Open Source
This project is open source under the GNU General Public License. You can:
- Audit the source code for security and privacy
- Contribute improvements and bug fixes
- Fork and customize for your specific needs
- Report security issues through responsible disclosure
---
## 🤝 Contributing
We welcome contributions! Whether you're fixing bugs, adding features, or improving documentation, your help makes **Ito** better for everyone.
### Getting Started
1. **Fork the repository** and clone your fork
2. **Create a feature branch** from `dev`
3. **Make your changes** with clear commit messages
4. **Test thoroughly** across supported platforms
5. **Submit a pull request** with a detailed description
### Development Guidelines
- **Code Style**: Use Prettier and ESLint configurations
- **Type Safety**: Maintain strong TypeScript typing
- **Testing**: Add tests for new features
- **Documentation**: Update docs for API changes
- **Performance**: Consider impact on time between recording and text insertion
### Areas for Contribution
- **Accuracy improvements**: Better transcription algorithms
- **Language support**: Additional language models
- **UI/UX enhancements**: Better user experience
- **Platform support**: Windows stability testing, Linux compatibility
- **Documentation**: Tutorials, guides, and examples
---
## 📄 License
This project is licensed under the **GNU General Public License** - see the [LICENSE](LICENSE) file for details.
---
## 🙏 Acknowledgments
**Ito** is built with and inspired by amazing open source projects:
- **[Electron React App](https://github.com/guasam/electron-react-app)** by @guasam - The foundational template that provided our modern Electron + React architecture
- **Electron** - Cross-platform desktop apps with web technologies
- **React** - Modern UI development
- **Rust** - Systems programming language for native components
- **gRPC** - High-performance RPC framework
- **TailwindCSS** - Utility-first CSS framework
---
## 📞 Support
- **Community**: [GitHub Discussions](https://github.com/heyito/ito/discussions)
- **Issues**: [GitHub Issues](https://github.com/heyito/ito/issues)
- **Website**: [heyito.ai](https://www.heyito.ai)
================================================
FILE: app/app.tsx
================================================
import { HashRouter, Routes, Route } from 'react-router-dom'
import appIcon from '@/resources/build/icon.png'
import HomeKit from '@/app/components/home/HomeKit'
import WelcomeKit from '@/app/components/welcome/WelcomeKit'
import Pill from '@/app/components/pill/Pill'
import {
STEP_NAMES,
STEP_NAMES_ARRAY,
useOnboardingStore,
} from '@/app/store/useOnboardingStore'
import { useAuth } from '@/app/components/auth/useAuth'
import { WindowContextProvider } from '@/lib/window'
import { Auth0Provider } from '@/app/components/auth/Auth0Provider'
import { useDeviceChangeListener } from './hooks/useDeviceChangeListener'
import { verifyStoredMicrophone } from './media/microphone'
import { useEffect } from 'react'
const MainApp = () => {
const { onboardingCompleted, onboardingStep } = useOnboardingStore()
const { isAuthenticated } = useAuth()
useDeviceChangeListener()
useEffect(() => {
verifyStoredMicrophone()
}, [])
const onboardingSetupCompleted =
onboardingStep >= STEP_NAMES_ARRAY.indexOf(STEP_NAMES.TRY_IT_OUT)
const shouldEnableShortcutGlobally =
onboardingCompleted || onboardingSetupCompleted
// If authenticated and onboarding completed, show main app
if (isAuthenticated && onboardingCompleted) {
window.api.send(
'electron-store-set',
'settings.isShortcutGloballyEnabled',
shouldEnableShortcutGlobally,
)
return <HomeKit />
}
// If authenticated but onboarding not completed, continue onboarding
window.api.send(
'electron-store-set',
'settings.isShortcutGloballyEnabled',
shouldEnableShortcutGlobally,
)
return <WelcomeKit />
}
export default function App() {
return (
<Auth0Provider>
<HashRouter>
<Routes>
{/* Route for the pill window */}
<Route
path="/pill"
element={
<>
<Pill />
</>
}
/>
{/* Default route for the main application window */}
<Route
path="/"
element={
<>
<WindowContextProvider
titlebar={{ title: 'Ito', icon: appIcon }}
>
<MainApp />
</WindowContextProvider>
</>
}
/>
</Routes>
</HashRouter>
</Auth0Provider>
)
}
================================================
FILE: app/assets/.gitignore
================================================
================================================
FILE: app/components/analytics/index.ts
================================================
import posthog from 'posthog-js'
import log from 'electron-log'
import { STORE_KEYS } from '../../../lib/constants/store-keys'
import { v4 as uuidv4 } from 'uuid'
import type { OnboardingCategory } from '../../store/useOnboardingStore'
// Get or generate a machine-based device ID that's shared across all windows
const getSharedDeviceId = async (): Promise<string> => {
try {
// Just request the device ID - main process handles generation/caching
const deviceId = await window.api?.invoke('analytics:get-device-id')
if (deviceId) {
console.log('[Analytics] Using machine-based device ID:', deviceId)
return deviceId
}
throw new Error('No device ID returned from main process')
} catch (error) {
log.error('[Analytics] Could not get machine device ID:', error)
// In true emergency, generate a temporary UUID as fallback
return uuidv4()
}
}
// Check if analytics should be enabled
const getAnalyticsEnabled = (): boolean => {
if (!import.meta.env.VITE_POSTHOG_API_KEY) {
console.warn('[Analytics] No PostHog API key found, analytics disabled')
return false
}
try {
const settings = window.electron?.store?.get(STORE_KEYS.SETTINGS)
return settings?.shareAnalytics ?? true
} catch (error) {
console.warn(
'[Analytics] Could not read settings, defaulting to enabled:',
error,
)
return true
}
}
const initPostHog = () => {
const isPill =
typeof window !== 'undefined' &&
typeof window.location !== 'undefined' &&
typeof window.location.hash === 'string' &&
window.location.hash.startsWith('#/pill')
posthog.init(import.meta.env.VITE_POSTHOG_API_KEY, {
api_host: import.meta.env.VITE_POSTHOG_HOST,
disable_session_recording: true,
disable_surveys: true,
advanced_disable_decide: true,
persistence: 'cookie',
// Disable default web auto-capture and pageviews for the pill window only
autocapture: !isPill,
capture_pageview: !isPill,
sanitize_properties: (props: Record<string, unknown>) => {
const p = { ...props }
delete (p as any).$current_url
delete (p as any).$pathname
delete (p as any).$host
delete (p as any).$referrer
return p
},
})
}
// Initialize PostHog only if analytics is enabled
let isAnalyticsInitialized = false
let sharedDeviceId: string | null = null
const analyticsEnabled = getAnalyticsEnabled()
console.log('[Analytics] Analytics enabled:', analyticsEnabled)
// Initialize PostHog asynchronously
const initializeAnalytics = async () => {
if (!analyticsEnabled) {
console.log('[Analytics] PostHog disabled by user settings')
return
}
try {
sharedDeviceId = await getSharedDeviceId()
console.log('[Analytics] Using shared device ID:', sharedDeviceId)
initPostHog()
if (sharedDeviceId) {
posthog.register({ device_id: sharedDeviceId })
}
// Attempt to resolve and alias install token to website distinct id
try {
const result = await window.api?.invoke('analytics:resolve-install-token')
if (result && result.success && result.websiteDistinctId) {
try {
posthog.alias(result.websiteDistinctId)
console.log(
'[Analytics] Aliased to website distinct_id from install token',
)
} catch (aliasErr) {
log.warn('[Analytics] alias() failed:', aliasErr)
}
}
} catch (err) {
log.warn('[Analytics] resolve-install-token failed:', err)
}
isAnalyticsInitialized = true
// Update the service instance after successful initialization
analytics.updateInitializationStatus(true, sharedDeviceId)
console.log(
'[Analytics] PostHog initialized with shared device ID:',
sharedDeviceId,
)
} catch (error) {
log.error('[Analytics] Failed to initialize analytics:', error)
}
}
// Initialize analytics when the module loads
initializeAnalytics()
// Event types for type safety
export interface BaseEventProperties {
timestamp?: string
session_id?: string
[key: string]: any
}
export interface OnboardingEventProperties extends BaseEventProperties {
step: number
step_name: string
category: OnboardingCategory
total_steps: number
referral_source?: string
provider?: string
}
export interface HotkeyEventProperties extends BaseEventProperties {
action: 'press' | 'release'
keys: string[]
duration_ms?: number
session_duration_ms?: number
}
export interface AuthEventProperties extends BaseEventProperties {
provider: string
is_returning_user: boolean
user_id?: string
}
export interface SettingsEventProperties extends BaseEventProperties {
setting_name: string
old_value: any
new_value: any
setting_category: string
}
export interface UserProperties {
user_id: string
email?: string
name?: string
provider?: string
created_at?: string
last_active?: string
onboarding_completed?: boolean
referral_source?: string
keyboard_shortcuts?: string[]
}
// Event constants
export const ANALYTICS_EVENTS = {
// Onboarding events
ONBOARDING_STARTED: 'onboarding_started',
ONBOARDING_STEP_COMPLETED: 'onboarding_step_completed',
ONBOARDING_STEP_VIEWED: 'onboarding_step_viewed',
ONBOARDING_COMPLETED: 'onboarding_completed',
ONBOARDING_ABANDONED: 'onboarding_abandoned',
// Authentication events
AUTH_SIGNUP_STARTED: 'auth_signup_started',
AUTH_SIGNUP_COMPLETED: 'auth_signup_completed',
AUTH_SIGNIN_STARTED: 'auth_signin_started',
AUTH_SIGNIN_COMPLETED: 'auth_signin_completed',
AUTH_SIGNIN_FAILED: 'auth_signin_failed',
AUTH_LOGOUT: 'auth_logout',
AUTH_LOGOUT_FAILED: 'auth_logout_failed',
AUTH_STATE_GENERATION_FAILED: 'auth_state_generation_failed',
AUTH_METHOD_FAILED: 'auth_method_failed',
// Recording events
RECORDING_STARTED: 'recording_started',
RECORDING_COMPLETED: 'recording_completed',
MANUAL_RECORDING_STARTED: 'manual_recording_started',
MANUAL_RECORDING_COMPLETED: 'manual_recording_completed',
MANUAL_RECORDING_ABANDONED: 'manual_recording_abandoned',
// Settings events
SETTING_CHANGED: 'setting_changed',
MICROPHONE_CHANGED: 'microphone_changed',
KEYBOARD_SHORTCUTS_CHANGED: 'keyboard_shortcuts_changed',
} as const
export type AnalyticsEvent =
(typeof ANALYTICS_EVENTS)[keyof typeof ANALYTICS_EVENTS]
/**
* Professional Analytics Service for Ito
* Handles all analytics tracking with proper typing and error handling
*/
class AnalyticsService {
private isInitialized: boolean = isAnalyticsInitialized
private currentUserId: string | null = null
private currentProvider: string | null = null
private sessionStartTime: number = Date.now()
private deviceId: string | null = null
constructor() {
// Device ID will be set after async initialization
this.deviceId = sharedDeviceId
console.log(
`[Analytics] Service initialized (enabled: ${this.isInitialized}, deviceId: ${this.deviceId || 'pending'})`,
)
}
/**
* Enable analytics (re-initialize if needed)
*/
async enableAnalytics() {
if (!this.isInitialized && import.meta.env.VITE_POSTHOG_API_KEY) {
try {
const deviceId = await getSharedDeviceId()
this.deviceId = deviceId
initPostHog()
if (deviceId) {
posthog.register({ device_id: deviceId })
}
this.isInitialized = true
console.log(
'[Analytics] Analytics enabled and initialized with shared device ID:',
deviceId,
)
} catch (error) {
log.error('[Analytics] Failed to enable analytics:', error)
}
}
}
/**
* Disable analytics
*/
disableAnalytics() {
this.isInitialized = false
this.currentUserId = null
this.currentProvider = null
try {
posthog.opt_out_capturing()
} catch (error) {
log.warn('[Analytics] Failed to opt-out capturing:', error)
}
console.log('[Analytics] Analytics disabled')
}
/**
* Check if analytics is currently enabled
*/
isEnabled(): boolean {
return this.isInitialized
}
/**
* Set user identification and properties
*/
identifyUser(
userId: string,
properties: Partial<UserProperties> = {},
provider?: string,
) {
console.log('identifyUser', userId, properties, provider)
// Store provider information
if (provider) {
this.currentProvider = provider
}
if (!this.shouldTrack()) {
console.log(
'[Analytics] User identification skipped - analytics disabled or self-hosted user',
)
return
}
try {
if (this.currentUserId !== userId) {
this.currentUserId = userId
const props = {
user_id: userId,
last_active: new Date().toISOString(),
...properties,
}
posthog.identify(userId, props)
console.log(
`[Analytics] User identified: ${userId} (deviceId: ${this.deviceId || 'pending'})`,
)
} else if (Object.keys(properties).length > 0) {
posthog.identify(undefined, {
...properties,
last_active: new Date().toISOString(),
})
}
} catch (error) {
log.error('[Analytics] Failed to identify user:', error)
}
}
/**
* Update user properties
*/
updateUserProperties(properties: Partial<UserProperties>) {
if (!this.shouldTrack() || !this.currentUserId) {
console.log(
'[Analytics] User properties update skipped - analytics disabled, self-hosted user, or user not identified',
)
return
}
try {
posthog.identify(undefined, properties)
console.log('[Analytics] User properties updated')
} catch (error) {
log.error('[Analytics] Failed to update user properties:', error)
}
}
/**
* Track a generic event
*/
track(eventName: AnalyticsEvent, properties: BaseEventProperties = {}) {
if (!this.shouldTrack()) {
return
}
try {
const eventProperties = {
timestamp: new Date().toISOString(),
session_duration_ms: Date.now() - this.sessionStartTime,
...properties,
}
posthog.capture(eventName, {
...eventProperties,
...(this.currentUserId ? { user_id: this.currentUserId } : {}),
})
console.log(
`[Analytics] Event tracked: ${eventName} (deviceId: ${this.deviceId || 'pending'}, userId: ${this.currentUserId || 'anonymous'})`,
)
} catch (error) {
log.error(`[Analytics] Failed to track event ${eventName}:`, error)
}
}
/**
* Track onboarding events
*/
trackOnboarding(
eventName: Extract<
AnalyticsEvent,
| 'onboarding_started'
| 'onboarding_step_completed'
| 'onboarding_step_viewed'
| 'onboarding_completed'
| 'onboarding_abandoned'
>,
properties: OnboardingEventProperties,
) {
console.log('trackOnboarding', eventName, properties)
this.track(eventName, properties)
}
/**
* Track authentication events
*/
trackAuth(
eventName: Extract<
AnalyticsEvent,
| 'auth_signup_started'
| 'auth_signup_completed'
| 'auth_signin_started'
| 'auth_signin_completed'
| 'auth_logout'
>,
properties: AuthEventProperties,
) {
this.track(eventName, properties)
}
/**
* Track settings changes
*/
trackSettings(
eventName: Extract<
AnalyticsEvent,
| 'setting_changed'
| 'microphone_changed'
| 'keyboard_shortcut_changed'
| 'privacy_mode_toggled'
| 'keyboard_shortcuts_changed'
>,
properties: SettingsEventProperties,
) {
this.track(eventName, properties)
}
/**
* Track permission events
*/
trackPermission(
eventName: Extract<
AnalyticsEvent,
'permission_requested' | 'permission_granted' | 'permission_denied'
>,
permissionType: 'microphone' | 'accessibility',
properties: BaseEventProperties = {},
) {
this.track(eventName, {
permission_type: permissionType,
...properties,
})
}
/**
* Reset analytics (for logout)
*/
resetUser() {
if (!this.isInitialized) {
console.log('[Analytics] User reset skipped - analytics disabled')
return
}
try {
posthog.reset()
this.currentUserId = null
this.currentProvider = null
console.log('[Analytics] User session reset')
} catch (error) {
log.error('[Analytics] Failed to reset user session:', error)
}
}
/**
* Get current session duration
*/
getSessionDuration(): number {
return Date.now() - this.sessionStartTime
}
/**
* Check if user is identified
*/
isUserIdentified(): boolean {
return this.currentUserId !== null
}
/**
* Get the current device ID
*/
getDeviceId(): string | null {
return this.deviceId
}
/**
* Update initialization status (called after async initialization completes)
*/
updateInitializationStatus(isInitialized: boolean, deviceId: string | null) {
this.isInitialized = isInitialized
this.deviceId = deviceId
console.log(
`[Analytics] Service status updated (enabled: ${this.isInitialized}, deviceId: ${this.deviceId})`,
)
}
/**
* Check if analytics should be tracked based on provider
*/
private shouldTrack(): boolean {
if (!this.isInitialized) {
return false
}
// Skip tracking for self-hosted users
if (this.currentProvider === 'self-hosted') {
console.log('[Analytics] Tracking skipped - self-hosted user')
return false
}
return true
}
}
// Export singleton instance
export const analytics = new AnalyticsService()
// Function to update analytics based on settings change
export const updateAnalyticsFromSettings = (shareAnalytics: boolean) => {
if (shareAnalytics && !analytics.isEnabled()) {
analytics.enableAnalytics()
console.log('[Analytics] Analytics enabled by settings change')
} else if (!shareAnalytics && analytics.isEnabled()) {
analytics.disableAnalytics()
console.log('[Analytics] Analytics disabled by settings change')
}
}
// Export convenience functions
export const trackEvent = analytics.track.bind(analytics)
export const identifyUser = analytics.identifyUser.bind(analytics)
export const updateUserProperties =
analytics.updateUserProperties.bind(analytics)
export const resetAnalytics = analytics.resetUser.bind(analytics)
================================================
FILE: app/components/auth/Auth0Provider.tsx
================================================
import React from 'react'
import { Auth0Provider as Auth0ReactProvider } from '@auth0/auth0-react'
import { Auth0Config, validateAuth0Config } from '../../../lib/auth/config'
interface Auth0ProviderProps {
children: React.ReactNode
}
export const Auth0Provider: React.FC<Auth0ProviderProps> = ({ children }) => {
// Validate configuration on startup
React.useEffect(() => {
try {
validateAuth0Config()
} catch (error) {
console.error('Auth0 configuration error:', error)
}
}, [])
return (
<Auth0ReactProvider
domain={Auth0Config.domain}
clientId={Auth0Config.clientId}
authorizationParams={{
redirect_uri: Auth0Config.redirectUri,
scope: Auth0Config.scope,
}}
useRefreshTokens={Auth0Config.useRefreshTokens}
cacheLocation={Auth0Config.cacheLocation}
>
{children}
</Auth0ReactProvider>
)
}
export default Auth0Provider
================================================
FILE: app/components/auth/useAuth.ts
================================================
import { useAuth0 } from '@auth0/auth0-react'
import { useCallback, useEffect, useMemo } from 'react'
import { Auth0Connections, Auth0Config } from '../../../lib/auth/config'
import { useAuthStore } from '../../store/useAuthStore'
import { type AuthUser, type AuthTokens } from '../../../lib/main/store'
import { useMainStore } from '@/app/store/useMainStore'
import { analytics, ANALYTICS_EVENTS } from '../analytics'
import { STORE_KEYS } from '../../../lib/constants/store-keys'
import { useOnboardingStore } from '@/app/store/useOnboardingStore'
export const useAuth = () => {
const {
logout,
user,
isAuthenticated: auth0IsAuthenticated,
isLoading: auth0IsLoading,
error: auth0Error,
getAccessTokenSilently,
getIdTokenClaims,
} = useAuth0()
// Get auth state from our store
const {
isAuthenticated: storeIsAuthenticated,
user: storeUser,
tokens,
isLoading: storeIsLoading,
error: storeError,
clearAuth,
setSelfHostedMode,
} = useAuthStore()
// Combine Auth0 and store state - prioritize store state for external auth
const isAuthenticated = storeIsAuthenticated || auth0IsAuthenticated
const isLoading = storeIsLoading || auth0IsLoading
const error = storeError || auth0Error
// Convert Auth0 user to our user interface
const auth0User: AuthUser | null = useMemo(() => {
if (!user) return null
return {
id: user.sub || '',
email: user.email,
name: user.name,
picture: user.picture,
provider: user.sub?.includes('|') ? user.sub.split('|')[0] : 'unknown',
lastSignInAt: new Date().toISOString(), // Only updated when user object changes
}
}, [user]) // Dependency array now correctly includes 'user'
// Prioritize store user over Auth0 user
const authUser = storeUser || auth0User
// Hydrate per-user onboarding state helper
async function hydrateOnboardingState(context?: string): Promise<void> {
try {
const saved = await window.api.getOnboardingState?.()
const onboarding = useOnboardingStore.getState()
if (saved?.onboardingCompleted) {
onboarding.setOnboardingCompleted()
} else {
onboarding.resetOnboarding()
onboarding.incrementOnboardingStep()
}
} catch (e) {
const suffix = context ? ` (${context})` : ''
console.warn(`[useAuth] Failed to hydrate onboarding state${suffix}:`, e)
}
}
// Check for token expiration on startup
useEffect(() => {
// Check if we have valid auth state stored
const storedAuth = window.electron?.store?.get(STORE_KEYS.AUTH)
const hasStoredTokens = storedAuth?.tokens?.access_token
// Also check main store for tokens (backwards compatibility)
const hasMainStoreToken = window.electron?.store?.get(
STORE_KEYS.ACCESS_TOKEN,
)
if ((hasStoredTokens || hasMainStoreToken) && !isAuthenticated) {
console.log('Detected expired tokens on startup, clearing auth state')
// Clear any remaining auth data
clearAuth(true)
// Track the automatic logout
const currentUser = authUser
if (currentUser) {
analytics.trackAuth(ANALYTICS_EVENTS.AUTH_LOGOUT, {
provider: currentUser.provider || 'unknown',
is_returning_user: true,
user_id: currentUser.id,
complete_signout: false,
session_duration_ms: analytics.getSessionDuration(),
reason: 'token_expired_startup',
})
}
}
}, [isAuthenticated, authUser, clearAuth])
useEffect(() => {
if (authUser) {
analytics.identifyUser(
authUser.id,
{
user_id: authUser.id,
email: authUser.email,
name: authUser.name,
provider: authUser.provider,
created_at: authUser.lastSignInAt,
},
authUser.provider,
)
// Notify pill window of user authentication
if (window.api?.notifyUserAuthUpdate) {
window.api.notifyUserAuthUpdate({
id: authUser.id,
email: authUser.email,
name: authUser.name,
provider: authUser.provider,
})
}
} else {
// Notify pill window that user is not authenticated (logout/reset)
if (window.api?.notifyUserAuthUpdate) {
console.log('[useAuth] Notifying pill window of user reset')
window.api.notifyUserAuthUpdate(null)
}
}
}, [
authUser,
auth0IsAuthenticated,
auth0User,
storeIsAuthenticated,
storeUser,
])
// Handle auth code from protocol URL - only set up listener once globally
useEffect(() => {
if (!window.api?.on) {
console.warn('window.api.on not available')
return
}
// Check if listener is already set up
if ((window as any).__authCodeListenerSetup) {
return
}
// Mark that we've set up the listener
;(window as any).__authCodeListenerSetup = true
const cleanup = window.api.on(
'auth-code-received',
async (authCode: string, state: string) => {
try {
// Exchange authorization code for tokens via main process
const result = await window.api.invoke('exchange-auth-code', {
authCode,
state,
config: Auth0Config,
})
if (!result.success) {
throw new Error(result.error)
}
// Store tokens and user info in the auth store
if (result.tokens && result.userInfo) {
// Extract provider from Auth0 user ID (format: "provider|id")
const providerId = result.userInfo.id || ''
const provider = providerId.includes('|')
? providerId.split('|')[0]
: 'unknown'
// Check if this is a returning user
const existingUser = useAuthStore.getState().user
const isReturningUser =
!!existingUser && existingUser.id === result.userInfo.id
useAuthStore
.getState()
.setAuthData(
result.tokens as AuthTokens,
result.userInfo as AuthUser,
provider,
)
// Track successful signin
analytics.trackAuth(ANALYTICS_EVENTS.AUTH_SIGNIN_COMPLETED, {
provider,
is_returning_user: isReturningUser,
user_id: result.userInfo.id,
})
useMainStore.getState().setCurrentPage('home')
await window.api.notifyLoginSuccess(
result.userInfo,
result.tokens.id_token,
result.tokens.access_token,
)
// Hydrate per-user onboarding state from SQLite for the now-authenticated user
await hydrateOnboardingState()
} else {
throw new Error('Missing tokens or user info in response')
}
} catch (error) {
console.error('Error handling auth code from protocol URL:', error)
// Track signin failure
analytics.track(ANALYTICS_EVENTS.AUTH_SIGNIN_FAILED, {
error_message:
error instanceof Error ? error.message : 'Unknown error',
auth_method: 'external_browser',
})
alert(
`Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
)
}
},
)
return () => {
cleanup()
;(window as any).__authCodeListenerSetup = false
}
}, [])
// External browser authentication - now the primary method
const openExternalAuth = useCallback(
async (
connection?: string,
options?: { email?: string; mode?: 'login' | 'signup' },
) => {
// Track signin attempt started
const provider = connection || 'unknown'
const eventType =
options?.mode === 'signup'
? ANALYTICS_EVENTS.AUTH_SIGNUP_STARTED
: ANALYTICS_EVENTS.AUTH_SIGNIN_STARTED
analytics.trackAuth(eventType, {
provider,
is_returning_user: false, // We don't know yet
auth_method: 'external_browser',
})
let authState = useAuthStore.getState().state
// Generate new auth state if not available
if (!authState) {
try {
// Generate fresh auth state from the main process
authState = await window.api.generateNewAuthState()
if (!authState) {
throw new Error('Generated auth state is null')
}
// Update the store with the new auth state
useAuthStore.getState().updateState(authState)
} catch (error) {
console.error('Failed to generate new auth state:', error)
// Track auth state generation failure
analytics.track(ANALYTICS_EVENTS.AUTH_STATE_GENERATION_FAILED, {
provider,
error_message:
error instanceof Error ? error.message : 'Unknown error',
})
throw new Error(
'Failed to generate auth state. Please restart the app.',
)
}
}
const params = new URLSearchParams({
response_type: 'code',
client_id: Auth0Config.clientId,
redirect_uri: Auth0Config.redirectUri,
scope: Auth0Config.scope,
prompt: 'select_account',
state: authState.state,
code_challenge: authState.codeChallenge,
code_challenge_method: 'S256',
})
// Add audience if configured
if (Auth0Config.audience) {
params.append('audience', Auth0Config.audience)
}
if (connection) {
params.append('connection', connection)
}
if (options?.email) {
params.append('login_hint', options.email)
}
if (options?.mode === 'signup') {
params.append('screen_hint', 'signup')
}
const authUrl = `https://${Auth0Config.domain}/authorize?${params.toString()}`
// Open in external browser
if (window.api?.invoke) {
await window.api.invoke('web-open-url', authUrl)
} else {
window.open(authUrl, '_blank')
}
},
[],
)
// Helper function to reduce duplication in social auth methods
const createSocialAuthMethod = useCallback(
(connection: string, providerName: string) => {
return async (email?: string) => {
try {
await openExternalAuth(connection, email ? { email } : undefined)
} catch (error) {
console.error(`${providerName} external auth failed:`, error)
// Track auth method failure
analytics.track(ANALYTICS_EVENTS.AUTH_METHOD_FAILED, {
provider: providerName.toLowerCase(),
error_message:
error instanceof Error ? error.message : 'Unknown error',
auth_method: 'external_browser',
})
throw error
}
}
},
[openExternalAuth],
)
// Social authentication methods - now use external browser by default
const loginWithGoogle = createSocialAuthMethod(
Auth0Connections.google,
'Google',
)
const loginWithMicrosoft = createSocialAuthMethod(
Auth0Connections.microsoft,
'Microsoft',
)
const loginWithApple = createSocialAuthMethod(Auth0Connections.apple, 'Apple')
// GitHub authentication - now uses external browser
const loginWithGitHub = createSocialAuthMethod(
Auth0Connections.github,
'GitHub',
)
// Email/password via Auth0 Database connection
const loginWithEmail = useCallback(
async (email?: string) => {
try {
await openExternalAuth(
Auth0Connections.database,
email ? { email, mode: 'login' } : { mode: 'login' },
)
} catch (error) {
console.error('Email login failed:', error)
analytics.track(ANALYTICS_EVENTS.AUTH_METHOD_FAILED, {
provider: 'email',
error_message:
error instanceof Error ? error.message : 'Unknown error',
auth_method: 'external_browser',
})
throw error
}
},
[openExternalAuth],
)
// Direct email/password login without opening a browser window
const loginWithEmailPassword = useCallback(
async (
email: string,
password: string,
options?: { skipNavigate?: boolean },
) => {
try {
analytics.trackAuth(ANALYTICS_EVENTS.AUTH_SIGNIN_STARTED, {
provider: 'email',
is_returning_user: false,
auth_method: 'password_realm',
})
const result = await window.api.invoke('auth0-db-login', {
email,
password,
})
if (!result?.success || !result?.tokens) {
throw new Error(result?.error || 'Login failed')
}
const tokens = result.tokens as AuthTokens
const idToken = tokens.id_token || ''
let userInfo: AuthUser | null = null
try {
const payloadPart = idToken.split('.')[1]
const base64 = payloadPart.replace(/-/g, '+').replace(/_/g, '/')
const padded = base64 + '==='.slice((base64.length + 3) % 4)
const json = atob(padded)
const payload = JSON.parse(json)
userInfo = {
id: payload.sub || '',
email: payload.email,
name: payload.name,
picture: payload.picture,
provider:
typeof payload.sub === 'string' && payload.sub.includes('|')
? payload.sub.split('|')[0]
: 'email',
lastSignInAt: new Date().toISOString(),
}
} catch {
console.warn(
'Failed to decode id_token, proceeding with minimal profile',
)
userInfo = {
id: 'email',
email,
provider: 'email',
lastSignInAt: new Date().toISOString(),
}
}
useAuthStore
.getState()
.setAuthData(tokens, userInfo as AuthUser, 'email')
analytics.trackAuth(ANALYTICS_EVENTS.AUTH_SIGNIN_COMPLETED, {
provider: 'email',
is_returning_user: false,
user_id: userInfo?.id,
})
if (!options?.skipNavigate) {
useMainStore.getState().setCurrentPage('home')
}
await window.api.notifyLoginSuccess(
userInfo,
tokens.id_token ?? null,
tokens.access_token ?? null,
)
// Hydrate per-user onboarding state
await hydrateOnboardingState('email/password')
} catch (error) {
console.error('Email/password login failed:', error)
analytics.track(ANALYTICS_EVENTS.AUTH_METHOD_FAILED, {
provider: 'email',
error_message:
error instanceof Error ? error.message : 'Unknown error',
auth_method: 'password_realm',
})
throw error
}
},
[],
)
const signupWithEmail = useCallback(
async (email?: string) => {
try {
await openExternalAuth(
Auth0Connections.database,
email ? { email, mode: 'signup' } : { mode: 'signup' },
)
} catch (error) {
console.error('Email signup failed:', error)
analytics.track(ANALYTICS_EVENTS.AUTH_METHOD_FAILED, {
provider: 'email',
error_message:
error instanceof Error ? error.message : 'Unknown error',
auth_method: 'external_browser',
})
throw error
}
},
[openExternalAuth],
)
// Directly create a database user via Auth0 Authentication API (proxied via main to avoid CORS)
const createDatabaseUser = useCallback(
async (email: string, password: string, name: string) => {
const result = await window.api.invoke('auth0-db-signup', {
email,
password,
name,
})
if (!result?.success) {
throw new Error(result?.error || 'Signup failed')
}
return result.data
},
[],
)
// Self-hosted authentication - bypasses all external auth
const loginWithSelfHosted = useCallback(async () => {
try {
// Track self-hosted signin attempt
analytics.trackAuth(ANALYTICS_EVENTS.AUTH_SIGNIN_STARTED, {
provider: 'self-hosted',
is_returning_user: false,
auth_method: 'self_hosted',
})
setSelfHostedMode()
// Notify main process about self-hosted login and wait for it to complete
const selfHostedProfile = {
id: 'self-hosted',
provider: 'self-hosted',
lastSignInAt: new Date().toISOString(),
}
await window.api.notifyLoginSuccess(
selfHostedProfile,
null, // No idToken for self-hosted
null, // No accessToken for self-hosted
)
// Hydrate per-user onboarding state
await hydrateOnboardingState('self-hosted')
// Track successful self-hosted signin
analytics.trackAuth(ANALYTICS_EVENTS.AUTH_SIGNIN_COMPLETED, {
provider: 'self-hosted',
is_returning_user: false,
user_id: 'self-hosted',
auth_method: 'self_hosted',
})
} catch (error) {
console.error('Self-hosted mode activation error:', error)
// Track self-hosted signin failure
analytics.track(ANALYTICS_EVENTS.AUTH_SIGNIN_FAILED, {
provider: 'self-hosted',
error_message: error instanceof Error ? error.message : 'Unknown error',
auth_method: 'self_hosted',
})
throw error
}
}, [setSelfHostedMode])
// Get access token for API calls
const getAccessToken = useCallback(async () => {
try {
// First try to get from our store (for external auth)
if (tokens?.access_token) {
return tokens.access_token
}
// Fallback to Auth0 silent auth (for popup/redirect auth)
return await getAccessTokenSilently()
} catch (error) {
console.error('Error getting access token:', error)
throw error
}
}, [getAccessTokenSilently, tokens])
// Manual token refresh
const refreshTokens = useCallback(async () => {
try {
console.log('Manually refreshing tokens...')
const result = await window.api.invoke('refresh-tokens')
if (result.success) {
console.log('Manual token refresh successful')
return result
} else {
console.error('Manual token refresh failed:', result.error)
throw new Error(result.error)
}
} catch (error) {
console.error('Error during manual token refresh:', error)
throw error
}
}, [])
// Logout
const logoutUser = useCallback(
async (completelySignOut: boolean = false) => {
try {
// Track logout attempt
const currentUser = authUser
analytics.trackAuth(ANALYTICS_EVENTS.AUTH_LOGOUT, {
provider: currentUser?.provider || 'unknown',
is_returning_user: true, // If they're logging out, they were logged in
user_id: currentUser?.id,
complete_signout: completelySignOut,
session_duration_ms: analytics.getSessionDuration(),
})
// Clear main process store first
await window.api.logout()
// Clear our auth store, preserving user data by default
clearAuth(!completelySignOut)
// Also logout from Auth0 if using Auth0 session
if (auth0IsAuthenticated) {
logout({
logoutParams: {
returnTo: window.location.origin,
},
})
}
} catch (error) {
console.error('Error during logout:', error)
// Track logout failure
analytics.track(ANALYTICS_EVENTS.AUTH_LOGOUT_FAILED, {
provider: authUser?.provider || 'unknown',
error_message:
error instanceof Error ? error.message : 'Unknown error',
complete_signout: completelySignOut,
})
// Still try to clear local auth even if main process logout fails
clearAuth(!completelySignOut)
}
},
[logout, clearAuth, auth0IsAuthenticated, authUser],
)
// Handle auth token events from main process
useEffect(() => {
if (!window.api?.on) {
console.warn('window.api.on not available')
return
}
// Check if listener is already set up
if ((window as any).__authTokenListenerSetup) {
return
}
// Mark that we've set up the listener
;(window as any).__authTokenListenerSetup = true
// Handle token refresh success
const cleanupTokensRefreshed = window.api.on(
'tokens-refreshed',
async (newTokens: AuthTokens) => {
console.log('Tokens refreshed successfully, updating auth store')
try {
// Update the auth store with refreshed tokens
const currentUser = authUser
if (currentUser) {
useAuthStore
.getState()
.setAuthData(newTokens, currentUser, currentUser.provider)
// Track successful token refresh
analytics.track(ANALYTICS_EVENTS.AUTH_SIGNIN_COMPLETED, {
provider: currentUser.provider || 'unknown',
user_id: currentUser.id,
is_returning_user: true,
reason: 'token_refresh',
})
}
} catch (error) {
console.error(
'Error updating auth store with refreshed tokens:',
error,
)
}
},
)
// Handle token expiration (when refresh fails or no refresh token available)
const cleanupTokenExpired = window.api.on(
'auth-token-expired',
async () => {
console.log('Auth token expired, automatically signing out user')
try {
// Track automatic logout due to token expiration
const currentUser = authUser
analytics.trackAuth(ANALYTICS_EVENTS.AUTH_LOGOUT, {
provider: currentUser?.provider || 'unknown',
is_returning_user: true,
user_id: currentUser?.id,
complete_signout: false,
session_duration_ms: analytics.getSessionDuration(),
reason: 'token_expired',
})
logoutUser(false)
// Auth state will automatically redirect to welcome page
} catch (error) {
console.error('Error during automatic logout:', error)
// Still try to clear local auth even if main process logout fails
clearAuth(true)
}
},
)
return () => {
cleanupTokensRefreshed()
cleanupTokenExpired()
;(window as any).__authTokenListenerSetup = false
}
}, [logout, clearAuth, auth0IsAuthenticated, authUser, logoutUser])
return {
// Auth state
user: authUser,
isAuthenticated,
isLoading,
error,
// Authentication methods
loginWithGoogle,
loginWithMicrosoft,
loginWithApple,
loginWithGitHub,
loginWithEmail,
loginWithEmailPassword,
signupWithEmail,
createDatabaseUser,
loginWithSelfHosted,
logoutUser,
// Utilities
getAccessToken,
getIdTokenClaims,
refreshTokens,
}
}
================================================
FILE: app/components/home/HomeKit.tsx
================================================
import {
Home,
BookOpen,
FileText,
CogFour,
InfoCircle,
} from '@mynaui/icons-react'
import { ItoIcon } from '../icons/ItoIcon'
import { useMainStore } from '@/app/store/useMainStore'
import { useUserMetadataStore } from '@/app/store/useUserMetadataStore'
import { useOnboardingStore } from '@/app/store/useOnboardingStore'
import { useAuth } from '@/app/components/auth/useAuth'
import useBillingState from '@/app/hooks/useBillingState'
import { PaidStatus } from '@/lib/main/sqlite/models'
import { useEffect, useState, useRef } from 'react'
import { NavItem } from '../ui/nav-item'
import HomeContent from './contents/HomeContent'
import DictionaryContent from './contents/DictionaryContent'
import NotesContent from './contents/NotesContent'
import SettingsContent from './contents/SettingsContent'
import AboutContent from './contents/AboutContent'
export default function HomeKit() {
const { navExpanded, currentPage, setCurrentPage } = useMainStore()
const { metadata } = useUserMetadataStore()
const { onboardingCompleted } = useOnboardingStore()
const { isAuthenticated, user } = useAuth()
const billingState = useBillingState()
const [showText, setShowText] = useState(navExpanded)
const hasStartedTrialRef = useRef(false)
const previousUserIdRef = useRef<string | undefined>(undefined)
const [isStartingTrial, setIsStartingTrial] = useState(false)
const isPro =
metadata?.paid_status === PaidStatus.PRO ||
metadata?.paid_status === PaidStatus.PRO_TRIAL ||
billingState.proStatus === 'active_pro' ||
billingState.proStatus === 'free_trial'
// Reset flags when user changes
useEffect(() => {
const currentUserId = user?.id
const previousUserId = previousUserIdRef.current
if (currentUserId && currentUserId !== previousUserId) {
// User changed - reset trial start flag
hasStartedTrialRef.current = false
setIsStartingTrial(false)
previousUserIdRef.current = currentUserId
} else if (currentUserId && previousUserId === undefined) {
// First time setting userId
previousUserIdRef.current = currentUserId
}
}, [user?.id])
// Start trial for users who don't have one yet
// Case 1: New users after onboarding completes
// Case 2: Existing users who completed onboarding but haven't started trial yet
useEffect(() => {
// Skip if still loading billing state or not authenticated
if (billingState.isLoading || !isAuthenticated) return
// Only proceed if onboarding is completed
if (!onboardingCompleted) return
// Check if user has a trial or subscription
const hasTrialOrSubscription =
billingState.proStatus === 'free_trial' ||
billingState.proStatus === 'active_pro' ||
isPro
// Start trial if:
// 1. User hasn't started trial yet (tracked by ref)
// 2. User doesn't have a trial or subscription
// 3. User has completed onboarding
if (!hasStartedTrialRef.current && !hasTrialOrSubscription) {
hasStartedTrialRef.current = true
setIsStartingTrial(true) // Set flag to indicate trial is being started
// Start trial
window.api.trial.startAfterOnboarding().catch(err => {
console.error('Failed to start trial:', err)
// Reset flag so we can retry if needed
hasStartedTrialRef.current = false
setIsStartingTrial(false)
})
}
}, [
onboardingCompleted,
isAuthenticated,
billingState.isLoading,
billingState.proStatus,
isPro,
])
// Listen for trial-started event to refresh billing state
useEffect(() => {
const offTrialStarted = window.api.on('trial-started', async () => {
// Trial started successfully - refresh billing state
// HomeContent will handle showing the dialog based on billing state transition
await billingState.refresh()
setIsStartingTrial(false) // Reset flag after trial starts
})
return () => {
offTrialStarted?.()
}
}, [billingState])
// Reset trial start flag when onboarding resets
useEffect(() => {
if (!onboardingCompleted) {
hasStartedTrialRef.current = false
setIsStartingTrial(false)
}
}, [onboardingCompleted])
// Listen for billing deep-link events and finalize subscription
useEffect(() => {
const offSuccess = window.api.on(
'billing-session-completed',
async (sessionId: string) => {
try {
if (sessionId) {
await window.api.billing.confirmSession(sessionId)
}
// Ensure trial is completed locally and on server
await window.api.trial.complete()
} catch (err) {
console.error('Failed to finalize billing session', err)
}
},
)
const offCancel = window.api.on('billing-session-cancelled', () => {
// No-op for now; could show a toast in the future
})
return () => {
offSuccess?.()
offCancel?.()
}
}, [])
// Handle text and positioning animation timing
useEffect(() => {
if (navExpanded) {
// When expanding: slide right first, then show text
const timer = setTimeout(() => {
setShowText(true) // Show text after slide starts
}, 75)
return () => clearTimeout(timer)
} else {
// When collapsing: hide text immediately, then center icons after slide completes
setShowText(false)
// Return no-op function
return () => {}
}
}, [navExpanded])
// Render the appropriate content based on current page
const renderContent = () => {
switch (currentPage) {
case 'home':
return <HomeContent isStartingTrial={isStartingTrial} />
case 'dictionary':
return <DictionaryContent />
case 'notes':
return <NotesContent />
case 'settings':
return <SettingsContent />
case 'about':
return <AboutContent />
default:
return <HomeContent />
}
}
return (
<div className="flex h-full">
{/* Sidebar */}
<div
className={`${navExpanded ? 'w-48' : 'w-20'} flex flex-col justify-between py-4 px-4 transition-all duration-100 ease-in-out border-r border-neutral-200`}
>
<div>
{/* Logo and Plan */}
<div className="flex items-center mb-10 px-3">
<ItoIcon
className="w-6 text-gray-900 flex-shrink-0"
style={{ height: '32px' }}
/>
<span
className={`text-2xl font-bold transition-opacity duration-100 ${showText ? 'opacity-100' : 'opacity-0'} ${showText ? 'ml-2' : 'w-0 overflow-hidden'}`}
>
ito
</span>
{isPro && showText && (
<span
className={`text-xs font-semibold px-2 py-0.5 rounded-md bg-gradient-to-r from-purple-500 to-pink-500 text-white transition-opacity duration-100 ${showText ? 'opacity-100' : 'opacity-0'} ${showText ? 'ml-2' : 'w-0 overflow-hidden'}`}
>
PRO
</span>
)}
</div>
{/* Nav */}
<div className="flex flex-col gap-1 text-sm">
<NavItem
icon={<Home className="w-5 h-5" />}
label="Home"
isActive={currentPage === 'home'}
showText={showText}
onClick={() => setCurrentPage('home')}
/>
<NavItem
icon={<BookOpen className="w-5 h-5" />}
label="Dictionary"
isActive={currentPage === 'dictionary'}
showText={showText}
onClick={() => setCurrentPage('dictionary')}
/>
<NavItem
icon={<FileText className="w-5 h-5" />}
label="Notes"
isActive={currentPage === 'notes'}
showText={showText}
onClick={() => setCurrentPage('notes')}
/>
<NavItem
icon={<CogFour className="w-5 h-5" />}
label="Settings"
isActive={currentPage === 'settings'}
showText={showText}
onClick={() => setCurrentPage('settings')}
/>
<NavItem
icon={<InfoCircle className="w-5 h-5" />}
label="About"
isActive={currentPage === 'about'}
showText={showText}
onClick={() => setCurrentPage('about')}
/>
</div>
</div>
</div>
{/* Main Content */}
<div className="flex flex-col flex-1 items-center bg-white rounded-lg m-2 ml-0 mt-0 pt-12">
{renderContent()}
</div>
</div>
)
}
================================================
FILE: app/components/home/ProUpgradeDialog.tsx
================================================
import React, { useState, useEffect } from 'react'
import { Check } from '@mynaui/icons-react'
import { Dialog, DialogContent, DialogFooter } from '@/app/components/ui/dialog'
import { Button } from '@/app/components/ui/button'
import proBannerImage from '@/app/assets/pro-banner.png'
import useBillingState from '@/app/hooks/useBillingState'
interface ProUpgradeDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export function ProUpgradeDialog({
open,
onOpenChange,
}: ProUpgradeDialogProps) {
const billingState = useBillingState()
const [checkoutLoading, setCheckoutLoading] = useState(false)
const [checkoutError, setCheckoutError] = useState<string | null>(null)
// Refresh billing state when checkout session completes
useEffect(() => {
const offSuccess = window.api.on('billing-session-completed', async () => {
// Refresh billing state to reflect the new subscription
await billingState.refresh()
setCheckoutError(null)
// Close the dialog after successful checkout
onOpenChange(false)
})
return () => {
offSuccess?.()
}
}, [billingState, onOpenChange])
const handleCheckout = async () => {
setCheckoutLoading(true)
setCheckoutError(null)
try {
const res = await window.api.billing.createCheckoutSession()
if (res?.success && res?.url) {
await window.api.invoke('web-open-url', res.url)
} else {
setCheckoutError(
res?.error || 'Failed to create checkout session. Please try again.',
)
}
} catch (err: any) {
setCheckoutError(
err?.message || 'Failed to create checkout session. Please try again.',
)
} finally {
setCheckoutLoading(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md p-0 overflow-hidden border-none">
{/* Banner Header with Image */}
<div
className="relative px-8 py-12 text-center bg-cover bg-center"
style={{ backgroundImage: `url(${proBannerImage})` }}
>
{/* PRO Badge */}
<div className="relative inline-block mb-6">
<div className="bg-white rounded-full px-12 py-4 shadow-lg">
<span className="text-5xl font-black bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent">
PRO
</span>
</div>
</div>
</div>
{/* Content */}
<div className="px-8 py-6 bg-white">
<h2 className="text-3xl font mb-2 ">
Congrats! You have been{' '}
<span className="bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent">
upgraded to Ito Pro for free!
</span>
</h2>
<p className="text-l text-gray-600 mb-6">
Enjoy all Pro features for{' '}
<span className="font-semibold">14 days</span>.
</p>
{/* Error Message */}
{checkoutError && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-sm text-red-800 mb-6">
{checkoutError}
</div>
)}
{/* Features List */}
<div className="space-y-3 mb-6 border border-gray-200 rounded-lg p-4">
<FeatureItem text="Unlimited words per week" />
<FeatureItem text="Ultra fast dictation as fast as 0.3 second" />
<FeatureItem text="Priority customer support" />
<FeatureItem text="Early access to new functionality" />
</div>
{/* Buttons */}
<DialogFooter className="flex-row justify-between sm:justify-between">
<Button
onClick={() => onOpenChange(false)}
variant="default"
size="lg"
className="bg-gray-900 hover:bg-gray-800 text-white rounded-xl"
>
Try for free
</Button>
<Button
onClick={handleCheckout}
variant="outline"
size="lg"
className="rounded-xl border-gray-200"
disabled={checkoutLoading || billingState.isLoading}
>
{checkoutLoading ? 'Loading...' : 'Upgrade Now'}{' '}
<span className="text-gray-500">(20% off)</span>
</Button>
</DialogFooter>
</div>
</DialogContent>
</Dialog>
)
}
function FeatureItem({ text }: { text: string }) {
return (
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
<Check className="w-5 h-5" strokeWidth={3} />
</div>
<span className="text-gray-900">{text}</span>
</div>
)
}
================================================
FILE: app/components/home/contents/AboutContent.tsx
================================================
import { Button } from '@/app/components/ui/button'
import DiscordIcon from '@/app/components/icons/DiscordIcon'
import XIcon from '@/app/components/icons/XIcon'
import GitHubIcon from '@/app/components/icons/GitHubIcon'
import { Globe, Telephone } from '@mynaui/icons-react'
import { EXTERNAL_LINKS } from '@/lib/constants/external-links'
import ItoIcon from '../../icons/ItoIcon'
interface AboutCardProps {
icon: React.ReactNode
title: string
description: string
buttonText: string
onClick: () => void
}
function AboutCard({
icon,
title,
description,
buttonText,
onClick,
}: AboutCardProps) {
return (
<div className="w-1/3 bg-white rounded-lg border border-gray-200 p-4 flex flex-col items-start text-left">
<div className="w-8 h-8 bg-white rounded-lg flex items-center justify-center mb-3">
{icon}
</div>
<h2 className="text-lg font-semibold mb-1">{title}</h2>
<p className="text-gray-500 mb-6 leading-relaxed">{description}</p>
<Button
onClick={onClick}
className="w-fit bg-white text-black border border-gray-300 hover:bg-gray-50 rounded-full cursor-pointer"
style={{
padding: '20px 28px',
}}
>
{buttonText}
</Button>
</div>
)
}
export default function AboutContent() {
const handleDiscordClick = () => {
window.open(EXTERNAL_LINKS.DISCORD, '_blank')
}
const handleTeamCallClick = () => {
window.open(EXTERNAL_LINKS.TEAM_CALL, '_blank')
}
const handleXClick = () => {
window.open(EXTERNAL_LINKS.X_TWITTER, '_blank')
}
const handleGitHubClick = () => {
window.open(EXTERNAL_LINKS.GITHUB, '_blank')
}
const handleWebsiteClick = () => {
window.open(EXTERNAL_LINKS.WEBSITE, '_blank')
}
return (
<div className="w-full px-24">
<div className="mb-8">
<h1 className="text-2xl font-medium">About</h1>
</div>
<div className="flex flex-col gap-4">
{/* First Row: 3 items */}
<div className="flex flex-row gap-4">
<AboutCard
icon={<DiscordIcon width={24} height={24} className="text-black" />}
title="Discord"
description="Join the community, share feedback, and grow with Ito."
buttonText="Join Discord"
onClick={handleDiscordClick}
/>
<AboutCard
icon={<Telephone className="w-6 h-6 text-black" />}
title="Team Call"
description="Got feedback or ideas? Book a quick call with the Ito team."
buttonText="Book a Call"
onClick={handleTeamCallClick}
/>
<AboutCard
icon={<XIcon width={24} height={24} className="text-black" />}
title="X (Twitter)"
description="Get updates, tips, and behind-the-scenes insights from the Ito team."
buttonText="Follow on X"
onClick={handleXClick}
/>
</div>
{/* Second Row: 2 items */}
<div className="flex flex-row gap-4">
<AboutCard
icon={<GitHubIcon width={24} height={24} className="text-black" />}
title="GitHub"
description="Check out the code, contribute, or star the repo."
buttonText="View on GitHub"
onClick={handleGitHubClick}
/>
<AboutCard
icon={<Globe className="w-6 h-6 text-black" />}
title="ito.ai"
description="Learn more about Ito, explore features, and see what's next."
buttonText="Go to Website"
onClick={handleWebsiteClick}
/>
<div className="w-1/3 bg-white rounded-lg border border-gray-200 p-4 flex flex-col items-start text-left">
<div className="bg-white rounded-lg flex items-center justify-center mb-4">
<ItoIcon
className="w-6 h-6 text-gray-900"
style={{ height: '24px' }}
/>
<span className={`text-lg font-bold ml-2`}>ito</span>
</div>
<h2 className="text-lg font-semibold mb-4">
Version {import.meta.env.VITE_ITO_VERSION}
</h2>
<p className="text-gray-500 mb-6 leading-relaxed">
Made with 🩷 in San Francisco.
</p>
</div>
</div>
</div>
</div>
)
}
================================================
FILE: app/components/home/contents/DictionaryContent.tsx
================================================
import { useEffect, useRef, useState } from 'react'
import { ArrowUp, Pencil, Trash, Plus } from '@mynaui/icons-react'
import { Tooltip, TooltipTrigger, TooltipContent } from '../../ui/tooltip'
import { Switch } from '../../ui/switch'
import { StatusIndicator } from '../../ui/status-indicator'
import { useDictionaryStore } from '../../../store/useDictionaryStore'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '../../ui/dialog'
import { Button } from '../../ui/button'
export default function DictionaryContent() {
const {
entries,
loadEntries,
addEntry,
addReplacement,
updateEntry,
deleteEntry,
} = useDictionaryStore()
const [showScrollToTop, setShowScrollToTop] = useState(false)
const [hoveredRow, setHoveredRow] = useState<number | null>(null)
const [editingEntry, setEditingEntry] = useState<{
id: string
type: 'normal' | 'replacement'
content?: string
from?: string
to?: string
} | null>(null)
const [editContent, setEditContent] = useState('')
const [editFrom, setEditFrom] = useState('')
const [editTo, setEditTo] = useState('')
const [showAddDialog, setShowAddDialog] = useState(false)
const [newEntryContent, setNewEntryContent] = useState('')
const [newFrom, setNewFrom] = useState('')
const [newTo, setNewTo] = useState('')
const [isReplacement, setIsReplacement] = useState(false)
const [statusIndicator, setStatusIndicator] = useState<
'success' | 'error' | null
>(null)
const [errorMessage, setErrorMessage] = useState<string>('')
const [successMessage, setSuccessMessage] = useState<string>('')
const containerRef = useRef<HTMLDivElement>(null)
const editInputRef = useRef<HTMLInputElement>(null)
const editFromRef = useRef<HTMLInputElement>(null)
const addInputRef = useRef<HTMLInputElement>(null)
const addFromRef = useRef<HTMLInputElement>(null)
useEffect(() => {
loadEntries()
}, [loadEntries])
// Handle scroll events
useEffect(() => {
const handleScroll = () => {
if (containerRef.current) {
const scrollTop = containerRef.current.scrollTop
setShowScrollToTop(scrollTop > 200) // Show button after scrolling 200px
}
}
const container = containerRef.current
if (container) {
container.addEventListener('scroll', handleScroll)
return () => container.removeEventListener('scroll', handleScroll)
}
return undefined
}, [])
const scrollToTop = () => {
if (containerRef.current) {
containerRef.current.scrollTo({
top: 0,
behavior: 'smooth',
})
}
}
const getDisplayText = (entry: (typeof entries)[0]) => {
if (entry.type === 'replacement') {
return `${entry.from} → ${entry.to}`
}
return entry.content
}
const handleEdit = (id: string) => {
const entry = entries.find(e => e.id === id)
if (entry) {
if (entry.type === 'normal') {
setEditingEntry({ id, type: 'normal', content: entry.content })
setEditContent(entry.content)
setEditFrom('')
setEditTo('')
// Focus the input after the dialog opens
setTimeout(() => {
editInputRef.current?.focus()
}, 100)
} else {
setEditingEntry({
id,
type: 'replacement',
from: entry.from,
to: entry.to,
})
setEditContent('')
setEditFrom(entry.from)
setEditTo(entry.to)
// Focus the first input after the dialog opens
setTimeout(() => {
editFromRef.current?.focus()
}, 100)
}
}
}
const handleSaveEdit = async () => {
if (!editingEntry) return
try {
if (editingEntry.type === 'normal' && editContent.trim() !== '') {
await updateEntry(editingEntry.id, {
type: 'normal',
content: editContent.trim(),
} as any)
setEditingEntry(null)
setEditContent('')
setErrorMessage('')
setSuccessMessage(`"${editContent.trim()}" updated successfully`)
setStatusIndicator('success')
} else if (
editingEntry.type === 'replacement' &&
editFrom.trim() !== '' &&
editTo.trim() !== ''
) {
await updateEntry(editingEntry.id, {
type: 'replacement',
from: editFrom.trim(),
to: editTo.trim(),
} as any)
setEditingEntry(null)
setEditFrom('')
setEditTo('')
setErrorMessage('')
setSuccessMessage(
`"${editFrom.trim()}" → "${editTo.trim()}" updated successfully`,
)
setStatusIndicator('success')
}
} catch (error: any) {
console.error('Failed to update dictionary entry:', error)
const errorMsg = error?.message || 'Failed to update dictionary entry'
setErrorMessage(errorMsg)
setStatusIndicator('error')
}
}
const handleCancelEdit = () => {
setEditingEntry(null)
setEditContent('')
setEditFrom('')
setEditTo('')
}
const handleDelete = async (id: string) => {
const entryToDelete = entries.find(e => e.id === id)
if (entryToDelete) {
const deletedItemText = getDisplayText(entryToDelete)
try {
await deleteEntry(id)
setErrorMessage('')
setSuccessMessage(`"${deletedItemText}" deleted successfully`)
setStatusIndicator('success')
} catch (error) {
console.error('Failed to delete dictionary entry:', error)
setErrorMessage(`Failed to delete "${deletedItemText}"`)
setStatusIndicator('error')
}
}
}
const handleAddNew = () => {
setShowAddDialog(true)
setNewEntryContent('')
setNewFrom('')
setNewTo('')
setIsReplacement(false)
// Focus the input after the dialog opens
setTimeout(() => {
addInputRef.current?.focus()
}, 100)
}
const handleSaveNew = async () => {
try {
if (isReplacement) {
if (newFrom.trim() !== '' && newTo.trim() !== '') {
await addReplacement(newFrom.trim(), newTo.trim())
setShowAddDialog(false)
setNewFrom('')
setNewTo('')
setErrorMessage('')
setSuccessMessage(
`"${newFrom.trim()}" → "${newTo.trim()}" added successfully`,
)
setStatusIndicator('success')
}
} else {
if (newEntryContent.trim() !== '') {
await addEntry(newEntryContent.trim())
setShowAddDialog(false)
setNewEntryContent('')
setErrorMessage('')
setSuccessMessage(`"${newEntryContent.trim()}" added successfully`)
setStatusIndicator('success')
}
}
} catch (error: any) {
console.error('Failed to add dictionary entry:', error)
const errorMsg = error?.message || 'Failed to add dictionary entry'
setErrorMessage(errorMsg)
setStatusIndicator('error')
}
}
const handleCancelNew = () => {
setShowAddDialog(false)
setNewEntryContent('')
setNewFrom('')
setNewTo('')
setIsReplacement(false)
}
const handleReplacementToggle = (checked: boolean) => {
setIsReplacement(checked)
// Focus appropriate input when toggling
setTimeout(() => {
if (checked) {
addFromRef.current?.focus()
} else {
addInputRef.current?.focus()
}
}, 100)
}
// Handle keyboard shortcuts in dialogs
const handleEditKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault()
handleSaveEdit()
} else if (e.key === 'Escape') {
e.preventDefault()
handleCancelEdit()
}
}
const handleAddKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault()
handleSaveNew()
} else if (e.key === 'Escape') {
e.preventDefault()
handleCancelNew()
}
}
const noEntries = entries.length === 0
return (
<div
ref={containerRef}
className="w-full px-24 max-h-160 overflow-y-auto relative"
style={{
msOverflowStyle: 'none',
scrollbarWidth: 'none',
}}
>
<div className="flex items-center justify-between mb-8">
<h1 className="text-2xl font-medium">Dictionary</h1>
<button
onClick={handleAddNew}
className="bg-gray-900 text-white px-6 py-3 rounded-full font-semibold hover:bg-gray-800 cursor-pointer flex items-center gap-2"
>
<Plus className="w-4 h-4" />
Add new
</button>
</div>
<div className="w-full h-[1px] bg-slate-200 my-10"></div>
{noEntries && (
<div className="text-gray-500">
<p className="text-sm">No entries yet</p>
<p className="text-xs mt-1">
Dictionary entries make the transcription more accurate
</p>
</div>
)}
{!noEntries && (
<div className="bg-white rounded-lg border border-slate-200 divide-y divide-slate-200">
{entries.map((entry, index) => (
<div
key={entry.id}
className="flex items-center justify-between px-4 py-4 gap-10 hover:bg-gray-50 transition-colors duration-200 group"
onMouseEnter={() => setHoveredRow(index)}
onMouseLeave={() => setHoveredRow(null)}
>
<div className="text-gray-900 flex-1">
{getDisplayText(entry)}
</div>
{/* Action Icons - shown on hover */}
<div
className={`flex items-center gap-2 transition-opacity duration-200 ${
hoveredRow === index ? 'opacity-100' : 'opacity-0'
}`}
>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => handleEdit(entry.id)}
className="p-1.5 hover:bg-gray-200 rounded transition-colors cursor-pointer"
aria-label="Edit entry"
>
<Pencil className="w-4 h-4 text-gray-600" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={5}>
Edit
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => handleDelete(entry.id)}
className="p-1.5 hover:bg-red-100 rounded transition-colors cursor-pointer"
aria-label="Delete entry"
>
<Trash className="w-4 h-4 text-gray-600 hover:text-red-600" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={5}>
Delete
</TooltipContent>
</Tooltip>
</div>
</div>
))}
</div>
)}
{/* Scroll to Top Button */}
{showScrollToTop && (
<button
onClick={scrollToTop}
className="fixed bottom-8 bg-black text-white right-8 w-8 h-8 rounded-full shadow-lg hover:shadow-xl hover:-translate-y-1 transition-all duration-200 flex items-center justify-center group z-50 cursor-pointer"
aria-label="Scroll to top"
>
<ArrowUp className="w-4 h-4 font-bold" />
</button>
)}
{/* Status Indicator */}
<StatusIndicator
status={statusIndicator}
onHide={() => {
setStatusIndicator(null)
setErrorMessage('')
setSuccessMessage('')
}}
successMessage={successMessage || 'Dictionary entry added successfully'}
errorMessage={errorMessage || 'Failed to add dictionary entry'}
/>
{/* Edit Entry Dialog */}
<Dialog
open={!!editingEntry}
onOpenChange={open => !open && handleCancelEdit()}
>
<DialogContent
className="!border-0 shadow-lg p-0"
showCloseButton={false}
>
<DialogHeader>
<DialogTitle className="sr-only">
{editingEntry?.type === 'replacement'
? 'Edit replacement'
: 'Edit Dictionary Entry'}
</DialogTitle>
</DialogHeader>
<div className="px-6">
<h2 className="text-lg font-semibold mb-4">
{editingEntry?.type === 'replacement'
? 'Edit replacement'
: 'Edit entry'}
</h2>
{editingEntry?.type === 'normal' ? (
<input
ref={editInputRef}
type="text"
value={editContent}
onChange={e => setEditContent(e.target.value)}
onKeyDown={handleEditKeyDown}
className="w-full p-4 rounded-md resize-none focus:outline-none border border-neutral-200"
placeholder="Enter dictionary entry..."
/>
) : (
<div className="space-y-4">
<div className="flex items-center gap-4">
<input
ref={editFromRef}
type="text"
value={editFrom}
onChange={e => setEditFrom(e.target.value)}
onKeyDown={handleEditKeyDown}
className="flex-1 p-4 rounded-md resize-none focus:outline-none border border-neutral-200"
placeholder="Misspelling"
/>
<span className="text-gray-500">→</span>
<input
type="text"
value={editTo}
onChange={e => setEditTo(e.target.value)}
onKeyDown={handleEditKeyDown}
className="flex-1 p-4 rounded-md resize-none focus:outline-none border border-neutral-200"
placeholder="Correct spelling"
/>
</div>
</div>
)}
</div>
<DialogFooter className="p-4">
<Button
className="bg-neutral-200 hover:bg-neutral-300 text-black cursor-pointer"
onClick={handleCancelEdit}
>
Cancel
</Button>
<Button
className="cursor-pointer"
onClick={handleSaveEdit}
disabled={
editingEntry?.type === 'normal'
? !editContent.trim()
: !editFrom.trim() || !editTo.trim()
}
>
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Add New Entry Dialog */}
<Dialog
open={showAddDialog}
onOpenChange={open => !open && handleCancelNew()}
>
<DialogContent
className="!border-0 shadow-lg p-0"
showCloseButton={false}
>
<DialogHeader>
<DialogTitle className="sr-only">Add to vocabulary</DialogTitle>
</DialogHeader>
<div className="px-6">
<h2 className="text-lg font-semibold mb-4">Add to vocabulary</h2>
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium">Make it a replacement</span>
<Switch
checked={isReplacement}
onCheckedChange={handleReplacementToggle}
/>
</div>
{!isReplacement ? (
<input
ref={addInputRef}
type="text"
value={newEntryContent}
onChange={e => setNewEntryContent(e.target.value)}
onKeyDown={handleAddKeyDown}
className="w-full p-4 rounded-md resize-none focus:outline-none border border-neutral-200"
placeholder="Enter dictionary entry..."
/>
) : (
<div className="space-y-4">
<div className="flex items-center gap-4">
<input
ref={addFromRef}
type="text"
value={newFrom}
onChange={e => setNewFrom(e.target.value)}
onKeyDown={handleAddKeyDown}
className="flex-1 p-4 rounded-md resize-none focus:outline-none border border-neutral-200"
placeholder="Misspelling"
/>
<span className="text-gray-500">→</span>
<input
type="text"
value={newTo}
onChange={e => setNewTo(e.target.value)}
onKeyDown={handleAddKeyDown}
className="flex-1 p-4 rounded-md resize-none focus:outline-none border border-neutral-200"
placeholder="Correct spelling"
/>
</div>
</div>
)}
</div>
<DialogFooter className="p-4">
<Button
className="bg-neutral-200 hover:bg-neutral-300 text-black cursor-pointer"
onClick={handleCancelNew}
>
Cancel
</Button>
<Button
className="cursor-pointer"
onClick={handleSaveNew}
disabled={
isReplacement
? !newFrom.trim() || !newTo.trim()
: !newEntryContent.trim()
}
>
Add word
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}
================================================
FILE: app/components/home/contents/HomeContent.tsx
================================================
import React, { useCallback, useEffect, useState } from 'react'
import {
ChartNoAxesColumn,
InfoCircle,
Play,
Stop,
Copy,
Check,
Download,
} from '@mynaui/icons-react'
import { EXTERNAL_LINKS } from '@/lib/constants/external-links'
import { useSettingsStore } from '../../../store/useSettingsStore'
import { Tooltip, TooltipTrigger, TooltipContent } from '../../ui/tooltip'
import { useAuthStore } from '@/app/store/useAuthStore'
import { Interaction } from '@/lib/main/sqlite/models'
import { TotalWordsIcon } from '../../icons/TotalWordsIcon'
import { SpeedIcon } from '../../icons/SpeedIcon'
import {
STREAK_MESSAGES,
SPEED_MESSAGES,
TOTAL_WORDS_MESSAGES,
getStreakLevel,
getSpeedLevel,
getTotalWordsLevel,
getActivityMessage,
} from './activityMessages'
import { ItoMode } from '@/app/generated/ito_pb'
import { getKeyDisplay } from '@/app/utils/keyboard'
import { createStereo48kWavFromMonoPCM } from '@/app/utils/audioUtils'
import { KeyName } from '@/lib/types/keyboard'
import { usePlatform } from '@/app/hooks/usePlatform'
import { ProUpgradeDialog } from '../ProUpgradeDialog'
import useBillingState from '@/app/hooks/useBillingState'
// Interface for interaction statistics
interface InteractionStats {
streakDays: number
totalWords: number
averageWPM: number
}
const StatCard = ({
title,
value,
description,
icon,
}: {
title: string
value: string
description: string
icon: React.ReactNode
}) => {
return (
<div className="flex flex-col p-4 w-1/3 border-2 border-neutral-100 rounded-xl gap-4">
<div className="flex flex-row items-center">
<div className="flex flex-col gap-1">
<div>{title}</div>
<div className="font-bold">{value}</div>
</div>
<div className="flex flex-col items-end flex-1">{icon}</div>
</div>
<div className="w-full text-neutral-400">{description}</div>
</div>
)
}
interface HomeContentProps {
isStartingTrial?: boolean
}
export default function HomeContent({
isStartingTrial = false,
}: HomeContentProps) {
const { getItoModeShortcuts } = useSettingsStore()
const keyboardShortcut = getItoModeShortcuts(ItoMode.TRANSCRIBE)[0].keys
const { user } = useAuthStore()
const firstName = user?.name?.split(' ')[0]
const platform = usePlatform()
const [interactions, setInteractions] = useState<Interaction[]>([])
const [loading, setLoading] = useState(true)
const [playingAudio, setPlayingAudio] = useState<string | null>(null)
const [audioInstances, setAudioInstances] = useState<
Map<string, HTMLAudioElement>
>(new Map())
const [copiedItems, setCopiedItems] = useState<Set<string>>(new Set())
const [openTooltipKey, setOpenTooltipKey] = useState<string | null>(null)
const [stats, setStats] = useState<InteractionStats>({
streakDays: 0,
totalWords: 0,
averageWPM: 0,
})
const [showProDialog, setShowProDialog] = useState(false)
const billingState = useBillingState()
// Persist "has shown trial dialog" flag in electron-store to survive remounts
const [hasShownTrialDialog, setHasShownTrialDialogState] = useState(() => {
try {
const authStore = window.electron?.store?.get('auth') || {}
const value = authStore?.hasShownTrialDialog === true
return value
} catch {
return false
}
})
const setHasShownTrialDialog = useCallback((value: boolean) => {
try {
setHasShownTrialDialogState(value)
window.api.send('electron-store-set', 'auth.hasShownTrialDialog', value)
} catch {
console.warn('Failed to persist hasShownTrialDialog flag')
}
}, [])
// Show trial dialog when trial starts
useEffect(() => {
if (
billingState.isTrialActive &&
billingState.proStatus === 'free_trial' &&
!hasShownTrialDialog &&
!billingState.isLoading
) {
setShowProDialog(true)
setHasShownTrialDialog(true)
}
}, [
billingState.isTrialActive,
billingState.proStatus,
billingState.isLoading,
isStartingTrial,
hasShownTrialDialog,
setHasShownTrialDialog,
])
// Listen for trial start event to refresh billing state
useEffect(() => {
const offTrialStarted = window.api.on('trial-started', async () => {
await billingState.refresh()
})
const offBillingSuccess = window.api.on(
'billing-session-completed',
async () => {
await billingState.refresh()
},
)
return () => {
offTrialStarted?.()
offBillingSuccess?.()
}
}, [billingState])
// Reset dialog flag when trial is no longer active or user becomes pro
// Only reset if we're certain the trial has ended (not just during loading/refreshing)
useEffect(() => {
if (billingState.isLoading) {
// Don't reset during loading to avoid race conditions
return
}
const shouldReset =
billingState.proStatus === 'active_pro' ||
(billingState.proStatus === 'none' && !billingState.isTrialActive)
if (shouldReset && hasShownTrialDialog) {
setHasShownTrialDialog(false)
}
}, [
billingState.proStatus,
billingState.isTrialActive,
billingState.isLoading,
hasShownTrialDialog,
setHasShownTrialDialog,
])
// Calculate statistics from interactions
const calculateStats = useCallback(
(interactions: Interaction[]): InteractionStats => {
if (interactions.length === 0) {
return { streakDays: 0, totalWords: 0, averageWPM: 0 }
}
// Calculate streak (consecutive days with interactions)
const streakDays = calculateStreak(interactions)
// Calculate total words from transcripts
const totalWords = calculateTotalWords(interactions)
// Calculate average WPM (estimate based on average speaking rate)
const averageWPM = calculateAverageWPM(interactions)
return { streakDays, totalWords, averageWPM }
},
[],
)
const calculateStreak = (interactions: Interaction[]): number => {
if (interactions.length === 0) return 0
// Group interactions by date
const dateGroups = new Map<string, Interaction[]>()
interactions.forEach(interaction => {
const date = new Date(interaction.created_at).toDateString()
if (!dateGroups.has(date)) {
dateGroups.set(date, [])
}
dateGroups.get(date)!.push(interaction)
})
// Sort dates in descending order (most recent first)
const sortedDates = Array.from(dateGroups.keys()).sort(
(a, b) => new Date(b).getTime() - new Date(a).getTime(),
)
let streak = 0
const today = new Date()
for (let i = 0; i < sortedDates.length; i++) {
const currentDate = new Date(sortedDates[i])
const expectedDate = new Date(today)
expectedDate.setDate(today.getDate() - i)
// Check if current date matches expected date (allowing for today or previous consecutive days)
if (currentDate.toDateString() === expectedDate.toDateString()) {
streak++
} else {
break
}
}
return streak
}
const calculateTotalWords = (interactions: Interaction[]): number => {
return interactions.reduce((total, interaction) => {
const transcript = interaction.asr_output?.transcript?.trim()
if (transcript) {
// Count words by splitting on whitespace and filtering out empty strings
const words = transcript.split(/\s+/).filter(word => word.length > 0)
return total + words.length
}
return total
}, 0)
}
const calculateAverageWPM = (interactions: Interaction[]): number => {
const validInteractions = interactions.filter(
interaction =>
interaction.asr_output?.transcript?.trim() && interaction.duration_ms,
)
if (validInteractions.length === 0) return 0
let totalWords = 0
let totalDurationMs = 0
validInteractions.forEach(interaction => {
const transcript = interaction.asr_output?.transcript?.trim()
if (transcript && interaction.duration_ms) {
// Count words by splitting on whitespace and filtering out empty strings
const words = transcript.split(/\s+/).filter(word => word.length > 0)
totalWords += words.length
totalDurationMs += interaction.duration_ms
}
})
if (totalDurationMs === 0) return 0
// Calculate WPM: (total words / total duration in minutes)
const totalMinutes = totalDurationMs / (1000 * 60)
const wpm = totalWords / totalMinutes
// Round to nearest integer and ensure it's reasonable
return Math.round(Math.max(1, wpm))
}
const formatStreakText = (days: number): string => {
if (days === 0) return '0 days'
if (days === 1) return '1 day'
if (days < 7) return `${days} days`
if (days < 14) return '1 week'
if (days < 30) return `${Math.floor(days / 7)} weeks`
if (days < 60) return '1 month'
return `${Math.floor(days / 30)} months`
}
const loadInteractions = useCallback(async () => {
try {
const allInteractions = await window.api.interactions.getAll()
// Sort by creation date (newest first) - remove the slice(0, 10) to show all interactions
const sortedInteractions = allInteractions.sort(
(a: Interaction, b: Interaction) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
)
setInteractions(sortedInteractions)
// Calculate and set statistics
const calculatedStats = calculateStats(sortedInteractions)
setStats(calculatedStats)
} catch (error) {
console.error('Failed to load interactions:', error)
} finally {
setLoading(false)
}
}, [calculateStats])
useEffect(() => {
loadInteractions()
// Listen for new interactions
const handleInteractionCreated = () => {
loadInteractions()
}
const unsubscribe = window.api.on(
'interaction-created',
handleInteractionCreated,
)
// Cleanup listener on unmount
return unsubscribe
}, [loadInteractions])
// Cleanup audio instances on unmount
useEffect(() => {
return () => {
audioInstances.forEach(audio => {
try {
audio.pause()
audio.currentTime = 0
// Best-effort release of object URL if used
if (audio.src?.startsWith('blob:')) {
URL.revokeObjectURL(audio.src)
}
} catch {
/* ignore */
}
})
}
}, [audioInstances])
const formatTime = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true,
})
}
const formatDate = (dateString: string) => {
const date = new Date(dateString)
const today = new Date()
const yesterday = new Date()
yesterday.setDate(today.getDate() - 1)
const isToday = date.toDateString() === today.toDateString()
const isYesterday = date.toDateString() === yesterday.toDateString()
if (isToday) return 'TODAY'
if (isYesterday) return 'YESTERDAY'
return date
.toLocaleDateString('en-US', {
weekday: 'long',
month: 'short',
day: 'numeric',
})
.toUpperCase()
}
const groupInteractionsByDate = (interactions: Interaction[]) => {
const groups: { [key: string]: Interaction[] } = {}
interactions.forEach(interaction => {
const dateKey = formatDate(interaction.created_at)
if (!groups[dateKey]) {
groups[dateKey] = []
}
groups[dateKey].push(interaction)
})
return groups
}
const getDisplayText = (interaction: Interaction) => {
// Check for errors first
if (interaction.asr_output?.error) {
// Prefer precise error code mapping when available
const code = interaction.asr_output?.errorCode
if (code === 'CLIENT_TRANSCRIPTION_QUALITY_ERROR') {
return {
text: 'Audio quality too low',
isError: true,
tooltip:
'Audio quality was too low to generate a reliable transcript',
}
}
if (
interaction.asr_output.error.includes('No speech detected in audio.') ||
interaction.asr_output.error.includes('Unable to transcribe audio.')
) {
return {
text: 'Audio is silent',
isError: true,
tooltip: "Ito didn't detect any words so the transcript is empty",
}
}
return {
text: 'Transcription failed',
isError: true,
tooltip: interaction.asr_output.error,
}
}
// Check for empty transcript
const transcript = interaction.asr_output?.transcript?.trim()
if (!transcript) {
return {
text: 'Audio is silent.',
isError: true,
tooltip: "Ito didn't detect any words so the transcript is empty",
}
}
// Return the actual transcript
return {
text: transcript,
isError: false,
tooltip: null,
}
}
const handleAudioPlayStop = async (interaction: Interaction) => {
try {
// If this interaction is currently playing, stop it
if (playingAudio === interaction.id) {
const current = audioInstances.get(interaction.id)
if (current) {
current.pause()
current.currentTime = 0
if (current.src?.startsWith('blob:')) {
URL.revokeObjectURL(current.src)
}
}
setPlayingAudio(null)
return
}
// Stop any other playing audio
if (playingAudio) {
const other = audioInstances.get(playingAudio)
if (other) {
other.pause()
other.currentTime = 0
if (other.src?.startsWith('blob:')) {
URL.revokeObjectURL(other.src)
}
}
}
if (!interaction.raw_audio) {
console.warn('No audio data available for this interaction')
return
}
// Set playing state immediately for responsive UI
setPlayingAudio(interaction.id)
// Reuse existing audio instance if available
let audio = audioInstances.get(interaction.id)
if (!audio) {
const pcmData = new Uint8Array(interaction.raw_audio)
try {
// Convert raw PCM (mono, typically 16 kHz) to 48 kHz stereo WAV for smoother playback
const wavBuffer = createStereo48kWavFromMonoPCM(
pcmData,
interaction.sample_rate || 16000,
48000,
)
const audioBlob = new Blob([wavBuffer], { type: 'audio/wav' })
const audioUrl = URL.createObjectURL(audioBlob)
audio = new Audio(audioUrl)
audio.onended = () => {
setPlayingAudio(null)
if (audio && audio.src?.startsWith('blob:')) {
URL.revokeObjectURL(audio.src)
}
}
audio.onerror = err => {
console.error('Audio playback error:', err)
setPlayingAudio(null)
if (audio && audio.src?.startsWith('blob:')) {
URL.revokeObjectURL(audio.src)
}
}
setAudioInstances(prev => new Map(prev).set(interaction.id, audio!))
} catch (error) {
console.error('Failed to create audio instance:', error)
setPlayingAudio(null)
return
}
}
try {
await audio.play()
} catch (playError) {
console.error('Failed to start audio playback:', playError)
setPlayingAudio(null)
}
} catch (error) {
console.error('Failed to play/stop audio:', error)
setPlayingAudio(null)
}
}
const groupedInteractions = groupInteractionsByDate(interactions)
const copyToClipboard = async (text: string, interactionId: string) => {
try {
await navigator.clipboard.writeText(text)
setCopiedItems(prev => new Set(prev).add(interactionId))
setOpenTooltipKey(`copy:${interactionId}`) // Keep tooltip open
// Reset the copied state after 2 seconds
setTimeout(() => {
setCopiedItems(prev => {
const newSet = new Set(prev)
newSet.delete(interactionId)
return newSet
})
// Close tooltip if it's still open for this item (do not override if user hovered elsewhere)
setOpenTooltipKey(prev =>
prev === `copy:${interactionId}` ? null : prev,
)
}, 2000)
} catch (error) {
console.error('Failed to copy text:', error)
}
}
const handleAudioDownload = async (interaction: Interaction) => {
try {
if (!interaction.raw_audio) {
console.warn('No audio data available for download')
return
}
const pcmData = new Uint8Array(interaction.raw_audio)
// Convert raw PCM to WAV format
const wavBuffer = createStereo48kWavFromMonoPCM(
pcmData,
interaction.sample_rate || 16000,
48000,
)
const audioBlob = new Blob([wavBuffer], { type: 'audio/wav' })
const audioUrl = URL.createObjectURL(audioBlob)
// Format filename with timestamp (YYYYMMDD_HHMMSS)
const date = new Date(interaction.created_at)
const timestamp = date
.toISOString()
.replace(/[-:]/g, '')
.replace('T', '_')
.slice(0, 15)
const filename = `ito-recording-${timestamp}.wav`
// Create temporary link and trigger download
const link = document.createElement('a')
link.href = audioUrl
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
// Clean up the blob URL
URL.revokeObjectURL(audioUrl)
} catch (error) {
console.error('Failed to download audio:', error)
}
}
return (
<div className="w-full h-full flex flex-col">
{/* Fixed Header Content */}
<div className="flex-shrink-0 px-24">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-2xl font-medium">
Welcome back{firstName ? `, ${firstName}!` : '!'}
</h1>
</div>
</div>
<div className="flex gap-4 w-full mb-6">
<div className="flex w-full items-center text-sm text-gray-700 gap-2">
<StatCard
title="Weekly Streak"
value={formatStreakText(stats.streakDays)}
description={getActivityMessage(
STREAK_MESSAGES,
getStreakLevel(stats.streakDays),
)}
icon={
<div className="p-2 bg-blue-50 rounded-md">
<ChartNoAxesColumn
className="w-6 h-6 text-blue-400 border-2 p-1 rounded-full"
strokeWidth={4}
/>
</div>
}
/>
<StatCard
title="Average Speed"
value={`${stats.averageWPM} words / minute`}
description={getActivityMessage(
SPEED_MESSAGES,
getSpeedLevel(stats.averageWPM),
)}
icon={
<div className="p-2 bg-green-50 rounded-md">
<SpeedIcon />
</div>
}
/>
<StatCard
title="Total Words"
value={`${stats.totalWords} ${stats.totalWords === 1 ? 'word' : 'words'}`}
description={getActivityMessage(
TOTAL_WORDS_MESSAGES,
getTotalWordsLevel(stats.totalWords),
)}
icon={
<div className="p-2 bg-orange-50 rounded-md">
<TotalWordsIcon />
</div>
}
/>
</div>
</div>
{/* Dictation Info Box */}
<div className="bg-slate-100 rounded-xl p-6 flex items-center justify-between mb-10">
<div>
<div className="text-base font-medium mb-1">
Voice dictation in any app
</div>
<div className="text-sm text-gray-600">
<span key="hold-down">Hold down the trigger key </span>
{keyboardShortcut.map((key, index) => (
<React.Fragment key={index}>
<span className="bg-slate-50 px-1 py-0.5 rounded text-xs font-mono shadow-sm">
{getKeyDisplay(key as KeyName, platform, {
showDirectionalText: false,
format: 'label',
})}
</span>
<span>{index < keyboardShortcut.length - 1 && ' + '}</span>
</React.Fragment>
))}
<span key="and"> and speak into any textbox</span>
</div>
</div>
<button
className="bg-gray-900 text-white px-6 py-3 rounded-full font-semibold hover:bg-gray-800 cursor-pointer"
onClick={() =>
window.api?.invoke('web-open-url', EXTERNAL_LINKS.WEBSITE)
}
>
Explore use cases
</button>
</div>
{/* Recent Activity Header */}
<div className="text-sm text-muted-foreground mb-6">
Recent activity
</div>
</div>
{/* Scrollable Recent Activity Section */}
<div className="flex-1 px-24 overflow-y-auto scrollbar-hide">
{loading ? (
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center text-gray-500">
Loading recent activity...
</div>
) : interactions.length === 0 ? (
<div className="bg-white rounded-lg border border-slate-200 p-8 text-center text-gray-500">
<p className="text-sm">No interactions yet</p>
<p className="text-xs mt-1">
Try using voice dictation by pressing{' '}
{keyboardShortcut.join(' + ')}
</p>
</div>
) : (
Object.entries(groupedInteractions).map(
([dateLabel, dateInteractions]) => (
<div key={dateLabel} className="mb-6">
<div className="text-xs text-gray-500 mb-4">{dateLabel}</div>
<div className="bg-white rounded-lg border border-slate-200 divide-y divide-slate-200">
{dateInteractions.map(interaction => {
const displayInfo = getDisplayText(interaction)
return (
<div
key={interaction.id}
className="flex items-center justify-between px-4 py-4 gap-10 hover:bg-gray-50 transition-colors duration-200 group"
>
<div className="flex items-center gap-10">
<div className="text-gray-600 min-w-[60px]">
{formatTime(interaction.created_at)}
</div>
<div
className={`${displayInfo.isError ? 'text-gray-600' : 'text-gray-900'} flex items-center gap-1`}
>
{displayInfo.text}
{displayInfo.tooltip && (
<Tooltip>
<TooltipTrigger>
<InfoCircle className="w-4 h-4 text-gray-400" />
</TooltipTrigger>
<TooltipContent>
{displayInfo.tooltip}
</TooltipContent>
</Tooltip>
)}
</div>
</div>
{/* Copy, Download, and Play buttons - only show on hover or when playing */}
<div
className={`flex items-center gap-2 ${playingAudio === interaction.id ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} transition-opacity duration-200`}
>
{/* Copy button */}
{!displayInfo.isError && (
<Tooltip
open={openTooltipKey === `copy:${interaction.id}`}
onOpenChange={open => {
if (open) {
// Opening: exclusively show this tooltip
setOpenTooltipKey(`copy:${interaction.id}`)
} else {
// Closing: if in copied state, keep it open until timer clears,
// otherwise close normally
if (!copiedItems.has(interaction.id)) {
setOpenTooltipKey(prev =>
prev === `copy:${interaction.id}`
? null
: prev,
)
}
}
}}
>
<TooltipTrigger asChild>
<button
className={`p-1.5 hover:bg-gray-200 rounded transition-colors cursor-pointer ${
copiedItems.has(interaction.id)
? 'text-green-600'
: 'text-gray-600'
}`}
onClick={() =>
copyToClipboard(
displayInfo.text,
interaction.id,
)
}
>
{copiedItems.has(interaction.id) ? (
<Check className="w-4 h-4" />
) : (
<Copy className="w-4 h-4" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={5}>
{copiedItems.has(interaction.id)
? 'Copied 🎉'
: 'Copy'}
</TooltipContent>
</Tooltip>
)}
{/* Download button */}
{interaction.raw_audio && (
<Tooltip
open={
openTooltipKey === `download:${interaction.id}`
}
onOpenChange={open => {
setOpenTooltipKey(
open ? `download:${interaction.id}` : null,
)
}}
>
<TooltipTrigger asChild>
<button
className="p-1.5 hover:bg-gray-200 rounded transition-colors cursor-pointer text-gray-600"
onClick={() =>
handleAudioDownload(interaction)
}
>
<Download className="w-4 h-4" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={5}>
Download audio
</TooltipContent>
</Tooltip>
)}
{/* Play/Stop button with tooltip */}
<Tooltip
open={openTooltipKey === `play:${interaction.id}`}
onOpenChange={open => {
setOpenTooltipKey(
open ? `play:${interaction.id}` : null,
)
}}
>
<TooltipTrigger asChild>
<button
className={`p-1.5 hover:bg-gray-200 rounded transition-colors cursor-pointer ${
playingAudio === interaction.id
? 'bg-blue-50 text-blue-600'
: 'text-gray-600'
}`}
onClick={() => handleAudioPlayStop(interaction)}
disabled={!interaction.raw_audio}
>
{playingAudio === interaction.id ? (
<Stop className="w-4 h-4" />
) : (
<Play className="w-4 h-4" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={5}>
{!interaction.raw_audio
? 'No audio available'
: playingAudio === interaction.id
? 'Stop'
: 'Play'}
</TooltipContent>
</Tooltip>
</div>
</div>
)
})}
</div>
</div>
),
)
)}
</div>
{/* Pro Upgrade Dialog */}
<ProUpgradeDialog open={showProDialog} onOpenChange={setShowProDialog} />
</div>
)
}
================================================
FILE: app/components/home/contents/NotesContent.tsx
================================================
import { useEffect, useRef, useState } from 'react'
import { useNotesStore } from '../../../store/useNotesStore'
import { useSettingsStore } from '../../../store/useSettingsStore'
import Masonry from '@mui/lab/Masonry'
import { AudioIcon } from '../../icons/AudioIcon'
import { ArrowUp, Grid, Rows, Search, X } from '@mynaui/icons-react'
import { Note } from '../../ui/note'
import { StatusIndicator } from '../../ui/status-indicator'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '../../ui/dialog'
import { Button } from '../../ui/button'
import { ItoMode } from '@/app/generated/ito_pb'
import { getKeyDisplayInfo } from '@/lib/types/keyboard'
import { usePlatform } from '@/app/hooks/usePlatform'
export default function NotesContent() {
const { notes, loadNotes, addNote, deleteNote, updateNote } = useNotesStore()
const { getItoModeShortcuts } = useSettingsStore()
const keyboardShortcut = getItoModeShortcuts(ItoMode.TRANSCRIBE)[0].keys
const [creatingNote, setCreatingNote] = useState(false)
const [showAddNoteButton, setShowAddNoteButton] = useState(false)
const [noteContent, setNoteContent] = useState('')
const [showScrollToTop, setShowScrollToTop] = useState(false)
const [containerHeight, setContainerHeight] = useState(128) // 128px = h-32
const [showSearch, setShowSearch] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [showDropdown, setShowDropdown] = useState<number | null>(null)
const [statusIndicator, setStatusIndicator] = useState<
'success' | 'error' | null
>(null)
const [statusMessage, setStatusMessage] = useState<string>('')
const textareaRef = useRef<HTMLTextAreaElement>(null)
const searchInputRef = useRef<HTMLInputElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
const [editingNote, setEditingNote] = useState<{
id: string
content: string
} | null>(null)
const [editContent, setEditContent] = useState('')
const editTextareaRef = useRef<HTMLTextAreaElement>(null)
const platform = usePlatform()
useEffect(() => {
loadNotes()
}, [loadNotes, addNote, notes.length])
const formatDate = (date: Date) => {
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
})
}
const formatTime = (date: Date) => {
return date.toLocaleString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true,
})
}
const truncateContent = (content: string, maxLength: number = 100) => {
if (content.length <= maxLength) {
return content
}
return content.slice(0, maxLength) + '...'
}
const handleBlur = () => {
// If the note isn't empty, don't close the input
setTimeout(() => {
if (textareaRef.current?.value.trim() === '') {
setCreatingNote(false)
}
}, 200)
}
const updateNoteContent = (content: string) => {
setNoteContent(content)
const fmt = new Intl.DateTimeFormat('en-GB', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3,
hour12: false,
})
const timestamp = fmt.format(new Date())
console.log(`${timestamp}: Pasted content: ${content}`)
if (content.trim() !== '') {
setShowAddNoteButton(true)
} else {
setShowAddNoteButton(false)
}
// Auto-resize textarea and container
if (textareaRef.current) {
textareaRef.current.style.height = 'auto'
const scrollHeight = textareaRef.current.scrollHeight
textareaRef.current.style.height = `${scrollHeight}px`
// Calculate container height: textarea height + padding + button space
const minHeight = 192 // min-h-48 = 192px
const paddingAndButton = 48 + 40 // 48px padding + 40px for button space
const newContainerHeight = Math.max(
minHeight,
scrollHeight + paddingAndButton,
)
setContainerHeight(newContainerHeight)
}
}
const toggleViewMode = () => {
setViewMode(viewMode === 'grid' ? 'list' : 'grid')
}
const openSearch = () => {
setShowSearch(true)
// Focus the search input after the component updates
setTimeout(() => {
searchInputRef.current?.focus()
}, 100)
}
const closeSearch = () => {
setShowSearch(false)
setSearchQuery('')
}
// Filter notes based on search query
const filteredNotes =
searchQuery.trim() === ''
? notes
: notes.filter(note =>
note.content.toLowerCase().includes(searchQuery.toLowerCase()),
)
const handleAddNote = async () => {
if (noteContent.trim() !== '') {
try {
await addNote(noteContent.trim())
setNoteContent('')
setCreatingNote(false)
setShowAddNoteButton(false)
setStatusMessage('Note saved')
setStatusIndicator('success')
} catch (error) {
console.error('Failed to add note:', error)
setStatusMessage('Failed to save note')
setStatusIndicator('error')
}
}
}
const handleCopyToClipboard = async (content: string) => {
try {
await navigator.clipboard.writeText(content)
setShowDropdown(null)
// You could add a toast notification here
} catch (err) {
console.error('Failed to copy text: ', err)
}
}
const handleDeleteNote = async (noteId: string) => {
try {
await deleteNote(noteId)
setShowDropdown(null)
setStatusMessage('Deleted note')
setStatusIndicator('success')
} catch (error) {
console.error('Failed to delete note:', error)
setStatusMessage('Failed to delete note')
setStatusIndicator('error')
}
}
const handleEditNote = (noteId: string) => {
const note = notes.find(n => n.id === noteId)
if (note) {
setEditingNote({ id: noteId, content: note.content })
setEditContent(note.content)
setShowDropdown(null)
// Focus the textarea after the dialog opens
setTimeout(() => {
editTextareaRef.current?.focus()
}, 100)
}
}
const handleSaveEdit = async () => {
if (editingNote && editContent.trim() !== '') {
try {
await updateNote(editingNote.id, editContent.trim())
setEditingNote(null)
setEditContent('')
setStatusMessage('Updated note')
setStatusIndicator('success')
} catch (error) {
console.error('Failed to update note:', error)
setStatusMessage('Failed to update note')
setStatusIndicator('error')
}
}
}
const handleCancelEdit = () => {
setEditingNote(null)
setEditContent('')
}
// Handle keyboard shortcuts in edit dialog
const handleEditKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
handleSaveEdit()
} else if (e.key === 'Escape') {
e.preventDefault()
handleCancelEdit()
}
}
const toggleDropdown = (index: number, e: React.MouseEvent) => {
e.stopPropagation()
setShowDropdown(showDropdown === index ? null : index)
}
// Auto-resize on mount and when creatingNote changes
useEffect(() => {
if (creatingNote && textareaRef.current) {
textareaRef.current.style.height = 'auto'
const scrollHeight = textareaRef.current.scrollHeight
textareaRef.current.style.height = `${scrollHeight}px`
// Set container height for creating state
const minHeight = 192 // min-h-48 = 192px
const paddingAndButton = 48 // 48px padding for button space
const newContainerHeight = Math.max(
minHeight,
scrollHeight + paddingAndButton,
)
setContainerHeight(newContainerHeight)
} else if (!creatingNote) {
// Reset to default height when not creating
setContainerHeight(128) // h-32 = 128px
if (textareaRef.current) {
textareaRef.current.style.height = ''
}
}
}, [creatingNote])
// Handle scroll events
useEffect(() => {
const handleScroll = () => {
if (containerRef.current) {
const scrollTop = containerRef.current.scrollTop
setShowScrollToTop(scrollTop > 200) // Show button after scrolling 200px
}
}
const container = containerRef.current
if (container) {
container.addEventListener('scroll', handleScroll)
return () => container.removeEventListener('scroll', handleScroll)
}
return () => {}
}, [])
// Handle escape key for closing search
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && showSearch) {
closeSearch()
}
}
if (showSearch) {
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}
return () => {}
}, [showSearch])
// Handle clicks outside dropdown to close it
useEffect(() => {
const handleClickOutside = () => {
setShowDropdown(null)
}
if (showDropdown !== null) {
document.addEventListener('click', handleClickOutside)
return () => document.removeEventListener('click', handleClickOutside)
}
return () => {}
}, [showDropdown])
const scrollToTop = () => {
if (containerRef.current) {
containerRef.current.scrollTo({
top: 0,
behavior: 'smooth',
})
}
}
return (
<div
ref={containerRef}
className="w-full max-w-6xl mx-auto px-4 h-200 overflow-y-auto relative px-24"
style={{
height: '640px',
msOverflowStyle: 'none' /* Internet Explorer 10+ */,
scrollbarWidth: 'none' /* Firefox */,
}}
>
{/* Header */}
{showSearch ? (
<div className="flex items-center gap-4 mb-8 px-4 py-2 bg-white border border-gray-200 rounded-lg">
<Search className="w-5 h-5 text-gray-400 flex-shrink-0" />
<input
ref={searchInputRef}
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder="Search your notes"
className="flex-1 text-sm outline-none placeholder-gray-400"
/>
<button
onClick={closeSearch}
className="p-1 hover:bg-gray-100 rounded transition-colors flex-shrink-0"
title="Close search"
>
<X className="w-5 h-5 text-gray-400" />
</button>
</div>
) : (
<div className="flex items-center justify-between mb-8">
<h1 className="text-xl font-medium text-gray-900 w-full text-center">
What's on your mind today?
</h1>
</div>
)}
{/* Text Input Area - Only show when not searching */}
{!showSearch && (
<div
className="shadow-lg rounded-2xl mb-8 border border-gray-200 w-3/5 mx-auto transition-all duration-200 ease-in-out relative"
style={{ height: `${containerHeight}px` }}
>
{!creatingNote && (
<div className="absolute top-6 left-6 flex items-center gap-1 text-gray-500 pointer-events-none">
<AudioIcon />
<span>Take a quick note with your voice</span>
</div>
)}
<textarea
ref={textareaRef}
className={`w-full pt-6 px-6 focus:outline-none resize-none overflow-hidden ${creatingNote ? 'cursor-text' : 'cursor-pointer'}`}
value={noteContent}
onChange={e => updateNoteContent(e.target.value)}
onClick={() => setCreatingNote(true)}
onBlur={handleBlur}
placeholder={`${creatingNote ? `Press and hold ${keyboardShortcut.map(k => getKeyDisplayInfo(k, platform).label).join(' + ')} and start speaking` : ''}`}
/>
{showAddNoteButton && (
<div className="absolute bottom-3 right-3">
<button
onClick={handleAddNote}
className="bg-neutral-200 px-4 py-2 rounded-md font-semibold hover:bg-neutral-300 cursor-pointer"
>
Add note
</button>
</div>
)}
</div>
)}
<div
className={`${viewMode === 'grid' || showSearch ? '' : 'm-auto w-3/5'}`}
>
<div className="flex items-center justify-between mb-1">
<div className="text-xs text-gray-500 font-medium uppercase tracking-wide">
{showSearch
? `Search Results (${filteredNotes.length})`
: `Notes (${notes.length})`}
</div>
<div className="flex items-center gap-1">
<button
className="p-2 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
title="Search"
onClick={openSearch}
>
<Search className="w-5 h-5 text-neutral-400" />
</button>
<button
className="p-2 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
title="List view"
onClick={toggleViewMode}
>
{viewMode === 'grid' ? (
<Rows className="w-5 h-5 text-neutral-400" />
) : (
<Grid className="w-5 h-5 text-neutral-400" />
)}
</button>
</div>
</div>
<div className="w-full h-[1px] bg-slate-200 mb-4"></div>
{/* Notes Masonry Layout */}
{(showSearch ? filteredNotes.length === 0 : notes.length === 0) ? (
<div className="py-4 text-gray-500">
{showSearch ? (
<>
<p className="text-sm">No notes found</p>
<p className="text-xs mt-1">Try a different search term</p>
</>
) : (
<>
<p className="text-sm">No notes yet</p>
</>
)}
</div>
) : (
<div className="py-4">
{viewMode === 'grid' && (
<Masonry columns={{ xs: 1, sm: 2, md: 3 }} spacing={2}>
{(showSearch ? filteredNotes : notes).map((note, index) => (
<Note
key={note.id}
note={note}
index={index}
showDropdown={showDropdown}
onEdit={handleEditNote}
onToggleDropdown={toggleDropdown}
onCopyToClipboard={handleCopyToClipboard}
onDeleteNote={handleDeleteNote}
formatDate={formatDate}
formatTime={formatTime}
truncateContent={truncateContent}
searchQuery={showSearch ? searchQuery : undefined}
/>
))}
</Masonry>
)}
{viewMode === 'list' && (
<div className="flex flex-col gap-4">
{(showSearch ? filteredNotes : notes).map((note, index) => (
<Note
key={note.id}
note={note}
index={index}
showDropdown={showDropdown}
onEdit={handleEditNote}
onToggleDropdown={toggleDropdown}
onCopyToClipboard={handleCopyToClipboard}
onDeleteNote={handleDeleteNote}
formatDate={formatDate}
formatTime={formatTime}
truncateContent={truncateContent}
searchQuery={showSearch ? searchQuery : undefined}
/>
))}
</div>
)}
</div>
)}
</div>
{/* Scroll to Top Button */}
{showScrollToTop && (
<button
onClick={scrollToTop}
className="fixed bottom-8 bg-black text-white right-8 w-8 h-8 rounded-full shadow-lg hover:shadow-xl hover:-translate-y-1 transition-all duration-200 flex items-center justify-center group z-50 cursor-pointer"
aria-label="Scroll to top"
>
<ArrowUp className="w-4 h-4 font-bold" />
</button>
)}
{/* Edit Note Dialog */}
<Dialog
open={!!editingNote}
onOpenChange={open => !open && handleCancelEdit()}
>
<DialogContent
className="!border-0 shadow-lg p-0"
showCloseButton={false}
>
<DialogHeader>
<DialogTitle className="sr-only">Edit Note</DialogTitle>
</DialogHeader>
<div>
<textarea
ref={editTextareaRef}
value={editContent}
onChange={e => setEditContent(e.target.value)}
onKeyDown={handleEditKeyDown}
className="w-full px-4 rounded-md resize-none focus:outline-none border-0"
rows={6}
placeholder="Edit your note..."
/>
</div>
<DialogFooter className="p-4">
<Button
className="bg-neutral-200 hover:bg-neutral-300 text-black cursor-pointer"
onClick={handleCancelEdit}
>
Cancel
</Button>
<Button
className="cursor-pointer"
onClick={handleSaveEdit}
disabled={!editContent.trim()}
>
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Status Indicator */}
<StatusIndicator
status={statusIndicator}
onHide={() => {
setStatusIndicator(null)
setStatusMessage('')
}}
successMessage={statusMessage}
errorMessage={statusMessage}
/>
</div>
)
}
================================================
FILE: app/components/home/contents/SettingsContent.tsx
================================================
import { useMainStore } from '@/app/store/useMainStore'
import GeneralSettingsContent from './settings/GeneralSettingsContent'
import AudioSettingsContent from './settings/AudioSettingsContent'
import AccountSettingsContent from './settings/AccountSettingsContent'
import KeyboardSettingsContent from './settings/KeyboardSettingsContent'
import AdvancedSettingsContent from './settings/AdvancedSettingsContent'
import PricingBillingSettingsContent from './settings/PricingBillingSettingsContent'
export default function SettingsContent() {
const { settingsPage, setSettingsPage } = useMainStore()
const settingsMenuItems = [
{ id: 'general', label: 'General', active: settingsPage === 'general' },
{ id: 'keyboard', label: 'Keyboard', active: settingsPage === 'keyboard' },
{ id: 'audio', label: 'Audio & Mic', active: settingsPage === 'audio' },
{
id: 'pricing-billing',
label: 'Pricing & Billing',
active: settingsPage === 'pricing-billing',
},
{ id: 'account', label: 'Account', active: settingsPage === 'account' },
{ id: 'advanced', label: 'Advanced', active: settingsPage === 'advanced' },
]
const renderSettingsContent = () => {
switch (settingsPage) {
case 'general':
return <GeneralSettingsContent />
case 'keyboard':
return <KeyboardSettingsContent />
case 'audio':
return <AudioSettingsContent />
case 'pricing-billing':
return <PricingBillingSettingsContent />
case 'account':
return <AccountSettingsContent />
case 'advanced':
return <AdvancedSettingsContent />
default:
return <GeneralSettingsContent />
}
}
return (
<div className="w-full px-32">
<div className="space-y-6">
{/* Horizontal Tab/Pill Selector */}
<div className="flex gap-1 p-1 bg-slate-100 rounded-lg w-fit mx-auto">
{settingsMenuItems.map(item => (
<button
key={item.id}
onClick={() => setSettingsPage(item.id as any)}
className={`px-4 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
item.active
? 'bg-white text-slate-900 shadow-sm'
: 'text-slate-600 hover:text-slate-900 hover:bg-slate-50'
}`}
>
{item.label}
</button>
))}
</div>
{/* Content Area */}
<div className="w-full pt-8">{renderSettingsContent()}</div>
</div>
</div>
)
}
================================================
FILE: app/components/home/contents/activityMessages.ts
================================================
// Activity message categories and levels
export interface ActivityMessage {
text: string
}
export interface ActivityMessageLevel {
messages: ActivityMessage[]
}
export interface ActivityMessageCategory {
levels: ActivityMessageLevel[]
}
// Weekly Streak Messages
export const STREAK_MESSAGES: ActivityMessageCategory = {
levels: [
{
messages: [
{ text: 'Momentum starts now 🚀' },
{ text: "You're doing it! ❤️" },
{ text: 'Your spark just lit my heart ✨' },
{ text: "Great start! I've got your back 💪" },
],
},
{
messages: [
{ text: "You're on a roll 🌀" },
{ text: 'Your rhythm makes me grin 😁' },
{ text: "Keep going, we're in this! 🙌" },
{ text: 'Streak climbing 📈' },
{ text: 'Love the consistency 💕' },
],
},
{
messages: [
{ text: 'A month strong! 💪' },
{ text: 'Your streak inspires me daily 🌟' },
{ text: 'Dedication looks good on you 😎' },
{ text: 'Dedication unlocked 🔓' },
{ text: "We're building greatness together 🧱" },
],
},
{
messages: [
{ text: '🔥🔥🔥🔥🔥' },
{ text: 'Persistence icon 👑' },
{ text: "You're unstoppable, I feel it! 💥" },
{ text: 'Elite status earned 🌟' },
{ text: "Let's keep this magic alive ✨" },
],
},
],
}
// Average Speed Messages
export const SPEED_MESSAGES: ActivityMessageCategory = {
levels: [
{
messages: [
{ text: 'Warm-up complete 🔥' },
{ text: "Take your time, I'm listening 🧏" },
{ text: 'Starting steady 🎯' },
{ text: 'Great pace! Keep going!' },
],
},
{
messages: [
{ text: "Nice pace! I'm smiling big 😁" },
{ text: 'Flowing like friends chatting 🗣️' },
{ text: 'Love this tempo, keep riffing 🎸' },
{ text: 'You talk, I dance along 💃' },
{ text: 'Our sync feels awesome 🎧' },
],
},
{
messages: [
{ text: "Now we're talking!" },
{ text: 'Flow state achieved!' },
{ text: "You're on fire, I'm hype 🔥" },
{ text: 'Smooth operator! 💃' },
{ text: 'Your flow fuels me 🚀' },
],
},
{
messages: [
{ text: "Lightning! I'm awed 🤯" },
{ text: 'Top 1% - I knew you could! 🌟' },
{ text: "World can't match your pace 😎" },
{ text: 'Speed demon! 💥' },
{ text: 'I race to keep up! 😂' },
],
},
],
}
// Total Words Messages
export const TOTAL_WORDS_MESSAGES: ActivityMessageCategory = {
levels: [
{
messages: [
{ text: 'Every word counts!' },
{ text: "Seed planted, I'm excited 🌱" },
{ text: 'Great beginning!' },
{ text: "Story begins: I'm hooked 📖" },
{ text: 'Love hearing every word 🥰' },
],
},
{
messages: [
{ text: 'Thousands in! Proud partner 🙌' },
{ text: "Now that's a short story!" },
{ text: "Paragraph party and I'm invited 🥳" },
{ text: 'Ideas streaming 🌊' },
{ text: 'Nice momentum 🚀' },
],
},
{
messages: [
{ text: 'Dictation natural!' },
{ text: 'Prolific vibes, my friend 🎶' },
{ text: 'Word mountain rising ⛰️' },
{ text: 'Author mode on 📝' },
{ text: 'Consistency royalty 👑' },
],
},
{
messages: [
{ text: 'Library worth of words! 📚' },
{ text: 'Status: Living legend 🔥' },
{ text: 'Wordsmith wizardry 🪄' },
{ text: 'You dictate history, buddy 🏛️' },
{ text: "My pride can't fit the page 😍" },
],
},
],
}
export const getStreakLevel = (streakDays: number): number => {
if (streakDays < 7) return 0
if (streakDays < 21) return 1
if (streakDays < 56) return 2
return 3
}
export const getSpeedLevel = (averageWPM: number): number => {
if (averageWPM <= 100) return 0
if (averageWPM <= 200) return 1
if (averageWPM <= 300) return 2
return 3
}
export const getTotalWordsLevel = (totalWords: number): number => {
if (totalWords <= 1000) return 0
if (totalWords <= 5000) return 1
if (totalWords <= 25000) return 2
return 3
}
export const getActivityMessage = (
category: ActivityMessageCategory,
level: number,
): string => {
const messages = category.levels[level]?.messages || []
if (messages.length === 0) return 'You are off to great start'
const hour = new Date().getHours()
const seed = hour % messages.length
return messages[seed].text
}
================================================
FILE: app/components/home/contents/settings/AccountSettingsContent.tsx
================================================
import React, { useState } from 'react'
import { useNotesStore } from '../../../../store/useNotesStore'
import { useDictionaryStore } from '../../../../store/useDictionaryStore'
import { useOnboardingStore } from '../../../../store/useOnboardingStore'
import { Button } from '../../../ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '../../../ui/dialog'
import { useAuthStore } from '@/app/store/useAuthStore'
import { useAuth } from '@/app/components/auth/useAuth'
export default function AccountSettingsContent() {
const { user, setName, clearAuth } = useAuthStore()
const { logoutUser } = useAuth()
const { loadNotes } = useNotesStore()
const { loadEntries } = useDictionaryStore()
const { resetOnboarding } = useOnboardingStore()
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const handleSignOut = async () => {
try {
await logoutUser()
} catch (error) {
console.error('Logout failed:', error)
}
}
const handleDeleteAccount = async () => {
try {
// Delete user data from both local and server databases
// Server now extracts userId from authenticated user's token
await window.api.deleteUserData()
// Clear KV-backed app state
window.electron.store.set('settings', {})
window.electron.store.set('main', {})
window.electron.store.set('onboarding', {})
window.electron.store.set('auth', {})
// Clear auth state
clearAuth(false)
// Reset all stores to their initial state
resetOnboarding()
loadNotes()
loadEntries()
// Close the dialog
setShowDeleteDialog(false)
// Note: The app will automatically navigate to onboarding since user is no longer authenticated
} catch (error) {
console.error('Failed to delete account data:', error)
// Still proceed with local cleanup even if server deletion fails
// Clear KV-backed app state
window.electron.store.set('settings', {})
window.electron.store.set('main', {})
window.electron.store.set('onboarding', {})
window.electron.store.set('auth', {})
// Clear auth state
clearAuth(false)
// Reset all stores to their initial state
resetOnboarding()
loadNotes()
loadEntries()
// Close the dialog
setShowDeleteDialog(false)
}
}
return (
<div className="h-full justify-between">
<div className="space-y-6">
{/* First name */}
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-gray-900">Name</label>
<input
type="text"
value={user?.name}
onChange={e => setName(e.target.value)}
className="w-80 bg-white border border-gray-300 rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
{/* Email */}
<div className="flex items-center justify-between py-3 my-1">
<label className="text-sm font-medium text-gray-900">Email</label>
<div className="w-80 text-sm text-gray-600 px-4">{user?.email}</div>
</div>
</div>
{/* Action buttons */}
<div className="flex pt-8 w-full justify-center">
<Button
variant="outline"
size="lg"
onClick={handleSignOut}
className="px-6 py-3 bg-neutral-200 text-neutral-700 hover:bg-neutral-300"
>
Sign out
</Button>
</div>
<div className="flex pt-12 w-full justify-center">
<Button
variant="ghost"
size="lg"
onClick={() => setShowDeleteDialog(true)}
className="px-6 py-3 text-red-400 hover:text-red-200"
>
Delete account
</Button>
</div>
{/* Delete Confirmation Dialog */}
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="text-red-600">Delete Account</DialogTitle>
<DialogDescription className="text-gray-600">
Are you absolutely sure you want to delete your account? This
action cannot be undone and will permanently remove:
<br />
<br />
gitextract_bmhz2bpu/ ├── .claude/ │ └── settings.local.json ├── .cursor/ │ └── rules/ │ ├── always.mdc │ ├── code-conventions.mdc │ ├── react.mdc │ └── typescript.mdc ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── app-deploy.yml │ ├── autolink-pr-to-issue.yml │ ├── build-image.yml │ ├── build.yml │ ├── ci-controller.yml │ ├── deploy-server.yml │ ├── infra-deploy.yml │ ├── native-build-check.yml │ └── test-runner.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode/ │ └── settings.json ├── AGENTS.md ├── CLAUDE.md ├── LICENSE ├── README.md ├── app/ │ ├── app.tsx │ ├── assets/ │ │ ├── .gitignore │ │ ├── accesssibility.webm │ │ └── microphone.webm │ ├── components/ │ │ ├── analytics/ │ │ │ └── index.ts │ │ ├── auth/ │ │ │ ├── Auth0Provider.tsx │ │ │ └── useAuth.ts │ │ ├── home/ │ │ │ ├── HomeKit.tsx │ │ │ ├── ProUpgradeDialog.tsx │ │ │ └── contents/ │ │ │ ├── AboutContent.tsx │ │ │ ├── DictionaryContent.tsx │ │ │ ├── HomeContent.tsx │ │ │ ├── NotesContent.tsx │ │ │ ├── SettingsContent.tsx │ │ │ ├── activityMessages.ts │ │ │ └── settings/ │ │ │ ├── AccountSettingsContent.tsx │ │ │ ├── AdvancedSettingsContent.tsx │ │ │ ├── AudioSettingsContent.tsx │ │ │ ├── GeneralSettingsContent.tsx │ │ │ ├── KeyboardSettingsContent.tsx │ │ │ └── PricingBillingSettingsContent.tsx │ │ ├── icons/ │ │ │ ├── AppleIcon.tsx │ │ │ ├── AppleNotesIcon.tsx │ │ │ ├── AsterikIcon.tsx │ │ │ ├── AudioIcon.tsx │ │ │ ├── AvatarIcon.tsx │ │ │ ├── ChatGPTIcon.tsx │ │ │ ├── ClaudeIcon.tsx │ │ │ ├── CodeWindowIcon.tsx │ │ │ ├── ColorSchemeIcon.tsx │ │ │ ├── CursorIcon.tsx │ │ │ ├── DiscordIcon.tsx │ │ │ ├── FanIcon.tsx │ │ │ ├── GitHubIcon.tsx │ │ │ ├── GmailIcon.tsx │ │ │ ├── GoogleIcon.tsx │ │ │ ├── IMessageIcon.tsx │ │ │ ├── ItoIcon.tsx │ │ │ ├── MicrosoftIcon.tsx │ │ │ ├── NotionIcon.tsx │ │ │ ├── SlackIcon.tsx │ │ │ ├── SpeedIcon.tsx │ │ │ ├── TotalWordsIcon.tsx │ │ │ ├── VSCodeIcon.tsx │ │ │ └── XIcon.tsx │ │ ├── pill/ │ │ │ ├── Pill.tsx │ │ │ └── contents/ │ │ │ ├── AudioBars.tsx │ │ │ ├── AudioBarsBase.tsx │ │ │ ├── LoadingAnimation.tsx │ │ │ ├── PreviewAudioBars.tsx │ │ │ └── TooltipButton.tsx │ │ ├── ui/ │ │ │ ├── animated-checkmark.tsx │ │ │ ├── app-orbit-image.tsx │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── keyboard-key.tsx │ │ │ ├── keyboard-shortcut-editor.tsx │ │ │ ├── microphone-selector.tsx │ │ │ ├── multi-shortcut-editor.tsx │ │ │ ├── nav-item.tsx │ │ │ ├── note.tsx │ │ │ ├── spinner.tsx │ │ │ ├── status-indicator.tsx │ │ │ ├── switch.tsx │ │ │ ├── tip.tsx │ │ │ └── tooltip.tsx │ │ ├── welcome/ │ │ │ ├── WelcomeKit.tsx │ │ │ ├── contents/ │ │ │ │ ├── AnyAppContent.tsx │ │ │ │ ├── CheckEmailContent.tsx │ │ │ │ ├── CreateAccountContent.tsx │ │ │ │ ├── DataControlContent.tsx │ │ │ │ ├── EmailLoginContent.tsx │ │ │ │ ├── EmailSignupContent.tsx │ │ │ │ ├── GoodToGoContent.tsx │ │ │ │ ├── IntroducingIntelligentModeContent.tsx │ │ │ │ ├── KeyboardTestContext.tsx │ │ │ │ ├── MicrophoneTestContent.tsx │ │ │ │ ├── PermissionsContent.tsx │ │ │ │ ├── ReferralContent.tsx │ │ │ │ ├── SignInContent.tsx │ │ │ │ └── TryItOutContent.tsx │ │ │ └── styles.css │ │ └── window/ │ │ ├── OnboardingTitlebar.tsx │ │ ├── Titlebar.tsx │ │ ├── TitlebarContext.tsx │ │ └── WindowContext.tsx │ ├── generated/ │ │ ├── buf/ │ │ │ └── validate/ │ │ │ └── validate_pb.ts │ │ ├── ito_connect.ts │ │ └── ito_pb.ts │ ├── hooks/ │ │ ├── useBillingState.test.ts │ │ ├── useBillingState.ts │ │ ├── useDeviceChangeListener.ts │ │ └── usePlatform.ts │ ├── index.d.ts │ ├── index.html │ ├── media/ │ │ └── microphone.ts │ ├── renderer.tsx │ ├── sentry.ts │ ├── store/ │ │ ├── useAdvancedSettingsStore.ts │ │ ├── useAudioStore.ts │ │ ├── useAuthStore.ts │ │ ├── useDictionaryStore.ts │ │ ├── useMainStore.ts │ │ ├── useNotesStore.ts │ │ ├── useOnboardingStore.ts │ │ ├── usePermissionsStore.ts │ │ ├── useSettingsStore.ts │ │ ├── useShortcutEditingStore.ts │ │ └── useUserMetadataStore.ts │ ├── styles/ │ │ ├── app.css │ │ ├── globals.css │ │ └── window.css │ └── utils/ │ ├── audioUtils.ts │ ├── healthCheck.test.ts │ ├── healthCheck.ts │ ├── keyboard.test.ts │ ├── keyboard.ts │ └── utils.ts ├── build/ │ ├── entitlements.mac.inherit.plist │ └── entitlements.mac.plist ├── build-app.sh ├── build-binaries.sh ├── commitlint.config.js ├── components.json ├── dev-app-update.yml ├── electron-builder.config.js ├── electron.vite.config.ts ├── eslint.config.mjs ├── lib/ │ ├── __tests__/ │ │ ├── fixtures/ │ │ │ ├── auth.ts │ │ │ └── database.ts │ │ ├── helpers/ │ │ │ └── testUtils.ts │ │ ├── mocks/ │ │ │ ├── electron.ts │ │ │ └── sqlite.ts │ │ └── setup.ts │ ├── auth/ │ │ ├── config.test.ts │ │ ├── config.ts │ │ ├── events.test.ts │ │ └── events.ts │ ├── clients/ │ │ ├── grpcClient.test.ts │ │ ├── grpcClient.ts │ │ └── itoHttpClient.ts │ ├── constants/ │ │ ├── external-links.ts │ │ ├── generated-defaults.ts │ │ ├── keyboard-defaults.ts │ │ ├── store-keys.test.ts │ │ └── store-keys.ts │ ├── main/ │ │ ├── app.ts │ │ ├── appNap.ts │ │ ├── audio/ │ │ │ ├── AudioStreamManager.test.ts │ │ │ └── AudioStreamManager.ts │ │ ├── autoUpdaterWrapper.ts │ │ ├── context/ │ │ │ └── ContextGrabber.ts │ │ ├── env.ts │ │ ├── grammar/ │ │ │ ├── GrammarRulesService.test.ts │ │ │ └── GrammarRulesService.ts │ │ ├── index.d.ts │ │ ├── interactions/ │ │ │ ├── InteractionManager.test.ts │ │ │ └── InteractionManager.ts │ │ ├── itoSessionManager.test.ts │ │ ├── itoSessionManager.ts │ │ ├── itoStreamController.test.ts │ │ ├── itoStreamController.ts │ │ ├── logger.ts │ │ ├── main.ts │ │ ├── recordingStateNotifier.ts │ │ ├── sentry.ts │ │ ├── sqlite/ │ │ │ ├── db.test.ts │ │ │ ├── db.ts │ │ │ ├── migrations.ts │ │ │ ├── models.ts │ │ │ ├── repo.test.ts │ │ │ ├── repo.ts │ │ │ ├── schema.ts │ │ │ └── utils.ts │ │ ├── store.test.ts │ │ ├── store.ts │ │ ├── syncService.test.ts │ │ ├── syncService.ts │ │ ├── teardown.ts │ │ ├── text/ │ │ │ ├── TextInserter.test.ts │ │ │ └── TextInserter.ts │ │ ├── timing/ │ │ │ ├── TimingCollector.test.ts │ │ │ └── TimingCollector.ts │ │ ├── tray.ts │ │ ├── voiceInputService.test.ts │ │ └── voiceInputService.ts │ ├── media/ │ │ ├── IAccessibilityContextProvider.ts │ │ ├── active-application.test.ts │ │ ├── active-application.ts │ │ ├── audio.test.ts │ │ ├── audio.ts │ │ ├── keyboard.test.ts │ │ ├── keyboard.ts │ │ ├── macOSAccessibilityContextProvider.ts │ │ ├── microphoneSetUp.ts │ │ ├── native-interface.test.ts │ │ ├── native-interface.ts │ │ ├── selected-text-reader.test.ts │ │ ├── selected-text-reader.ts │ │ ├── systemAudio.ts │ │ └── text-writer.ts │ ├── preload/ │ │ ├── api.test.ts │ │ ├── api.ts │ │ ├── index.d.ts │ │ └── preload.ts │ ├── protocol/ │ │ └── index.ts │ ├── types/ │ │ ├── cursorContext.ts │ │ ├── ipc.ts │ │ └── keyboard.ts │ ├── utils/ │ │ ├── applicationDetection.ts │ │ ├── crossPlatform.ts │ │ ├── settings.test.ts │ │ └── settings.ts │ ├── utils.ts │ └── window/ │ ├── index.ts │ ├── ipcDev.ts │ ├── ipcEvents.test.ts │ └── ipcEvents.ts ├── native/ │ ├── Cargo.toml │ ├── active-application/ │ │ ├── Cargo.toml │ │ ├── active-application.manifest │ │ ├── build.rs │ │ └── src/ │ │ └── main.rs │ ├── audio-recorder/ │ │ ├── Cargo.toml │ │ ├── audio-recorder.manifest │ │ ├── build.rs │ │ └── src/ │ │ └── main.rs │ ├── clippy.toml │ ├── cursor-context/ │ │ ├── Package.resolved │ │ ├── Package.swift │ │ └── Sources/ │ │ └── cursor-context/ │ │ ├── CLI.swift │ │ └── main.swift │ ├── global-key-listener/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── build.rs │ │ ├── global-key-listener.manifest │ │ └── src/ │ │ ├── key_codes.rs │ │ └── main.rs │ ├── macos-text/ │ │ ├── .gitignore │ │ ├── Package.swift │ │ └── Sources/ │ │ └── focused-text-reader/ │ │ └── main.swift │ ├── rustfmt.toml │ ├── selected-text-reader/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ ├── selected-text-reader.manifest │ │ └── src/ │ │ ├── macos.rs │ │ ├── main.rs │ │ └── windows.rs │ └── text-writer/ │ ├── Cargo.toml │ ├── build.rs │ ├── src/ │ │ ├── macos_writer.rs │ │ ├── main.rs │ │ └── windows_writer.rs │ └── text-writer.manifest ├── package.json ├── resources/ │ └── build/ │ ├── entitlements.mac.plist │ └── icon.icns ├── scripts/ │ ├── clean-app-data.js │ └── generate-constants.js ├── server/ │ ├── .dockerignore │ ├── Dockerfile │ ├── README.md │ ├── buf.gen.yaml │ ├── buf.yaml │ ├── docker-compose.yml │ ├── infra/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── bin/ │ │ │ └── infra.ts │ │ ├── cdk.context.json │ │ ├── cdk.json │ │ ├── jest.config.js │ │ ├── lambdas/ │ │ │ ├── firehose-transform.ts │ │ │ ├── opensearch-bootstrap.ts │ │ │ ├── run-migration.ts │ │ │ └── timing-merger.ts │ │ ├── lib/ │ │ │ ├── cicd-stack.ts │ │ │ ├── constants.ts │ │ │ ├── helpers.ts │ │ │ ├── network-stack.ts │ │ │ ├── observability-stack.ts │ │ │ ├── platform-stack.ts │ │ │ ├── security-stack.ts │ │ │ ├── service/ │ │ │ │ ├── fargate-task.ts │ │ │ │ ├── firehose-config.ts │ │ │ │ ├── index.ts │ │ │ │ ├── log-groups.ts │ │ │ │ ├── migration-lambda.ts │ │ │ │ └── opensearch-bootstrap.ts │ │ │ ├── service-stack.ts │ │ │ └── timing-config.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── package.json │ ├── scripts/ │ │ ├── migrate-audio-to-s3.ts │ │ ├── migrate.sh │ │ └── setup-minio.sh │ ├── src/ │ │ ├── auth/ │ │ │ ├── auth0Helpers.ts │ │ │ └── userContext.ts │ │ ├── clients/ │ │ │ ├── asrConfig.ts │ │ │ ├── cerebrasClient.ts │ │ │ ├── errors.ts │ │ │ ├── groqClient.test.ts │ │ │ ├── groqClient.ts │ │ │ ├── intentTranscriptionConfig.ts │ │ │ ├── llmProvider.ts │ │ │ ├── providerUtils.ts │ │ │ ├── providers.ts │ │ │ └── s3storageClient.ts │ │ ├── constants/ │ │ │ ├── generated-defaults.ts │ │ │ ├── markers.ts │ │ │ └── storage.ts │ │ ├── db/ │ │ │ ├── models.ts │ │ │ └── repo.ts │ │ ├── db.ts │ │ ├── generated/ │ │ │ ├── buf/ │ │ │ │ └── validate/ │ │ │ │ └── validate_pb.ts │ │ │ ├── ito_connect.ts │ │ │ └── ito_pb.ts │ │ ├── index.ts │ │ ├── ito.proto │ │ ├── migrations/ │ │ │ ├── 1722889955000_initial_schema.js │ │ │ ├── 1752006262324_add-raw-audio-column.js │ │ │ ├── 1752099660683_add-duration-ms.js │ │ │ ├── 1753297915000_add-advanced-settings.js │ │ │ ├── 1754938499581_update-advanced-settings.js │ │ │ ├── 1756922843670_raw-audio-reference.js │ │ │ ├── 1760496947939_add-temporary-analytics-token.js │ │ │ ├── 1761765111646_add-user-trials.js │ │ │ ├── 1761778190395_add-user-subscriptions.js │ │ │ ├── 1762468699097_add-subscription-end-at.js │ │ │ ├── 1763753112000_make-llm-settings-nullable.js │ │ │ └── schema/ │ │ │ └── initial.js │ │ ├── prompts/ │ │ │ ├── transcription.test.ts │ │ │ └── transcription.ts │ │ ├── server.ts │ │ ├── services/ │ │ │ ├── __tests__/ │ │ │ │ └── helpers.ts │ │ │ ├── auth0.ts │ │ │ ├── billing.test.ts │ │ │ ├── billing.ts │ │ │ ├── cloudWatchLogger.ts │ │ │ ├── errorInterceptor.ts │ │ │ ├── ito/ │ │ │ │ ├── audioUtils.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── helpers.ts │ │ │ │ ├── itoService.ts │ │ │ │ ├── timingService.ts │ │ │ │ ├── transcribeStreamHandler.ts │ │ │ │ ├── transcribeStreamV2Handler.ts │ │ │ │ └── types.ts │ │ │ ├── logging.test.ts │ │ │ ├── logging.ts │ │ │ ├── loggingInterceptor.ts │ │ │ ├── stripeWebhook.test.ts │ │ │ ├── stripeWebhook.ts │ │ │ ├── timing/ │ │ │ │ └── ServerTimingCollector.ts │ │ │ ├── trial.test.ts │ │ │ ├── trial.ts │ │ │ ├── validationInterceptor.test.ts │ │ │ └── validationInterceptor.ts │ │ ├── utils/ │ │ │ ├── abortUtils.ts │ │ │ ├── audio.ts │ │ │ ├── audioProcessing.ts │ │ │ └── renderCallback.ts │ │ └── validation/ │ │ ├── HeaderValidator.test.ts │ │ ├── HeaderValidator.ts │ │ ├── schemas.test.ts │ │ └── schemas.ts │ ├── test-client.ts │ └── tsconfig.json ├── shared-constants.js ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.web.json └── vite-env.d.ts
SYMBOL INDEX (1208 symbols across 233 files)
FILE: app/app.tsx
function App (line 52) | function App() {
FILE: app/components/analytics/index.ts
type BaseEventProperties (line 126) | interface BaseEventProperties {
type OnboardingEventProperties (line 132) | interface OnboardingEventProperties extends BaseEventProperties {
type HotkeyEventProperties (line 141) | interface HotkeyEventProperties extends BaseEventProperties {
type AuthEventProperties (line 148) | interface AuthEventProperties extends BaseEventProperties {
type SettingsEventProperties (line 154) | interface SettingsEventProperties extends BaseEventProperties {
type UserProperties (line 161) | interface UserProperties {
constant ANALYTICS_EVENTS (line 174) | const ANALYTICS_EVENTS = {
type AnalyticsEvent (line 206) | type AnalyticsEvent =
class AnalyticsService (line 213) | class AnalyticsService {
method constructor (line 220) | constructor() {
method enableAnalytics (line 231) | async enableAnalytics() {
method disableAnalytics (line 254) | disableAnalytics() {
method isEnabled (line 269) | isEnabled(): boolean {
method identifyUser (line 276) | identifyUser(
method updateUserProperties (line 321) | updateUserProperties(properties: Partial<UserProperties>) {
method track (line 340) | track(eventName: AnalyticsEvent, properties: BaseEventProperties = {}) {
method trackOnboarding (line 367) | trackOnboarding(
method trackAuth (line 385) | trackAuth(
method trackSettings (line 402) | trackSettings(
method trackPermission (line 419) | trackPermission(
method resetUser (line 436) | resetUser() {
method getSessionDuration (line 455) | getSessionDuration(): number {
method isUserIdentified (line 462) | isUserIdentified(): boolean {
method getDeviceId (line 469) | getDeviceId(): string | null {
method updateInitializationStatus (line 476) | updateInitializationStatus(isInitialized: boolean, deviceId: string | ...
method shouldTrack (line 487) | private shouldTrack(): boolean {
FILE: app/components/auth/Auth0Provider.tsx
type Auth0ProviderProps (line 5) | interface Auth0ProviderProps {
FILE: app/components/auth/useAuth.ts
function hydrateOnboardingState (line 55) | async function hydrateOnboardingState(context?: string): Promise<void> {
FILE: app/components/home/HomeKit.tsx
function HomeKit (line 23) | function HomeKit() {
FILE: app/components/home/ProUpgradeDialog.tsx
type ProUpgradeDialogProps (line 8) | interface ProUpgradeDialogProps {
function ProUpgradeDialog (line 13) | function ProUpgradeDialog({
function FeatureItem (line 131) | function FeatureItem({ text }: { text: string }) {
FILE: app/components/home/contents/AboutContent.tsx
type AboutCardProps (line 9) | interface AboutCardProps {
function AboutCard (line 17) | function AboutCard({
function AboutContent (line 44) | function AboutContent() {
FILE: app/components/home/contents/DictionaryContent.tsx
function DictionaryContent (line 16) | function DictionaryContent() {
FILE: app/components/home/contents/HomeContent.tsx
type InteractionStats (line 36) | interface InteractionStats {
type HomeContentProps (line 67) | interface HomeContentProps {
function HomeContent (line 71) | function HomeContent({
FILE: app/components/home/contents/NotesContent.tsx
function NotesContent (line 21) | function NotesContent() {
FILE: app/components/home/contents/SettingsContent.tsx
function SettingsContent (line 9) | function SettingsContent() {
FILE: app/components/home/contents/activityMessages.ts
type ActivityMessage (line 2) | interface ActivityMessage {
type ActivityMessageLevel (line 6) | interface ActivityMessageLevel {
type ActivityMessageCategory (line 10) | interface ActivityMessageCategory {
constant STREAK_MESSAGES (line 15) | const STREAK_MESSAGES: ActivityMessageCategory = {
constant SPEED_MESSAGES (line 56) | const SPEED_MESSAGES: ActivityMessageCategory = {
constant TOTAL_WORDS_MESSAGES (line 97) | const TOTAL_WORDS_MESSAGES: ActivityMessageCategory = {
FILE: app/components/home/contents/settings/AccountSettingsContent.tsx
function AccountSettingsContent (line 17) | function AccountSettingsContent() {
FILE: app/components/home/contents/settings/AdvancedSettingsContent.tsx
type LlmSettingConfig (line 15) | type LlmSettingConfig = {
function formatDisplayValue (line 110) | function formatDisplayValue(value: string | number | null): string {
type SettingInputProps (line 121) | interface SettingInputProps {
function AdvancedSettingsContent (line 203) | function AdvancedSettingsContent() {
FILE: app/components/home/contents/settings/AudioSettingsContent.tsx
function AudioSettingsContent (line 5) | function AudioSettingsContent() {
FILE: app/components/home/contents/settings/GeneralSettingsContent.tsx
function GeneralSettingsContent (line 7) | function GeneralSettingsContent() {
FILE: app/components/home/contents/settings/KeyboardSettingsContent.tsx
function KeyboardSettingsContent (line 5) | function KeyboardSettingsContent() {
FILE: app/components/home/contents/settings/PricingBillingSettingsContent.tsx
type BillingPeriod (line 6) | type BillingPeriod = 'monthly' | 'annual'
function PricingBillingSettingsContent (line 8) | function PricingBillingSettingsContent() {
type PricingCardProps (line 277) | interface PricingCardProps {
function PricingCard (line 286) | function PricingCard({
FILE: app/components/icons/AppleIcon.tsx
type AppleIconProps (line 3) | interface AppleIconProps {
function AppleIcon (line 9) | function AppleIcon({
FILE: app/components/icons/AppleNotesIcon.tsx
type AppleNotesIconProps (line 3) | interface AppleNotesIconProps extends React.SVGProps<SVGSVGElement> {
function AppleNotesIcon (line 10) | function AppleNotesIcon({
FILE: app/components/icons/ChatGPTIcon.tsx
type ChatGPTIconProps (line 1) | interface ChatGPTIconProps {
FILE: app/components/icons/ClaudeIcon.tsx
type ClaudeIconProps (line 3) | interface ClaudeIconProps extends React.SVGProps<SVGSVGElement> {
function ClaudeIcon (line 10) | function ClaudeIcon({
FILE: app/components/icons/DiscordIcon.tsx
type DiscordIconProps (line 3) | interface DiscordIconProps {
function DiscordIcon (line 9) | function DiscordIcon({
FILE: app/components/icons/GitHubIcon.tsx
type GitHubIconProps (line 3) | interface GitHubIconProps {
function GitHubIcon (line 9) | function GitHubIcon({
FILE: app/components/icons/GmailIcon.tsx
type GmailIconProps (line 1) | interface GmailIconProps {
FILE: app/components/icons/GoogleIcon.tsx
type GoogleIconProps (line 3) | interface GoogleIconProps {
function GoogleIcon (line 9) | function GoogleIcon({
FILE: app/components/icons/IMessageIcon.tsx
type IMessageIconProps (line 3) | interface IMessageIconProps extends React.SVGProps<SVGSVGElement> {
function IMessageIcon (line 10) | function IMessageIcon({
FILE: app/components/icons/ItoIcon.tsx
type ItoIconProps (line 3) | interface ItoIconProps {
FILE: app/components/icons/MicrosoftIcon.tsx
type MicrosoftIconProps (line 3) | interface MicrosoftIconProps {
function MicrosoftIcon (line 9) | function MicrosoftIcon({
FILE: app/components/icons/SlackIcon.tsx
type SlackIconProps (line 1) | interface SlackIconProps {
FILE: app/components/icons/VSCodeIcon.tsx
type VSCodeIconProps (line 3) | interface VSCodeIconProps extends React.SVGProps<SVGSVGElement> {
function VSCodeIcon (line 10) | function VSCodeIcon({
FILE: app/components/icons/XIcon.tsx
type XIconProps (line 3) | interface XIconProps {
function XIcon (line 9) | function XIcon({
FILE: app/components/pill/Pill.tsx
constant BAR_UPDATE_INTERVAL (line 48) | const BAR_UPDATE_INTERVAL = 64
FILE: app/components/pill/contents/AudioBarsBase.tsx
type AudioBarsBaseProps (line 1) | interface AudioBarsBaseProps {
constant BAR_COUNT (line 6) | const BAR_COUNT = 21
FILE: app/components/pill/contents/LoadingAnimation.tsx
type LoadingAnimationProps (line 3) | interface LoadingAnimationProps {
FILE: app/components/pill/contents/TooltipButton.tsx
type TooltipButtonProps (line 7) | interface TooltipButtonProps {
FILE: app/components/ui/animated-checkmark.tsx
function AnimatedCheck (line 5) | function AnimatedCheck({ trigger }: { trigger: boolean }) {
FILE: app/components/ui/app-orbit-image.tsx
function AppOrbitImage (line 91) | function AppOrbitImage() {
FILE: app/components/ui/badge.tsx
function Badge (line 28) | function Badge({
FILE: app/components/ui/button.tsx
function Button (line 38) | function Button({
FILE: app/components/ui/dialog.tsx
function Dialog (line 7) | function Dialog({
function DialogTrigger (line 13) | function DialogTrigger({
function DialogPortal (line 19) | function DialogPortal({
function DialogClose (line 25) | function DialogClose({
function DialogOverlay (line 31) | function DialogOverlay({
function DialogContent (line 47) | function DialogContent({
function DialogHeader (line 81) | function DialogHeader({ className, ...props }: React.ComponentProps<'div...
function DialogFooter (line 91) | function DialogFooter({ className, ...props }: React.ComponentProps<'div...
function DialogTitle (line 104) | function DialogTitle({
function DialogDescription (line 117) | function DialogDescription({
FILE: app/components/ui/dropdown-menu.tsx
function DropdownMenu (line 7) | function DropdownMenu({
function DropdownMenuPortal (line 13) | function DropdownMenuPortal({
function DropdownMenuTrigger (line 21) | function DropdownMenuTrigger({
function DropdownMenuContent (line 32) | function DropdownMenuContent({
function DropdownMenuGroup (line 52) | function DropdownMenuGroup({
function DropdownMenuItem (line 60) | function DropdownMenuItem({
function DropdownMenuCheckboxItem (line 83) | function DropdownMenuCheckboxItem({
function DropdownMenuRadioGroup (line 109) | function DropdownMenuRadioGroup({
function DropdownMenuRadioItem (line 120) | function DropdownMenuRadioItem({
function DropdownMenuLabel (line 144) | function DropdownMenuLabel({
function DropdownMenuSeparator (line 164) | function DropdownMenuSeparator({
function DropdownMenuShortcut (line 177) | function DropdownMenuShortcut({
function DropdownMenuSub (line 193) | function DropdownMenuSub({
function DropdownMenuSubTrigger (line 199) | function DropdownMenuSubTrigger({
function DropdownMenuSubContent (line 223) | function DropdownMenuSubContent({
FILE: app/components/ui/keyboard-key.tsx
type KeyboardKeyProps (line 177) | interface KeyboardKeyProps extends ComponentPropsWithoutRef<'div'> {
function KeyboardKey (line 187) | function KeyboardKey({
FILE: app/components/ui/keyboard-shortcut-editor.tsx
type KeyboardShortcutEditorProps (line 12) | interface KeyboardShortcutEditorProps {
constant MAX_KEYS_PER_SHORTCUT (line 29) | const MAX_KEYS_PER_SHORTCUT = 5
function KeyboardShortcutEditor (line 31) | function KeyboardShortcutEditor({
FILE: app/components/ui/microphone-selector.tsx
type MicrophoneSelectorProps (line 17) | interface MicrophoneSelectorProps {
function MicrophoneSelector (line 32) | function MicrophoneSelector({
FILE: app/components/ui/multi-shortcut-editor.tsx
type KeyboardShortcutConfig (line 12) | interface KeyboardShortcutConfig {
type Props (line 18) | type Props = {
constant MAX_KEYS_PER_SHORTCUT (line 26) | const MAX_KEYS_PER_SHORTCUT = 5
function MultiShortcutEditor (line 28) | function MultiShortcutEditor({
FILE: app/components/ui/nav-item.tsx
type NavItemProps (line 4) | interface NavItemProps {
function NavItem (line 12) | function NavItem({
FILE: app/components/ui/note.tsx
type NoteProps (line 5) | interface NoteProps {
function highlightText (line 20) | function highlightText(text: string, searchQuery: string): React.ReactEl...
function Note (line 47) | function Note({
FILE: app/components/ui/spinner.tsx
type SpinnerContentProps (line 31) | interface SpinnerContentProps
function Spinner (line 38) | function Spinner({
FILE: app/components/ui/status-indicator.tsx
type StatusIndicatorProps (line 4) | interface StatusIndicatorProps {
function StatusIndicator (line 12) | function StatusIndicator({
FILE: app/components/ui/switch.tsx
function Switch (line 6) | function Switch({
FILE: app/components/ui/tip.tsx
function Tip (line 4) | function Tip({
FILE: app/components/ui/tooltip.tsx
function TooltipProvider (line 6) | function TooltipProvider({
function Tooltip (line 19) | function Tooltip({
function TooltipTrigger (line 29) | function TooltipTrigger({
function TooltipContent (line 35) | function TooltipContent({
FILE: app/components/welcome/WelcomeKit.tsx
function WelcomeKit (line 18) | function WelcomeKit() {
FILE: app/components/welcome/contents/AnyAppContent.tsx
function AnyAppContent (line 5) | function AnyAppContent() {
FILE: app/components/welcome/contents/CheckEmailContent.tsx
type Props (line 6) | type Props = {
function CheckEmailContent (line 14) | function CheckEmailContent({
FILE: app/components/welcome/contents/CreateAccountContent.tsx
function CreateAccountContent (line 26) | function CreateAccountContent() {
FILE: app/components/welcome/contents/DataControlContent.tsx
function DataControlContent (line 7) | function DataControlContent() {
FILE: app/components/welcome/contents/EmailLoginContent.tsx
type Props (line 7) | type Props = {
function EmailLoginContent (line 13) | function EmailLoginContent({
FILE: app/components/welcome/contents/EmailSignupContent.tsx
type Props (line 9) | type Props = {
function EmailSignupContent (line 15) | function EmailSignupContent({
FILE: app/components/welcome/contents/GoodToGoContent.tsx
function GoodToGoContent (line 5) | function GoodToGoContent() {
FILE: app/components/welcome/contents/IntroducingIntelligentModeContent.tsx
function IntroducingIntelligentMode (line 10) | function IntroducingIntelligentMode() {
FILE: app/components/welcome/contents/KeyboardTestContext.tsx
function KeyboardTestContent (line 11) | function KeyboardTestContent() {
FILE: app/components/welcome/contents/MicrophoneTestContent.tsx
function MicrophoneBars (line 7) | function MicrophoneBars({ volume }: { volume: number }) {
function MicrophoneTestContent (line 37) | function MicrophoneTestContent() {
FILE: app/components/welcome/contents/PermissionsContent.tsx
function PermissionsContent (line 17) | function PermissionsContent() {
FILE: app/components/welcome/contents/ReferralContent.tsx
function ReferralContent (line 25) | function ReferralContent() {
FILE: app/components/welcome/contents/SignInContent.tsx
constant AUTH_PROVIDERS (line 24) | const AUTH_PROVIDERS = {
type AuthButtonProps (line 64) | interface AuthButtonProps {
function AuthButton (line 73) | function AuthButton({
function SignInContent (line 112) | function SignInContent() {
FILE: app/components/welcome/contents/TryItOutContent.tsx
function TryItOut (line 17) | function TryItOut() {
FILE: app/components/window/Titlebar.tsx
type TitlebarProps (line 271) | interface TitlebarProps {
FILE: app/components/window/TitlebarContext.tsx
type TitlebarContextProps (line 26) | interface TitlebarContextProps {}
FILE: app/components/window/WindowContext.tsx
type WindowContextProps (line 59) | interface WindowContextProps {
type WindowInitProps (line 64) | interface WindowInitProps {
type WindowContextProviderProps (line 72) | interface WindowContextProviderProps {
FILE: app/generated/buf/validate/validate_pb.ts
type Rule (line 50) | type Rule = Message<"buf.validate.Rule"> & {
type MessageRules (line 93) | type MessageRules = Message<"buf.validate.MessageRules"> & {
type MessageOneofRule (line 167) | type MessageOneofRule = Message<"buf.validate.MessageOneofRule"> & {
type OneofRules (line 198) | type OneofRules = Message<"buf.validate.OneofRules"> & {
type FieldRules (line 235) | type FieldRules = Message<"buf.validate.FieldRules"> & {
type PredefinedRules (line 487) | type PredefinedRules = Message<"buf.validate.PredefinedRules"> & {
type FloatRules (line 522) | type FloatRules = Message<"buf.validate.FloatRules"> & {
type DoubleRules (line 704) | type DoubleRules = Message<"buf.validate.DoubleRules"> & {
type Int32Rules (line 886) | type Int32Rules = Message<"buf.validate.Int32Rules"> & {
type Int64Rules (line 1060) | type Int64Rules = Message<"buf.validate.Int64Rules"> & {
type UInt32Rules (line 1234) | type UInt32Rules = Message<"buf.validate.UInt32Rules"> & {
type UInt64Rules (line 1408) | type UInt64Rules = Message<"buf.validate.UInt64Rules"> & {
type SInt32Rules (line 1581) | type SInt32Rules = Message<"buf.validate.SInt32Rules"> & {
type SInt64Rules (line 1754) | type SInt64Rules = Message<"buf.validate.SInt64Rules"> & {
type Fixed32Rules (line 1927) | type Fixed32Rules = Message<"buf.validate.Fixed32Rules"> & {
type Fixed64Rules (line 2100) | type Fixed64Rules = Message<"buf.validate.Fixed64Rules"> & {
type SFixed32Rules (line 2273) | type SFixed32Rules = Message<"buf.validate.SFixed32Rules"> & {
type SFixed64Rules (line 2446) | type SFixed64Rules = Message<"buf.validate.SFixed64Rules"> & {
type BoolRules (line 2620) | type BoolRules = Message<"buf.validate.BoolRules"> & {
type StringRules (line 2668) | type StringRules = Message<"buf.validate.StringRules"> & {
type BytesRules (line 3328) | type BytesRules = Message<"buf.validate.BytesRules"> & {
type EnumRules (line 3576) | type EnumRules = Message<"buf.validate.EnumRules"> & {
type RepeatedRules (line 3698) | type RepeatedRules = Message<"buf.validate.RepeatedRules"> & {
type MapRules (line 3786) | type MapRules = Message<"buf.validate.MapRules"> & {
type AnyRules (line 3873) | type AnyRules = Message<"buf.validate.AnyRules"> & {
type DurationRules (line 3921) | type DurationRules = Message<"buf.validate.DurationRules"> & {
type TimestampRules (line 4096) | type TimestampRules = Message<"buf.validate.TimestampRules"> & {
type Violations (line 4278) | type Violations = Message<"buf.validate.Violations"> & {
type Violation (line 4341) | type Violation = Message<"buf.validate.Violation"> & {
type FieldPath (line 4440) | type FieldPath = Message<"buf.validate.FieldPath"> & {
type FieldPathElement (line 4465) | type FieldPathElement = Message<"buf.validate.FieldPathElement"> & {
type Ignore (line 4575) | enum Ignore {
type KnownRegex (line 4685) | enum KnownRegex {
FILE: app/generated/ito_pb.ts
type Empty (line 22) | type Empty = Message<"ito.Empty"> & {
type ClientError (line 35) | type ClientError = Message<"ito.ClientError"> & {
type AudioChunk (line 76) | type AudioChunk = Message<"ito.AudioChunk"> & {
type ContextInfo (line 97) | type ContextInfo = Message<"ito.ContextInfo"> & {
type StreamConfig (line 133) | type StreamConfig = Message<"ito.StreamConfig"> & {
type TranscribeStreamRequest (line 168) | type TranscribeStreamRequest = Message<"ito.TranscribeStreamRequest"> & {
type TranscriptionResponse (line 203) | type TranscriptionResponse = Message<"ito.TranscriptionResponse"> & {
type Note (line 228) | type Note = Message<"ito.Note"> & {
type CreateNoteRequest (line 275) | type CreateNoteRequest = Message<"ito.CreateNoteRequest"> & {
type GetNoteRequest (line 302) | type GetNoteRequest = Message<"ito.GetNoteRequest"> & {
type ListNotesRequest (line 319) | type ListNotesRequest = Message<"ito.ListNotesRequest"> & {
type ListNotesResponse (line 338) | type ListNotesResponse = Message<"ito.ListNotesResponse"> & {
type UpdateNoteRequest (line 355) | type UpdateNoteRequest = Message<"ito.UpdateNoteRequest"> & {
type DeleteNoteRequest (line 377) | type DeleteNoteRequest = Message<"ito.DeleteNoteRequest"> & {
type Interaction (line 397) | type Interaction = Message<"ito.Interaction"> & {
type CreateInteractionRequest (line 474) | type CreateInteractionRequest = Message<"ito.CreateInteractionRequest"> & {
type GetInteractionRequest (line 520) | type GetInteractionRequest = Message<"ito.GetInteractionRequest"> & {
type ListInteractionsRequest (line 537) | type ListInteractionsRequest = Message<"ito.ListInteractionsRequest"> & {
type ListInteractionsResponse (line 556) | type ListInteractionsResponse = Message<"ito.ListInteractionsResponse"> & {
type UpdateInteractionRequest (line 573) | type UpdateInteractionRequest = Message<"ito.UpdateInteractionRequest"> & {
type DeleteInteractionRequest (line 595) | type DeleteInteractionRequest = Message<"ito.DeleteInteractionRequest"> & {
type DictionaryItem (line 615) | type DictionaryItem = Message<"ito.DictionaryItem"> & {
type CreateDictionaryItemRequest (line 662) | type CreateDictionaryItemRequest = Message<"ito.CreateDictionaryItemRequ...
type ListDictionaryItemsRequest (line 689) | type ListDictionaryItemsRequest = Message<"ito.ListDictionaryItemsReques...
type ListDictionaryItemsResponse (line 708) | type ListDictionaryItemsResponse = Message<"ito.ListDictionaryItemsRespo...
type UpdateDictionaryItemRequest (line 725) | type UpdateDictionaryItemRequest = Message<"ito.UpdateDictionaryItemRequ...
type DeleteDictionaryItemRequest (line 752) | type DeleteDictionaryItemRequest = Message<"ito.DeleteDictionaryItemRequ...
type DeleteUserDataRequest (line 774) | type DeleteUserDataRequest = Message<"ito.DeleteUserDataRequest"> & {
type LlmSettings (line 787) | type LlmSettings = Message<"ito.LlmSettings"> & {
type AdvancedSettings (line 849) | type AdvancedSettings = Message<"ito.AdvancedSettings"> & {
type GetAdvancedSettingsRequest (line 893) | type GetAdvancedSettingsRequest = Message<"ito.GetAdvancedSettingsReques...
type UpdateAdvancedSettingsRequest (line 906) | type UpdateAdvancedSettingsRequest = Message<"ito.UpdateAdvancedSettings...
type TimingEvent (line 926) | type TimingEvent = Message<"ito.TimingEvent"> & {
type TimingReport (line 958) | type TimingReport = Message<"ito.TimingReport"> & {
type SubmitTimingReportsRequest (line 1015) | type SubmitTimingReportsRequest = Message<"ito.SubmitTimingReportsReques...
type SubmitTimingReportsResponse (line 1034) | type SubmitTimingReportsResponse = Message<"ito.SubmitTimingReportsRespo...
type ItoMode (line 1047) | enum ItoMode {
type ClientProvider (line 1071) | enum ClientProvider {
type ErrorType (line 1092) | enum ErrorType {
FILE: app/hooks/useBillingState.test.ts
function renderHook (line 76) | function renderHook<T>(hook: () => T): {
FILE: app/hooks/useBillingState.ts
type BillingState (line 3) | type BillingState = {
function useBillingState (line 15) | function useBillingState() {
FILE: app/hooks/usePlatform.ts
type Platform (line 3) | type Platform = 'darwin' | 'win32'
function usePlatform (line 5) | function usePlatform(): Platform | undefined {
FILE: app/index.d.ts
type Window (line 40) | interface Window {
type IpcApi (line 45) | interface IpcApi {
FILE: app/media/microphone.ts
type Microphone (line 3) | type Microphone = {
type MicrophoneToRender (line 8) | type MicrophoneToRender = {
function getAvailableMicrophones (line 13) | async function getAvailableMicrophones(): Promise<Microphone[]> {
function verifyStoredMicrophone (line 36) | async function verifyStoredMicrophone() {
FILE: app/store/useAdvancedSettingsStore.ts
type LlmSettings (line 4) | interface LlmSettings {
type AdvancedSettingsState (line 16) | interface AdvancedSettingsState {
FILE: app/store/useAudioStore.ts
type AudioState (line 4) | interface AudioState {
FILE: app/store/useAuthStore.ts
type AuthZustandStore (line 10) | interface AuthZustandStore {
FILE: app/store/useDictionaryStore.ts
type DictionaryEntry (line 5) | type DictionaryEntry = {
type DictionaryStore (line 22) | interface DictionaryStore {
FILE: app/store/useMainStore.ts
type PageType (line 4) | type PageType = 'home' | 'dictionary' | 'notes' | 'settings' | 'about'
type SettingsPageType (line 5) | type SettingsPageType =
type MainStore (line 13) | interface MainStore {
FILE: app/store/useNotesStore.ts
type Note (line 4) | type Note = {
type NotesStore (line 13) | interface NotesStore {
FILE: app/store/useOnboardingStore.ts
constant ONBOARDING_CATEGORIES (line 6) | const ONBOARDING_CATEGORIES = {
type OnboardingCategory (line 14) | type OnboardingCategory =
type OnboardingState (line 17) | interface OnboardingState {
constant STEP_NAMES (line 32) | const STEP_NAMES = {
constant STEP_NAMES_ARRAY (line 46) | const STEP_NAMES_ARRAY = [
FILE: app/store/usePermissionsStore.ts
type PermissionsState (line 3) | interface PermissionsState {
FILE: app/store/useSettingsStore.ts
type SettingsState (line 20) | interface SettingsState {
type SettingCategory (line 46) | type SettingCategory = 'general' | 'audio&mic' | 'keyboard' | 'account'
FILE: app/store/useShortcutEditingStore.ts
type ShortcutEditingState (line 3) | interface ShortcutEditingState {
FILE: app/store/useUserMetadataStore.ts
type UserMetadataStore (line 6) | interface UserMetadataStore {
constant DEFAULT_METADATA (line 22) | const DEFAULT_METADATA = {
FILE: app/utils/audioUtils.ts
function createStereo48kWavFromMonoPCM (line 1) | function createStereo48kWavFromMonoPCM(
FILE: app/utils/healthCheck.ts
type HealthCheckResult (line 5) | interface HealthCheckResult {
function checkLocalServerHealth (line 15) | async function checkLocalServerHealth(): Promise<HealthCheckResult> {
FILE: app/utils/keyboard.ts
function getDirectionalIndicator (line 14) | function getDirectionalIndicator(
function getKeyDisplay (line 33) | function getKeyDisplay(
type ShortcutError (line 81) | type ShortcutError =
type ShortcutResult (line 87) | type ShortcutResult = {
constant MODIFIER_SEQUENCE (line 93) | const MODIFIER_SEQUENCE = [
constant MODIFIER_INDEX (line 110) | const MODIFIER_INDEX: Record<string, number> = MODIFIER_SEQUENCE.reduce(
function normalizeKey (line 118) | function normalizeKey(raw: KeyName): KeyName {
function sortKeysCanonical (line 122) | function sortKeysCanonical(keys: KeyName[]): KeyName[] {
function normalizeChord (line 139) | function normalizeChord(keys: KeyName[]): KeyName[] {
function modifierVariants (line 144) | function modifierVariants(modifier: string): string[] {
function createReservedCombos (line 149) | function createReservedCombos(
function getReservedCombinations (line 161) | function getReservedCombinations(
function isReservedCombination (line 197) | function isReservedCombination(
function isDuplicateShortcut (line 229) | function isDuplicateShortcut(
function validateShortcutForDuplicate (line 260) | function validateShortcutForDuplicate(
class KeyState (line 281) | class KeyState {
method constructor (line 285) | constructor(shortcut: KeyName[] = []) {
method updateShortcut (line 293) | updateShortcut(shortcut: KeyName[]) {
method update (line 302) | update(event: KeyEvent) {
method getPressedKeys (line 322) | getPressedKeys(): string[] {
method isKeyPressed (line 331) | isKeyPressed(key: KeyName): boolean {
method clear (line 338) | clear() {
FILE: lib/__tests__/fixtures/auth.ts
constant VALID_JWT_TOKEN (line 2) | const VALID_JWT_TOKEN =
constant EXPIRED_JWT_TOKEN (line 5) | const EXPIRED_JWT_TOKEN =
constant MALFORMED_JWT_TOKEN (line 8) | const MALFORMED_JWT_TOKEN = 'invalid.jwt.token'
constant VALID_AUTH0_CONFIG (line 11) | const VALID_AUTH0_CONFIG = {
constant INCOMPLETE_AUTH0_CONFIG (line 21) | const INCOMPLETE_AUTH0_CONFIG = {
constant VALID_TOKEN_RESPONSE (line 29) | const VALID_TOKEN_RESPONSE = {
constant REFRESH_TOKEN_RESPONSE (line 37) | const REFRESH_TOKEN_RESPONSE = {
constant TOKEN_ERROR_RESPONSE (line 45) | const TOKEN_ERROR_RESPONSE = {
constant SAMPLE_USER_PROFILE (line 51) | const SAMPLE_USER_PROFILE = {
constant SAMPLE_USER_PROFILE_MINIMAL (line 60) | const SAMPLE_USER_PROFILE_MINIMAL = {
constant SAMPLE_AUTH_STATE (line 66) | const SAMPLE_AUTH_STATE = {
constant SAMPLE_STORED_AUTH (line 73) | const SAMPLE_STORED_AUTH = {
constant SAMPLE_EXPIRED_AUTH (line 85) | const SAMPLE_EXPIRED_AUTH = {
constant VALID_ENV_VARS (line 116) | const VALID_ENV_VARS = {
constant INCOMPLETE_ENV_VARS (line 122) | const INCOMPLETE_ENV_VARS = {
FILE: lib/__tests__/fixtures/database.ts
constant TEST_USER_ID (line 8) | const TEST_USER_ID = 'test-user-123'
constant TEST_USER_ID_2 (line 9) | const TEST_USER_ID_2 = 'test-user-456'
FILE: lib/__tests__/helpers/testUtils.ts
function fakeTimers (line 5) | function fakeTimers() {
FILE: lib/__tests__/setup.ts
method constructor (line 35) | constructor() {
method loadURL (line 42) | loadURL() {}
method loadFile (line 43) | loadFile() {}
method on (line 44) | on() {}
method once (line 45) | once() {}
method show (line 46) | show() {}
method hide (line 47) | hide() {}
method close (line 48) | close() {}
method destroy (line 49) | destroy() {}
method minimize (line 50) | minimize() {}
method maximize (line 51) | maximize() {}
method restore (line 52) | restore() {}
method focus (line 53) | focus() {}
method blur (line 54) | blur() {}
method isFocused (line 55) | isFocused() {
method isVisible (line 58) | isVisible() {
method isMinimized (line 61) | isMinimized() {
method isMaximized (line 64) | isMaximized() {
method setTitle (line 67) | setTitle() {}
method getTitle (line 68) | getTitle() {
FILE: lib/auth/events.ts
type JwtPayload (line 10) | interface JwtPayload {
FILE: lib/clients/grpcClient.test.ts
method constructor (line 139) | constructor(message: string, code: number) {
FILE: lib/clients/grpcClient.ts
class GrpcClient (line 51) | class GrpcClient {
method constructor (line 58) | constructor() {
method setMainWindow (line 71) | setMainWindow(window: BrowserWindow) {
method safeSendToMainWindow (line 76) | private safeSendToMainWindow(channel: string, ...args: any[]) {
method setAuthToken (line 95) | setAuthToken(token: string | null) {
method getHeaders (line 99) | private getHeaders() {
method getHeadersWithMetadata (line 108) | private async getHeadersWithMetadata(mode: ItoMode) {
method withRetry (line 209) | private async withRetry<T>(operation: () => Promise<T>): Promise<T> {
method handleAuthError (line 224) | private async handleAuthError(error: any): Promise<boolean> {
method transcribeStream (line 276) | async transcribeStream(stream: AsyncIterable<AudioChunk>, mode: ItoMod...
method transcribeStreamV2 (line 285) | async transcribeStreamV2(
method createNote (line 302) | async createNote(note: Note) {
method updateNote (line 315) | async updateNote(note: Note) {
method deleteNote (line 327) | async deleteNote(note: Note) {
method listNotesSince (line 338) | async listNotesSince(since?: string): Promise<NotePb[]> {
method createInteraction (line 350) | async createInteraction(interaction: Interaction) {
method updateInteraction (line 383) | async updateInteraction(interaction: Interaction) {
method deleteInteraction (line 395) | async deleteInteraction(interaction: Interaction) {
method listInteractionsSince (line 406) | async listInteractionsSince(since?: string): Promise<InteractionPb[]> {
method createDictionaryItem (line 418) | async createDictionaryItem(item: DictionaryItem) {
method updateDictionaryItem (line 431) | async updateDictionaryItem(item: DictionaryItem) {
method deleteDictionaryItem (line 444) | async deleteDictionaryItem(item: DictionaryItem) {
method listDictionaryItemsSince (line 455) | async listDictionaryItemsSince(since?: string): Promise<DictionaryItem...
method deleteUserData (line 467) | async deleteUserData() {
method getAdvancedSettings (line 476) | async getAdvancedSettings(): Promise<AdvancedSettingsPb | null> {
method updateAdvancedSettings (line 495) | async updateAdvancedSettings(
method submitTimingReports (line 532) | async submitTimingReports(reports: TimingReport[]) {
FILE: lib/clients/itoHttpClient.ts
type RequestOptions (line 4) | interface RequestOptions {
class ItoHttpClient (line 12) | class ItoHttpClient {
method getBaseUrl (line 13) | private getBaseUrl(): string {
method getAccessToken (line 17) | private getAccessToken(): string {
method get (line 21) | async get(path: string, options: RequestOptions = {}) {
method post (line 54) | async post(path: string, body?: any, options: RequestOptions = {}) {
FILE: lib/constants/external-links.ts
constant EXTERNAL_LINKS (line 1) | const EXTERNAL_LINKS = {
FILE: lib/constants/generated-defaults.ts
constant DEFAULT_ADVANCED_SETTINGS (line 7) | const DEFAULT_ADVANCED_SETTINGS = {
FILE: lib/constants/keyboard-defaults.ts
constant ITO_MODE_SHORTCUT_DEFAULTS_MAC (line 4) | const ITO_MODE_SHORTCUT_DEFAULTS_MAC = {
constant ITO_MODE_SHORTCUT_DEFAULTS_WIN (line 9) | const ITO_MODE_SHORTCUT_DEFAULTS_WIN = {
function getPlatform (line 15) | function getPlatform(): 'darwin' | 'win32' {
function getItoModeShortcutDefaults (line 24) | function getItoModeShortcutDefaults(
constant ITO_MODE_SHORTCUT_DEFAULTS (line 37) | const ITO_MODE_SHORTCUT_DEFAULTS = getItoModeShortcutDefaults()
FILE: lib/constants/store-keys.ts
constant STORE_KEYS (line 3) | const STORE_KEYS = {
type StoreKey (line 17) | type StoreKey = (typeof STORE_KEYS)[keyof typeof STORE_KEYS]
FILE: lib/main/app.ts
function getPillWindow (line 11) | function getPillWindow(): BrowserWindow | null {
function createAppWindow (line 16) | function createAppWindow(): BrowserWindow {
constant PILL_MAX_WIDTH (line 84) | const PILL_MAX_WIDTH = 172
constant PILL_MAX_HEIGHT (line 85) | const PILL_MAX_HEIGHT = 84
function createPillWindow (line 86) | function createPillWindow(): void {
function startPillPositioner (line 140) | function startPillPositioner() {
function updatePillPosition (line 154) | function updatePillPosition() {
function registerResourcesProtocol (line 196) | function registerResourcesProtocol() {
FILE: lib/main/appNap.ts
function preventAppNap (line 6) | function preventAppNap() {
function allowAppNap (line 13) | function allowAppNap() {
FILE: lib/main/audio/AudioStreamManager.ts
class AudioStreamManager (line 5) | class AudioStreamManager {
method streamAudioChunks (line 13) | async *streamAudioChunks() {
method initialize (line 35) | initialize() {
method stopStreaming (line 42) | stopStreaming() {
method setupListeners (line 51) | private setupListeners() {
method removeListeners (line 57) | private removeListeners() {
method addAudioChunk (line 77) | addAudioChunk(chunk: Buffer) {
method getInteractionAudioBuffer (line 91) | getInteractionAudioBuffer(): Buffer {
method setAudioConfig (line 95) | setAudioConfig(config: { sampleRate?: number; channels?: number }) {
method getCurrentSampleRate (line 101) | getCurrentSampleRate(): number {
method isCurrentlyStreaming (line 105) | isCurrentlyStreaming(): boolean {
method clearInteractionAudio (line 109) | clearInteractionAudio() {
method getAudioDurationMs (line 113) | getAudioDurationMs(): number {
FILE: lib/main/autoUpdaterWrapper.ts
type UpdateStatus (line 8) | interface UpdateStatus {
function getUpdateStatus (line 18) | function getUpdateStatus(): UpdateStatus {
function initializeAutoUpdater (line 22) | function initializeAutoUpdater() {
function setupAutoUpdaterEvents (line 80) | function setupAutoUpdaterEvents() {
function installUpdateNow (line 116) | async function installUpdateNow() {
FILE: lib/main/context/ContextGrabber.ts
type ContextData (line 14) | interface ContextData {
class ContextGrabber (line 26) | class ContextGrabber {
method gatherContext (line 30) | public async gatherContext(mode: ItoMode): Promise<ContextData> {
method getVocabulary (line 59) | private async getVocabulary(): Promise<string[]> {
method getWindowContext (line 72) | private async getWindowContext(): Promise<{
method getContextText (line 91) | private async getContextText(mode: ItoMode): Promise<string> {
method getCursorContextForGrammar (line 152) | public async getCursorContextForGrammar(
FILE: lib/main/env.ts
constant ITO_ENV (line 19) | const ITO_ENV = stage
FILE: lib/main/grammar/GrammarRulesService.ts
class GrammarRulesService (line 3) | class GrammarRulesService {
method constructor (line 6) | public constructor(context: string) {
method setCaseFirstWord (line 13) | public setCaseFirstWord(transcript: string): string {
method addLeadingSpaceIfNeeded (line 60) | public addLeadingSpaceIfNeeded(transcript: string): string {
method needsLeadingSpace (line 72) | private needsLeadingSpace(context: string): boolean {
method isProperNoun (line 103) | private isProperNoun(word: string): boolean {
method shouldCapitalizeBasedOnContext (line 117) | private shouldCapitalizeBasedOnContext(
FILE: lib/main/interactions/InteractionManager.test.ts
method get (line 18) | get() {
method set (line 21) | set() {}
method delete (line 22) | delete() {}
FILE: lib/main/interactions/InteractionManager.ts
class InteractionManager (line 9) | class InteractionManager {
method initialize (line 13) | initialize(): string {
method getCurrentInteractionId (line 19) | getCurrentInteractionId(): string | null {
method getInteractionStartTime (line 23) | getInteractionStartTime(): number | null {
method adoptInteractionId (line 27) | adoptInteractionId(id: string) {
method createInteraction (line 32) | async createInteraction(
method clearCurrentInteraction (line 116) | clearCurrentInteraction() {
FILE: lib/main/itoSessionManager.ts
class ItoSessionManager (line 13) | class ItoSessionManager {
method startSession (line 23) | public async startSession(mode: ItoMode) {
method fetchAndSendContext (line 69) | private async fetchAndSendContext() {
method setMode (line 91) | public setMode(mode: ItoMode) {
method cancelSession (line 99) | public async cancelSession() {
method completeSession (line 128) | public async completeSession() {
method handleTranscriptionResponse (line 203) | private async handleTranscriptionResponse(result: {
method handleTranscriptionError (line 257) | private async handleTranscriptionError(error: any) {
FILE: lib/main/itoStreamController.ts
class ItoStreamController (line 21) | class ItoStreamController {
method initialize (line 30) | public async initialize(mode: ItoMode): Promise<boolean> {
method startGrpcStream (line 52) | public async startGrpcStream(): Promise<{
method getCurrentMode (line 88) | public getCurrentMode(): ItoMode {
method setMode (line 92) | public setMode(mode: ItoMode) {
method scheduleConfigUpdate (line 105) | public scheduleConfigUpdate(context: ContextData) {
method sendModeUpdate (line 116) | private sendModeUpdate(mode: ItoMode) {
method endInteraction (line 137) | public endInteraction() {
method cancelTranscription (line 147) | public cancelTranscription() {
method getAudioDurationMs (line 160) | public getAudioDurationMs(): number {
method stopStreaming (line 164) | private stopStreaming() {
method clearInteractionAudio (line 168) | public clearInteractionAudio() {
method createStreamGenerator (line 172) | private async *createStreamGenerator(): AsyncGenerator<TranscribeStrea...
method buildStreamConfig (line 212) | private buildStreamConfig(context: ContextData): TranscribeStreamReque...
FILE: lib/main/logger.ts
constant LOG_QUEUE_KEY (line 8) | const LOG_QUEUE_KEY = 'log_queue:events'
function initializeLogging (line 10) | function initializeLogging() {
FILE: lib/main/recordingStateNotifier.ts
class RecordingStateNotifier (line 12) | class RecordingStateNotifier {
method notifyRecordingStarted (line 13) | public notifyRecordingStarted(mode: ItoMode) {
method notifyRecordingStopped (line 23) | public notifyRecordingStopped() {
method notifyProcessingStarted (line 30) | public notifyProcessingStarted() {
method notifyProcessingStopped (line 37) | public notifyProcessingStopped() {
method sendToWindows (line 44) | private sendToWindows(
FILE: lib/main/sqlite/db.test.ts
constant MOCK_MIGRATIONS (line 84) | const MOCK_MIGRATIONS = [
FILE: lib/main/sqlite/db.ts
constant DB_FILE (line 9) | const DB_FILE = 'ito.db'
FILE: lib/main/sqlite/migrations.ts
type Migration (line 1) | interface Migration {
constant MIGRATIONS (line 7) | const MIGRATIONS: Migration[] = [
FILE: lib/main/sqlite/models.ts
type Interaction (line 1) | interface Interaction {
type Note (line 16) | interface Note {
type DictionaryItem (line 26) | interface DictionaryItem {
type PaidStatus (line 36) | enum PaidStatus {
type UserMetadata (line 42) | interface UserMetadata {
FILE: lib/main/sqlite/repo.ts
constant SQLITE_CONSTRAINT_UNIQUE (line 7) | const SQLITE_CONSTRAINT_UNIQUE = 'SQLITE_CONSTRAINT_UNIQUE'
function isUniqueConstraintError (line 10) | function isUniqueConstraintError(error: any): boolean {
type DbResult (line 19) | type DbResult<T> =
function handleDictionaryConstraintError (line 24) | function handleDictionaryConstraintError(
function parseJsonField (line 43) | function parseJsonField(value: any): any {
function parseInteractionJsonFields (line 62) | function parseInteractionJsonFields(interaction: Interaction): Interacti...
type InsertInteraction (line 76) | type InsertInteraction = Omit<
class InteractionsTable (line 81) | class InteractionsTable {
method insert (line 82) | static async insert(
method findById (line 117) | static async findById(id: string): Promise<Interaction | undefined> {
method findAll (line 125) | static async findAll(user_id?: string): Promise<Interaction[]> {
method softDelete (line 135) | static async softDelete(id: string): Promise<void> {
method deleteAllUserData (line 141) | static async deleteAllUserData(userId: string): Promise<void> {
method findModifiedSince (line 151) | static async findModifiedSince(timestamp: string): Promise<Interaction...
method upsert (line 160) | static async upsert(interaction: Interaction): Promise<void> {
type InsertNote (line 199) | type InsertNote = Omit<Note, 'id' | 'created_at' | 'updated_at' | 'delet...
class NotesTable (line 201) | class NotesTable {
method insert (line 202) | static async insert(noteData: InsertNote): Promise<Note> {
method findById (line 229) | static async findById(id: string): Promise<Note | undefined> {
method findAll (line 233) | static async findAll(user_id?: string): Promise<Note[]> {
method findByInteractionId (line 241) | static async findByInteractionId(interactionId: string): Promise<Note[...
method updateContent (line 248) | static async updateContent(id: string, content: string): Promise<void> {
method softDelete (line 257) | static async softDelete(id: string): Promise<void> {
method deleteAllUserData (line 263) | static async deleteAllUserData(userId: string): Promise<void> {
method findModifiedSince (line 273) | static async findModifiedSince(timestamp: string): Promise<Note[]> {
method upsert (line 279) | static async upsert(note: Note): Promise<void> {
type InsertDictionaryItem (line 309) | type InsertDictionaryItem = Omit<
class DictionaryTable (line 314) | class DictionaryTable {
method insert (line 315) | static async insert(
method findAll (line 348) | static async findAll(user_id?: string): Promise<DictionaryItem[]> {
method update (line 356) | static async update(
method softDelete (line 371) | static async softDelete(id: string): Promise<void> {
method deleteAllUserData (line 378) | static async deleteAllUserData(userId: string): Promise<void> {
method findModifiedSince (line 385) | static async findModifiedSince(timestamp: string): Promise<DictionaryI...
method upsert (line 392) | static async upsert(item: DictionaryItem): Promise<DbResult<void>> {
class KeyValueStore (line 424) | class KeyValueStore {
method set (line 425) | static async set(key: string, value: string): Promise<void> {
method get (line 434) | static async get(key: string): Promise<string | undefined> {
method delete (line 442) | static async delete(key: string): Promise<void> {
type UserMetadataRow (line 455) | type UserMetadataRow = {
function parseUserMetadataRow (line 471) | function parseUserMetadataRow(row: UserMetadataRow): UserMetadata {
type InsertUserMetadata (line 494) | type InsertUserMetadata = Omit<UserMetadata, 'id' | 'created_at' | 'upda...
class UserMetadataTable (line 496) | class UserMetadataTable {
method insert (line 497) | static async insert(metadataData: InsertUserMetadata): Promise<UserMet...
method findByUserId (line 532) | static async findByUserId(userId: string): Promise<UserMetadata | unde...
method update (line 544) | static async update(
method upsert (line 585) | static async upsert(metadata: UserMetadata): Promise<void> {
method deleteByUserId (line 619) | static async deleteByUserId(userId: string): Promise<void> {
FILE: lib/main/sqlite/schema.ts
constant INITIAL_SCHEMA (line 1) | const INITIAL_SCHEMA = `
FILE: lib/main/store.ts
type KeyboardShortcutConfig (line 10) | interface KeyboardShortcutConfig {
type MainStore (line 16) | interface MainStore {
type OnboardingStore (line 19) | interface OnboardingStore {
type SettingsStore (line 24) | interface SettingsStore {
type AuthState (line 40) | interface AuthState {
type AuthUser (line 47) | interface AuthUser {
type AuthTokens (line 55) | interface AuthTokens {
type AuthStore (line 64) | interface AuthStore {
type AdvancedSettings (line 70) | interface AdvancedSettings {
type AppStore (line 77) | interface AppStore {
type StoreLike (line 173) | type StoreLike<T = any> = {
function deepGet (line 197) | function deepGet(obj: any, pathParts: string[]): any {
function deepSet (line 204) | function deepSet(obj: any, pathParts: string[], value: any): any {
function persistTopLevelKey (line 214) | async function persistTopLevelKey(key: string) {
type Migration (line 255) | type Migration = { id: string; run: (s: StoreLike<AppStore>) => void }
function runMigrations (line 286) | function runMigrations(s: StoreLike<AppStore>, allMigrations: Migration[...
function ensureDefaultsDeep (line 302) | function ensureDefaultsDeep<T = unknown>(
function initializeStore (line 335) | async function initializeStore() {
FILE: lib/main/syncService.ts
constant LAST_SYNCED_AT_KEY (line 16) | const LAST_SYNCED_AT_KEY = 'lastSyncedAt'
function getEnvNamespace (line 18) | function getEnvNamespace(): string {
function getLastSyncedAtKey (line 28) | function getLastSyncedAtKey(userId: string): string {
class SyncService (line 33) | class SyncService {
method constructor (line 38) | private constructor() {
method getInstance (line 42) | public static getInstance(): SyncService {
method start (line 49) | public async start() {
method stop (line 60) | public stop() {
method runSync (line 68) | private async runSync() {
method pushNotes (line 120) | private async pushNotes(lastSyncedAt: string): Promise<number> {
method pushInteractions (line 141) | private async pushInteractions(lastSyncedAt: string): Promise<number> {
method pushDictionaryItems (line 162) | private async pushDictionaryItems(lastSyncedAt: string): Promise<numbe...
method pullNotes (line 182) | private async pullNotes(lastSyncedAt?: string): Promise<number> {
method pullInteractions (line 205) | private async pullInteractions(lastSyncedAt?: string): Promise<number> {
method pullDictionaryItems (line 252) | private async pullDictionaryItems(lastSyncedAt?: string): Promise<numb...
method syncAdvancedSettings (line 275) | private async syncAdvancedSettings(lastSyncedAt?: string) {
FILE: lib/main/teardown.ts
constant WIN_HELPERS (line 20) | const WIN_HELPERS = [
constant MAC_HELPERS (line 29) | const MAC_HELPERS = [
function killByName (line 43) | function killByName(name: string): Promise<void> {
function hardKillAll (line 53) | async function hardKillAll(): Promise<void> {
FILE: lib/main/text/TextInserter.ts
class TextInserter (line 4) | class TextInserter {
method insertText (line 5) | async insertText(transcript: string): Promise<boolean> {
FILE: lib/main/timing/TimingCollector.ts
type TimingEventName (line 13) | enum TimingEventName {
type ActiveTiming (line 31) | interface ActiveTiming {
class TimingCollector (line 41) | class TimingCollector {
method constructor (line 52) | constructor() {
method shouldCollect (line 57) | private shouldCollect(): boolean {
method startInteraction (line 66) | startInteraction(interactionId?: string) {
method startTiming (line 86) | startTiming(eventName: TimingEventName, interactionId?: string) {
method endTiming (line 112) | endTiming(eventName: TimingEventName, interactionId?: string) {
method finalizeInteraction (line 147) | finalizeInteraction(interactionId?: string) {
method clearInteraction (line 220) | clearInteraction(interactionId?: string) {
method flush (line 233) | async flush({ flushAll = false } = {}) {
method scheduleFlush (line 263) | private scheduleFlush() {
method shutdown (line 274) | async shutdown() {
method timeAsync (line 297) | async timeAsync<T>(
FILE: lib/main/tray.ts
constant TRAY_GUID (line 10) | const TRAY_GUID = '7c6b7a2e-0d7e-4a4a-9d3d-2a3d9b6f2b10' // This is a GU...
constant TRAY_HEIGHT (line 11) | const TRAY_HEIGHT = 16
function getTrayIconPath (line 13) | function getTrayIconPath(): string {
function buildMicrophoneSubmenu (line 21) | async function buildMicrophoneSubmenu(): Promise<
function rebuildTrayMenu (line 76) | async function rebuildTrayMenu(): Promise<void> {
function createAppTray (line 108) | async function createAppTray(): Promise<void> {
function destroyAppTray (line 140) | function destroyAppTray(): void {
FILE: lib/main/voiceInputService.ts
class VoiceInputService (line 9) | class VoiceInputService {
FILE: lib/media/IAccessibilityContextProvider.ts
type IAccessibilityContextProvider (line 6) | interface IAccessibilityContextProvider {
FILE: lib/media/active-application.ts
type ActiveWindow (line 6) | type ActiveWindow = {
function getActiveWindow (line 19) | async function getActiveWindow(): Promise<ActiveWindow | null> {
FILE: lib/media/audio.ts
constant MSG_TYPE_JSON (line 7) | const MSG_TYPE_JSON = 1
constant MSG_TYPE_AUDIO (line 8) | const MSG_TYPE_AUDIO = 2
type Message (line 10) | interface Message {
class AudioRecorderService (line 15) | class AudioRecorderService extends EventEmitter {
method constructor (line 27) | constructor() {
method initialize (line 34) | public initialize(): void {
method terminate (line 75) | public terminate(): void {
method startRecording (line 87) | public startRecording(deviceName: string): void {
method stopRecording (line 95) | public stopRecording(): void {
method getDeviceList (line 103) | public getDeviceList(): Promise<string[]> {
method requestDeviceConfig (line 117) | public requestDeviceConfig(deviceName: string): void {
method #onData (line 126) | #onData(chunk: Buffer): void {
method #onStdErr (line 131) | #onStdErr(data: Buffer): void {
method #onClose (line 135) | #onClose(code: number | null): void {
method #onError (line 141) | #onError(err: Error): void {
method #processData (line 151) | #processData(): void {
method #parseMessage (line 166) | #parseMessage(): Message | null {
method #handleMessage (line 193) | #handleMessage(message: Message): void {
method awaitDrainComplete (line 238) | public awaitDrainComplete(timeoutMs: number = 500): Promise<void> {
method #sendCommand (line 273) | #sendCommand(command: object): void {
method #calculateVolume (line 282) | #calculateVolume(buffer: Buffer): number {
FILE: lib/media/keyboard.ts
type KeyEvent (line 9) | interface KeyEvent {
type HeartbeatEvent (line 16) | interface HeartbeatEvent {
type RegisteredHotkeysEvent (line 22) | interface RegisteredHotkeysEvent {
type ProcessEvent (line 27) | type ProcessEvent = KeyEvent | HeartbeatEvent | RegisteredHotkeysEvent
constant HEARTBEAT_CHECK_INTERVAL_MS (line 36) | const HEARTBEAT_CHECK_INTERVAL_MS = 5000 // Check every 5 seconds
constant HEARTBEAT_TIMEOUT_MS (line 37) | const HEARTBEAT_TIMEOUT_MS = 15000 // 15 seconds without heartbeat trigg...
function normalizeKey (line 55) | function normalizeKey(rawKey: string): KeyName {
function handleHeartbeat (line 63) | function handleHeartbeat(_event: HeartbeatEvent) {
function startHeartbeatChecker (line 67) | function startHeartbeatChecker() {
function stopHeartbeatChecker (line 81) | function stopHeartbeatChecker() {
function restartKeyListener (line 88) | function restartKeyListener() {
constant STUCK_KEY_TIMEOUT (line 107) | const STUCK_KEY_TIMEOUT = 5000 // 5 seconds
constant STUCK_KEY_CHECK_INTERVAL (line 108) | const STUCK_KEY_CHECK_INTERVAL = 1000 // Check every 1 second
function checkForStuckKeys (line 111) | function checkForStuckKeys() {
function startStuckKeyChecker (line 160) | function startStuckKeyChecker() {
function stopStuckKeyChecker (line 170) | function stopStuckKeyChecker() {
function handleKeyEventInMain (line 177) | async function handleKeyEventInMain(event: KeyEvent) {
FILE: lib/media/macOSAccessibilityContextProvider.ts
constant NATIVE_MODULE_NAME (line 18) | const NATIVE_MODULE_NAME = 'cursor-context'
class MacOSAccessibilityContextProvider (line 19) | class MacOSAccessibilityContextProvider
method constructor (line 24) | constructor() {}
method initialize (line 26) | public initialize(): void {
method shutdown (line 42) | public shutdown(): void {
method isRunning (line 46) | public isRunning(): boolean {
method getCursorContext (line 50) | public async getCursorContext(
FILE: lib/media/selected-text-reader.ts
type SelectedTextOptions (line 6) | interface SelectedTextOptions {
type SelectedTextResult (line 11) | interface SelectedTextResult {
type SelectedTextCommand (line 18) | interface SelectedTextCommand {
type CursorContextResult (line 25) | interface CursorContextResult {
type CursorContextCommand (line 32) | interface CursorContextCommand {
constant MAXIUMUM_TEXT_LENGTH_DEFAULT (line 40) | const MAXIUMUM_TEXT_LENGTH_DEFAULT = 10000 // Maximum length of text to ...
type PendingRequest (line 42) | type PendingRequest = {
class SelectedTextReaderService (line 47) | class SelectedTextReaderService extends EventEmitter {
method constructor (line 52) | constructor() {
method initialize (line 59) | public initialize(): void {
method terminate (line 105) | public terminate(): void {
method getSelectedText (line 125) | public async getSelectedText(
method getCursorContext (line 161) | public async getCursorContext(
method #sendCommand (line 192) | #sendCommand(command: SelectedTextCommand | CursorContextCommand): void {
method #onData (line 208) | #onData(data: Buffer): void {
method #onStdErr (line 241) | #onStdErr(data: Buffer): void {
method #onClose (line 245) | #onClose(code: number, signal: string): void {
method #onError (line 260) | #onError(error: Error): void {
method isRunning (line 265) | public isRunning(): boolean {
function getSelectedText (line 273) | function getSelectedText(
function getSelectedTextString (line 285) | async function getSelectedTextString(
function hasSelectedText (line 303) | async function hasSelectedText(): Promise<boolean> {
function getCursorContext (line 319) | async function getCursorContext(contextLength: number): Promise<string> {
FILE: lib/media/systemAudio.ts
function getSystemVolume (line 10) | function getSystemVolume(): number | null {
function setSystemVolume (line 31) | function setSystemVolume(volume: number): boolean {
function muteSystemAudio (line 51) | function muteSystemAudio(): boolean {
function unmuteSystemAudio (line 74) | function unmuteSystemAudio(): boolean {
FILE: lib/media/text-writer.ts
type TextWriterOptions (line 5) | interface TextWriterOptions {
function setFocusedText (line 12) | function setFocusedText(
FILE: lib/preload/index.d.ts
type TrialStatus (line 3) | type TrialStatus = {
type KeyEvent (line 14) | interface KeyEvent {
type StoreAPI (line 21) | interface StoreAPI {
type UpdaterAPI (line 26) | interface UpdaterAPI {
type SelectedTextOptions (line 32) | interface SelectedTextOptions {
type SelectedTextResult (line 37) | interface SelectedTextResult {
type SelectedTextAPI (line 44) | interface SelectedTextAPI {
type Window (line 51) | interface Window {
FILE: lib/preload/preload.ts
method get (line 17) | get(key) {
method set (line 20) | set(property, val) {
method get (line 35) | get(key) {
method set (line 38) | set(property, val) {
FILE: lib/protocol/index.ts
constant PROTOCOL (line 7) | const PROTOCOL = ITO_ENV === 'prod' ? 'ito' : `ito-dev`
function handleProtocolUrl (line 10) | function handleProtocolUrl(url: string) {
function setupProtocolHandling (line 124) | function setupProtocolHandling(): void {
function processStartupProtocolUrl (line 182) | function processStartupProtocolUrl(): void {
FILE: lib/types/cursorContext.ts
type CursorPosition (line 11) | interface CursorPosition {
type TextRange (line 23) | interface TextRange {
type CursorContext (line 35) | interface CursorContext {
type CursorContextResult (line 57) | interface CursorContextResult {
type CursorContextOptions (line 68) | interface CursorContextOptions {
FILE: lib/types/ipc.ts
constant IPC_EVENTS (line 4) | const IPC_EVENTS = {
type RecordingStatePayload (line 15) | interface RecordingStatePayload {
type ProcessingStatePayload (line 20) | interface ProcessingStatePayload {
type VolumeUpdatePayload (line 25) | interface VolumeUpdatePayload {
type IpcResult (line 30) | type IpcResult<T> =
type IpcResponse (line 34) | type IpcResponse<T> = Promise<IpcResult<T>>
FILE: lib/types/keyboard.ts
type ModifierKey (line 64) | type ModifierKey =
type RegularKey (line 76) | type RegularKey =
type KeyName (line 125) | type KeyName = ModifierKey | RegularKey
function normalizeLegacyKey (line 137) | function normalizeLegacyKey(key: string): KeyName {
type KeyDisplayInfo (line 142) | interface KeyDisplayInfo {
function getKeyDisplayInfo (line 150) | function getKeyDisplayInfo(
FILE: lib/utils.ts
function cn (line 4) | function cn(...inputs: ClassValue[]) {
FILE: lib/utils/applicationDetection.ts
constant TERMINAL_APPS (line 3) | const TERMINAL_APPS = new Set([
function canGetContextWithAccessibilityApis (line 78) | async function canGetContextWithAccessibilityApis(): Promise<boolean> {
function isTerminalApplication (line 92) | function isTerminalApplication(appName: string): boolean {
function canGetContextFromCurrentApp (line 97) | async function canGetContextFromCurrentApp(): Promise<boolean> {
FILE: lib/utils/crossPlatform.ts
function checkAccessibilityPermission (line 12) | function checkAccessibilityPermission(prompt: boolean = false): boolean {
function checkMicrophonePermission (line 25) | async function checkMicrophonePermission(
FILE: lib/utils/settings.ts
function resolveDefaultKeys (line 4) | function resolveDefaultKeys(
FILE: lib/window/ipcDev.ts
function registerDevIPC (line 4) | function registerDevIPC() {
FILE: lib/window/ipcEvents.ts
function registerIPC (line 56) | function registerIPC() {
FILE: native/active-application/build.rs
function main (line 1) | fn main() {
FILE: native/active-application/src/main.rs
function main (line 4) | fn main() {
function output_result (line 14) | fn output_result(active_window: ActiveWindow) {
FILE: native/audio-recorder/build.rs
function main (line 1) | fn main() {
FILE: native/audio-recorder/src/main.rs
type Command (line 15) | enum Command {
type DeviceList (line 26) | struct DeviceList {
type AudioConfig (line 33) | struct AudioConfig {
constant MSG_TYPE_JSON (line 41) | const MSG_TYPE_JSON: u8 = 1;
constant MSG_TYPE_AUDIO (line 42) | const MSG_TYPE_AUDIO: u8 = 2;
function write_framed_message (line 44) | fn write_framed_message(writer: &mut impl Write, msg_type: u8, data: &[u...
function main (line 52) | fn main() {
type CommandProcessor (line 75) | struct CommandProcessor {
method new (line 86) | fn new(cmd_rx: crossbeam_channel::Receiver<Command>, stdout: Arc<Mutex...
method get_or_create_host (line 97) | fn get_or_create_host(&mut self) -> Rc<cpal::Host> {
method run (line 132) | fn run(&mut self) {
method list_devices (line 143) | fn list_devices(&mut self) {
method start_recording (line 161) | fn start_recording(&mut self, device_name: Option<String>) {
method stop_recording (line 176) | fn stop_recording(&mut self) {
method get_device_config (line 190) | fn get_device_config(&mut self, device_name: Option<String>) {
function write_audio_chunk (line 226) | fn write_audio_chunk(data: &[f32], stdout: &Arc<Mutex<io::Stdout>>) {
type CaptureHandles (line 241) | struct CaptureHandles {
function downmix_to_mono_vec (line 247) | fn downmix_to_mono_vec<T>(data: &[T], num_channels: usize) -> Vec<f32>
function writer_loop (line 288) | fn writer_loop(
function start_capture (line 435) | fn start_capture(
function test_downmix_to_mono_single_channel (line 594) | fn test_downmix_to_mono_single_channel() {
function test_downmix_to_mono_stereo (line 603) | fn test_downmix_to_mono_stereo() {
function test_downmix_to_mono_quad (line 614) | fn test_downmix_to_mono_quad() {
function test_downmix_partial_frame (line 624) | fn test_downmix_partial_frame() {
function test_write_framed_message_structure (line 635) | fn test_write_framed_message_structure() {
function test_write_framed_message_audio_type (line 654) | fn test_write_framed_message_audio_type() {
FILE: native/global-key-listener/build.rs
function main (line 1) | fn main() {
FILE: native/global-key-listener/src/key_codes.rs
function key_to_code (line 4) | pub fn key_to_code(key: &Key) -> Option<u32> {
function test_key_to_code_letters (line 103) | fn test_key_to_code_letters() {
function test_key_to_code_numbers (line 111) | fn test_key_to_code_numbers() {
function test_key_to_code_modifiers (line 119) | fn test_key_to_code_modifiers() {
function test_key_to_code_function_keys (line 129) | fn test_key_to_code_function_keys() {
function test_key_to_code_special_keys (line 137) | fn test_key_to_code_special_keys() {
function test_key_to_code_arrow_keys (line 147) | fn test_key_to_code_arrow_keys() {
FILE: native/global-key-listener/src/main.rs
type HotkeyCombo (line 22) | struct HotkeyCombo {
type Command (line 28) | enum Command {
function prevent_app_nap (line 51) | fn prevent_app_nap() -> id {
function prevent_app_nap (line 70) | fn prevent_app_nap() {
function main (line 74) | fn main() {
function handle_command (line 114) | fn handle_command(command: Command) {
function should_block (line 125) | fn should_block() -> bool {
function callback (line 145) | fn callback(event: Event) -> Option<Event> {
function output_event (line 270) | fn output_event(event_type: &str, key: &Key) {
FILE: native/selected-text-reader/build.rs
function main (line 1) | fn main() {
FILE: native/selected-text-reader/src/macos.rs
function count_editor_chars (line 9) | pub fn count_editor_chars(text: &str) -> usize {
type __CGEvent (line 15) | struct __CGEvent(c_void);
type CGEventRef (line 16) | type CGEventRef = *mut __CGEvent;
type CGKeyCode (line 18) | type CGKeyCode = u16;
type CGEventFlags (line 19) | type CGEventFlags = u64;
constant CG_EVENT_FLAG_MASK_COMMAND (line 20) | const CG_EVENT_FLAG_MASK_COMMAND: CGEventFlags = 0x100000;
constant CG_EVENT_FLAG_MASK_SHIFT (line 21) | const CG_EVENT_FLAG_MASK_SHIFT: CGEventFlags = 0x020000;
type CGEventTapLocation (line 23) | type CGEventTapLocation = u32;
constant CG_SESSION_EVENT_TAP (line 24) | const CG_SESSION_EVENT_TAP: CGEventTapLocation = 1;
function CGEventCreateKeyboardEvent (line 27) | fn CGEventCreateKeyboardEvent(
function CGEventSetFlags (line 32) | fn CGEventSetFlags(event: CGEventRef, flags: CGEventFlags);
function CGEventPost (line 33) | fn CGEventPost(tap: CGEventTapLocation, event: CGEventRef);
function CGEventSetIntegerValueField (line 34) | fn CGEventSetIntegerValueField(event: CGEventRef, field: u32, value: i64);
function CFRelease (line 35) | fn CFRelease(cf: *const c_void);
function get_selected_text (line 38) | pub fn get_selected_text() -> Result<String, Box<dyn std::error::Error>> {
function native_cmd_c (line 67) | pub fn native_cmd_c() -> Result<(), Box<dyn std::error::Error>> {
function select_previous_chars_and_copy (line 108) | pub fn select_previous_chars_and_copy(
function shift_cursor_right_with_deselect (line 174) | pub fn shift_cursor_right_with_deselect(
function test_count_editor_chars (line 218) | fn test_count_editor_chars() {
FILE: native/selected-text-reader/src/main.rs
type Command (line 15) | enum Command {
type SelectedTextResponse (line 36) | struct SelectedTextResponse {
type CursorContextResponse (line 46) | struct CursorContextResponse {
function main (line 56) | fn main() {
type CommandProcessor (line 83) | struct CommandProcessor {
method new (line 88) | fn new(cmd_rx: crossbeam_channel::Receiver<Command>) -> Self {
method run (line 92) | fn run(&mut self) {
method handle_get_text (line 113) | fn handle_get_text(&mut self, max_length: Option<usize>, request_id: S...
method handle_get_cursor_context (line 160) | fn handle_get_cursor_context(
function get_selected_text (line 213) | fn get_selected_text() -> Result<String, Box<dyn std::error::Error>> {
function get_selected_text (line 218) | fn get_selected_text() -> Result<String, Box<dyn std::error::Error>> {
function get_cursor_context (line 222) | fn get_cursor_context(context_length: usize) -> Result<String, Box<dyn s...
function copy_selected_text (line 322) | fn copy_selected_text() -> Result<(), Box<dyn std::error::Error>> {
function copy_selected_text (line 327) | fn copy_selected_text() -> Result<(), Box<dyn std::error::Error>> {
function select_previous_chars_and_copy (line 332) | fn select_previous_chars_and_copy(
function select_previous_chars_and_copy (line 340) | fn select_previous_chars_and_copy(
function shift_cursor_right_with_deselect (line 348) | fn shift_cursor_right_with_deselect(char_count: usize) -> Result<(), Box...
function shift_cursor_right_with_deselect (line 353) | fn shift_cursor_right_with_deselect(char_count: usize) -> Result<(), Box...
function count_editor_chars (line 358) | fn count_editor_chars(text: &str) -> usize {
function count_editor_chars (line 363) | fn count_editor_chars(text: &str) -> usize {
FILE: native/selected-text-reader/src/windows.rs
function count_editor_chars (line 8) | pub fn count_editor_chars(text: &str) -> usize {
function get_selected_text (line 14) | pub fn get_selected_text() -> Result<String, Box<dyn std::error::Error>> {
function copy_selected_text (line 19) | pub fn copy_selected_text() -> Result<(), Box<dyn std::error::Error>> {
function select_previous_chars_and_copy (line 31) | pub fn select_previous_chars_and_copy(
function shift_cursor_right_with_deselect (line 74) | pub fn shift_cursor_right_with_deselect(
function test_count_editor_chars_normal_text (line 103) | fn test_count_editor_chars_normal_text() {
function test_count_editor_chars_with_unix_newline (line 108) | fn test_count_editor_chars_with_unix_newline() {
function test_count_editor_chars_with_crlf (line 113) | fn test_count_editor_chars_with_crlf() {
function test_count_editor_chars_multiple_crlf (line 119) | fn test_count_editor_chars_multiple_crlf() {
function test_count_editor_chars_unicode (line 124) | fn test_count_editor_chars_unicode() {
function test_count_editor_chars_emoji (line 129) | fn test_count_editor_chars_emoji() {
function test_count_editor_chars_empty (line 134) | fn test_count_editor_chars_empty() {
FILE: native/text-writer/build.rs
function main (line 1) | fn main() {
FILE: native/text-writer/src/macos_writer.rs
function type_text_macos (line 13) | pub fn type_text_macos(text: &str, _char_delay: u64) -> Result<(), Strin...
FILE: native/text-writer/src/main.rs
type Args (line 23) | struct Args {
function main (line 44) | fn main() {
FILE: native/text-writer/src/windows_writer.rs
function type_text_windows (line 10) | pub fn type_text_windows(text: &str, _char_delay: u64) -> Result<(), Str...
FILE: server/infra/bin/infra.ts
type AppStageProps (line 13) | interface AppStageProps extends StageProps {
class AppStage (line 17) | class AppStage extends Stage {
method constructor (line 20) | constructor(scope: Construct, id: string, props: AppStageProps) {
FILE: server/infra/lambdas/firehose-transform.ts
type FirehoseRecord (line 3) | type FirehoseRecord = {
type FirehoseResponseRecord (line 8) | type FirehoseResponseRecord = {
type FirehoseEvent (line 14) | type FirehoseEvent = {
type CwLogEvent (line 18) | type CwLogEvent = {
type CwLogsBatch (line 28) | type CwLogsBatch = {
constant DATASET (line 37) | const DATASET = process.env.DATASET || 'server'
constant STAGE (line 38) | const STAGE = process.env.STAGE || 'dev'
function tryJsonParse (line 40) | function tryJsonParse<T>(text: unknown): T | undefined {
function isGzip (line 51) | function isGzip(buffer: Buffer): boolean {
function decodeRecord (line 55) | function decodeRecord(b64: string): string {
function utf8ToBase64 (line 61) | function utf8ToBase64(s: string): string {
function isoTimestamp (line 65) | function isoTimestamp(ms?: number): string {
function clean (line 70) | function clean(obj: Record<string, unknown>): Record<string, unknown> {
FILE: server/infra/lambdas/opensearch-bootstrap.ts
type Event (line 4) | type Event = {
constant DOMAIN_ENDPOINT (line 8) | const DOMAIN_ENDPOINT = process.env.DOMAIN_ENDPOINT!
constant REGION (line 9) | const REGION = process.env.REGION || 'us-west-2'
constant STAGE (line 10) | const STAGE = process.env.STAGE || 'dev'
function signRequest (line 24) | function signRequest(
function request (line 91) | function request(path: string, method: string, body?: any): Promise<any> {
FILE: server/infra/lambdas/timing-merger.ts
constant OPENSEARCH_ENDPOINT (line 7) | const OPENSEARCH_ENDPOINT = process.env.OPENSEARCH_ENDPOINT
function getTimingIndexName (line 13) | function getTimingIndexName(): string {
type TimingEvent (line 29) | interface TimingEvent {
type ClientTimingData (line 36) | interface ClientTimingData {
type ServerTimingData (line 49) | interface ServerTimingData {
type TimingData (line 57) | type TimingData = ClientTimingData | ServerTimingData
type MergedEvent (line 59) | interface MergedEvent {
function getS3Object (line 67) | async function getS3Object(bucket: string, key: string): Promise<string> {
function mergeAndUpsertTimingReport (line 78) | async function mergeAndUpsertTimingReport(
FILE: server/infra/lib/cicd-stack.ts
type GitHubOidcStackProps (line 23) | interface GitHubOidcStackProps extends StackProps {
class GitHubOidcStack (line 27) | class GitHubOidcStack extends Stack {
method constructor (line 28) | constructor(scope: Construct, id: string, props: GitHubOidcStackProps) {
FILE: server/infra/lib/constants.ts
constant DB_NAME (line 1) | const DB_NAME = 'ItoDb'
constant SERVER_NAME (line 2) | const SERVER_NAME = 'ito-server'
constant CLUSTER_NAME (line 3) | const CLUSTER_NAME = 'ito-cluster'
constant SERVICE_NAME (line 4) | const SERVICE_NAME = 'ito-service'
constant ITO_PREFIX (line 5) | const ITO_PREFIX = 'Ito'
constant SERVICE_REPO_ARN (line 6) | const SERVICE_REPO_ARN = 'ServiceRepoArn'
constant DB_PORT (line 7) | const DB_PORT = 5432
FILE: server/infra/lib/helpers.ts
function isDev (line 1) | function isDev(stage: string) {
FILE: server/infra/lib/network-stack.ts
class NetworkStack (line 5) | class NetworkStack extends Stack {
method constructor (line 8) | constructor(scope: Construct, id: string, props?: StackProps) {
FILE: server/infra/lib/observability-stack.ts
type ObservabilityStackProps (line 9) | interface ObservabilityStackProps extends StackProps {
class ObservabilityStack (line 13) | class ObservabilityStack extends Stack {
method constructor (line 16) | constructor(scope: Construct, id: string, props: ObservabilityStackPro...
FILE: server/infra/lib/platform-stack.ts
type PlatformStackProps (line 49) | interface PlatformStackProps extends StackProps {
class PlatformStack (line 53) | class PlatformStack extends Stack {
method constructor (line 62) | constructor(scope: Construct, id: string, props: PlatformStackProps) {
FILE: server/infra/lib/security-stack.ts
type SecurityStackProps (line 7) | interface SecurityStackProps extends StackProps {
class SecurityStack (line 12) | class SecurityStack extends Stack {
method constructor (line 13) | constructor(scope: Construct, id: string, props: SecurityStackProps) {
FILE: server/infra/lib/service-stack.ts
type ServiceStackProps (line 41) | interface ServiceStackProps extends StackProps {
class ServiceStack (line 51) | class ServiceStack extends Stack {
method constructor (line 56) | constructor(scope: Construct, id: string, props: ServiceStackProps) {
FILE: server/infra/lib/service/fargate-task.ts
type FargateTaskConfig (line 23) | interface FargateTaskConfig {
type FargateTaskResources (line 41) | interface FargateTaskResources {
function createFargateTask (line 49) | function createFargateTask(
FILE: server/infra/lib/service/firehose-config.ts
type FirehoseConfig (line 17) | interface FirehoseConfig {
type FirehoseResources (line 28) | interface FirehoseResources {
function createFirehoseStreams (line 39) | function createFirehoseStreams(
FILE: server/infra/lib/service/log-groups.ts
type LogGroupConfig (line 6) | interface LogGroupConfig {
type LogGroupResources (line 10) | interface LogGroupResources {
function createLogGroups (line 17) | function createLogGroups(
FILE: server/infra/lib/service/migration-lambda.ts
type MigrationLambdaConfig (line 15) | interface MigrationLambdaConfig {
type MigrationLambdaResources (line 27) | interface MigrationLambdaResources {
function createMigrationLambda (line 31) | function createMigrationLambda(
FILE: server/infra/lib/service/opensearch-bootstrap.ts
type OpenSearchBootstrapConfig (line 8) | interface OpenSearchBootstrapConfig {
type OpenSearchBootstrapResources (line 13) | interface OpenSearchBootstrapResources {
function createOpenSearchBootstrap (line 19) | function createOpenSearchBootstrap(
FILE: server/infra/lib/timing-config.ts
type TimingConfig (line 11) | interface TimingConfig {
type TimingResources (line 18) | interface TimingResources {
function createTimingInfrastructure (line 25) | function createTimingInfrastructure(
FILE: server/scripts/migrate-audio-to-s3.ts
type InteractionRow (line 13) | interface InteractionRow {
function migrateAudioToS3 (line 20) | async function migrateAudioToS3() {
FILE: server/src/auth/auth0Helpers.ts
function getAuth0ManagementToken (line 1) | async function getAuth0ManagementToken(): Promise<string | null> {
function getUserInfoFromAuth0 (line 32) | async function getUserInfoFromAuth0(
FILE: server/src/auth/userContext.ts
type Auth0User (line 4) | interface Auth0User {
FILE: server/src/clients/asrConfig.ts
type TranscriptionOptions (line 1) | interface TranscriptionOptions {
FILE: server/src/clients/cerebrasClient.ts
class CerebrasClient (line 21) | class CerebrasClient implements LlmProvider {
method constructor (line 26) | constructor(apiKey: string, userCommandModel: string) {
method isAvailable (line 38) | public get isAvailable(): boolean {
method adjustTranscript (line 47) | public async adjustTranscript(
method transcribeAudio (line 92) | public async transcribeAudio(
FILE: server/src/clients/errors.ts
type ErrorType (line 10) | enum ErrorType {
method constructor (line 24) | constructor(
method mapProviderToProtobuf (line 36) | private mapProviderToProtobuf(provider: ClientProvider): ClientProviderPb {
method mapErrorTypeToProtobuf (line 50) | private mapErrorTypeToProtobuf(type: ErrorType): ErrorTypePb {
method toProtobuf (line 68) | toProtobuf(): ClientErrorPb {
method constructor (line 94) | constructor(
class ClientApiKeyError (line 106) | class ClientApiKeyError extends ClientConfigurationError {
method constructor (line 109) | constructor(provider: ClientProvider) {
class ClientModelError (line 117) | class ClientModelError extends ClientConfigurationError {
method constructor (line 120) | constructor(provider: ClientProvider) {
class ClientUnavailableError (line 128) | class ClientUnavailableError extends ClientError {
method constructor (line 132) | constructor(provider: ClientProvider) {
method constructor (line 143) | constructor(
class ClientNoSpeechError (line 155) | class ClientNoSpeechError extends ClientAudioError {
method constructor (line 158) | constructor(
class ClientTranscriptionQualityError (line 169) | class ClientTranscriptionQualityError extends ClientAudioError {
method constructor (line 172) | constructor(
class ClientAudioTooShortError (line 183) | class ClientAudioTooShortError extends ClientAudioError {
method constructor (line 186) | constructor(provider: ClientProvider) {
class ClientApiError (line 194) | class ClientApiError extends ClientError {
method constructor (line 198) | constructor(
function isClientError (line 214) | function isClientError(error: unknown): error is ClientError {
function isClientErrorType (line 221) | function isClientErrorType<T extends ClientError>(
function errorToProtobuf (line 233) | function errorToProtobuf(
FILE: server/src/clients/groqClient.test.ts
method constructor (line 27) | constructor() {
FILE: server/src/clients/groqClient.ts
class GroqClient (line 27) | class GroqClient implements LlmProvider {
method constructor (line 32) | constructor(apiKey: string, userCommandModel: string) {
method isAvailable (line 44) | public get isAvailable(): boolean {
method adjustTranscript (line 53) | public async adjustTranscript(
method transcribeAudio (line 97) | public async transcribeAudio(
FILE: server/src/clients/intentTranscriptionConfig.ts
type IntentTranscriptionOptions (line 1) | interface IntentTranscriptionOptions {
FILE: server/src/clients/llmProvider.ts
type LlmProvider (line 4) | interface LlmProvider {
FILE: server/src/clients/providerUtils.ts
function getAsrProvider (line 12) | function getAsrProvider(providerName: string): LlmProvider {
function getLlmProvider (line 30) | function getLlmProvider(providerName: string): LlmProvider {
function getAvailableAsrProviders (line 53) | function getAvailableAsrProviders(): ClientProvider[] {
function getAvailableLlmProviders (line 67) | function getAvailableLlmProviders(): ClientProvider[] {
FILE: server/src/clients/providers.ts
type ClientProvider (line 1) | enum ClientProvider {
FILE: server/src/clients/s3storageClient.ts
class S3StorageClient (line 17) | class S3StorageClient {
method constructor (line 22) | constructor(bucketName?: string) {
method uploadObject (line 54) | async uploadObject(
method getObject (line 71) | async getObject(key: string): Promise<{
method deleteObject (line 90) | async deleteObject(key: string): Promise<void> {
method listObjects (line 99) | async listObjects(
method hardDeletePrefix (line 120) | async hardDeletePrefix(prefix: string): Promise<number> {
method objectExists (line 156) | async objectExists(key: string): Promise<boolean> {
method getObjectUrl (line 176) | async getObjectUrl(key: string, _expiresIn?: number): Promise<string> {
method getBucketName (line 182) | getBucketName(): string {
function getStorageClient (line 190) | function getStorageClient(): S3StorageClient {
FILE: server/src/constants/generated-defaults.ts
constant DEFAULT_ADVANCED_SETTINGS (line 7) | const DEFAULT_ADVANCED_SETTINGS = {
FILE: server/src/constants/markers.ts
constant START_WINDOW_TITLE_MARKER (line 1) | const START_WINDOW_TITLE_MARKER = '{START_WINDOW_TITLE_MARKER}'
constant END_WINDOW_TITLE_MARKER (line 2) | const END_WINDOW_TITLE_MARKER = '{END_WINDOW_TITLE_MARKER}'
constant START_APP_NAME_MARKER (line 3) | const START_APP_NAME_MARKER = '{START_APP_NAME_MARKER}'
constant END_APP_NAME_MARKER (line 4) | const END_APP_NAME_MARKER = '{END_APP_NAME_MARKER}'
constant START_USER_COMMAND_MARKER (line 5) | const START_USER_COMMAND_MARKER = '{START_USER_COMMAND_MARKER}'
constant END_USER_COMMAND_MARKER (line 6) | const END_USER_COMMAND_MARKER = '{END_USER_COMMAND_MARKER}'
constant START_CONTEXT_MARKER (line 7) | const START_CONTEXT_MARKER = '{START_CONTEXT_MARKER}'
constant END_CONTEXT_MARKER (line 8) | const END_CONTEXT_MARKER = '{END_CONTEXT_MARKER}'
FILE: server/src/constants/storage.ts
constant AUDIO_KEY_PREFIX (line 1) | const AUDIO_KEY_PREFIX = 'raw-audio'
function createAudioKey (line 3) | function createAudioKey(userId: string, audioUuid: string): string {
FILE: server/src/db/models.ts
type Note (line 1) | interface Note {
type Interaction (line 11) | interface Interaction {
type DictionaryItem (line 25) | interface DictionaryItem {
type LlmSettingsBase (line 35) | interface LlmSettingsBase {
type LlmSettings (line 48) | interface LlmSettings extends LlmSettingsBase {
type AdvancedSettings (line 55) | interface AdvancedSettings {
type UserTrial (line 63) | interface UserTrial {
type UserSubscription (line 73) | interface UserSubscription {
FILE: server/src/db/repo.ts
class NotesRepository (line 21) | class NotesRepository {
method create (line 22) | static async create(
method findById (line 39) | static async findById(id: string): Promise<Note | undefined> {
method findByUserId (line 46) | static async findByUserId(userId: string, since?: Date): Promise<Note[...
method update (line 61) | static async update(noteData: UpdateNoteRequest): Promise<Note | undef...
method softDelete (line 72) | static async softDelete(id: string): Promise<boolean> {
method deleteAllUserData (line 82) | static async deleteAllUserData(userId: string): Promise<boolean> {
method hardDeleteAllUserData (line 92) | static async hardDeleteAllUserData(userId: string): Promise<number> {
class InteractionsRepository (line 100) | class InteractionsRepository {
method create (line 101) | static async create(
method findById (line 124) | static async findById(id: string): Promise<Interaction | undefined> {
method findByUserId (line 132) | static async findByUserId(
method update (line 150) | static async update(
method softDelete (line 163) | static async softDelete(id: string): Promise<boolean> {
method deleteAllUserData (line 173) | static async deleteAllUserData(userId: string): Promise<boolean> {
method hardDeleteAllUserData (line 183) | static async hardDeleteAllUserData(userId: string): Promise<number> {
class DictionaryRepository (line 192) | class DictionaryRepository {
method create (line 193) | static async create(
method findByUserId (line 205) | static async findByUserId(
method update (line 223) | static async update(
method softDelete (line 236) | static async softDelete(id: string): Promise<boolean> {
method deleteAllUserData (line 246) | static async deleteAllUserData(userId: string): Promise<boolean> {
method hardDeleteAllUserData (line 256) | static async hardDeleteAllUserData(userId: string): Promise<number> {
class AdvancedSettingsRepository (line 265) | class AdvancedSettingsRepository {
method findByUserId (line 266) | static async findByUserId(
method upsert (line 299) | static async upsert(
method hardDeleteByUserId (line 361) | static async hardDeleteByUserId(userId: string): Promise<number> {
class IpLinkRepository (line 369) | class IpLinkRepository {
method cleanupExpired (line 370) | static async cleanupExpired(): Promise<number> {
method registerCandidate (line 377) | static async registerCandidate(
method consumeLatestForIp (line 389) | static async consumeLatestForIp(ipHash: string): Promise<string | null> {
class TrialsRepository (line 406) | class TrialsRepository {
method getByUserId (line 407) | static async getByUserId(userId: string): Promise<UserTrial | undefine...
method getByStripeSubscriptionId (line 415) | static async getByStripeSubscriptionId(
method upsertFromStripeSubscription (line 425) | static async upsertFromStripeSubscription(
method startTrial (line 455) | static async startTrial(userId: string, startAt?: Date): Promise<UserT...
method completeTrial (line 486) | static async completeTrial(userId: string): Promise<UserTrial> {
class SubscriptionsRepository (line 509) | class SubscriptionsRepository {
method getByUserId (line 510) | static async getByUserId(
method upsertActive (line 520) | static async upsertActive(
method updateSubscriptionEndAt (line 544) | static async updateSubscriptionEndAt(
method deleteByStripeSubscriptionId (line 558) | static async deleteByStripeSubscriptionId(
FILE: server/src/generated/buf/validate/validate_pb.ts
type Rule (line 50) | type Rule = Message<"buf.validate.Rule"> & {
type MessageRules (line 93) | type MessageRules = Message<"buf.validate.MessageRules"> & {
type MessageOneofRule (line 167) | type MessageOneofRule = Message<"buf.validate.MessageOneofRule"> & {
type OneofRules (line 198) | type OneofRules = Message<"buf.validate.OneofRules"> & {
type FieldRules (line 235) | type FieldRules = Message<"buf.validate.FieldRules"> & {
type PredefinedRules (line 487) | type PredefinedRules = Message<"buf.validate.PredefinedRules"> & {
type FloatRules (line 522) | type FloatRules = Message<"buf.validate.FloatRules"> & {
type DoubleRules (line 704) | type DoubleRules = Message<"buf.validate.DoubleRules"> & {
type Int32Rules (line 886) | type Int32Rules = Message<"buf.validate.Int32Rules"> & {
type Int64Rules (line 1060) | type Int64Rules = Message<"buf.validate.Int64Rules"> & {
type UInt32Rules (line 1234) | type UInt32Rules = Message<"buf.validate.UInt32Rules"> & {
type UInt64Rules (line 1408) | type UInt64Rules = Message<"buf.validate.UInt64Rules"> & {
type SInt32Rules (line 1581) | type SInt32Rules = Message<"buf.validate.SInt32Rules"> & {
type SInt64Rules (line 1754) | type SInt64Rules = Message<"buf.validate.SInt64Rules"> & {
type Fixed32Rules (line 1927) | type Fixed32Rules = Message<"buf.validate.Fixed32Rules"> & {
type Fixed64Rules (line 2100) | type Fixed64Rules = Message<"buf.validate.Fixed64Rules"> & {
type SFixed32Rules (line 2273) | type SFixed32Rules = Message<"buf.validate.SFixed32Rules"> & {
type SFixed64Rules (line 2446) | type SFixed64Rules = Message<"buf.validate.SFixed64Rules"> & {
type BoolRules (line 2620) | type BoolRules = Message<"buf.validate.BoolRules"> & {
type StringRules (line 2668) | type StringRules = Message<"buf.validate.StringRules"> & {
type BytesRules (line 3328) | type BytesRules = Message<"buf.validate.BytesRules"> & {
type EnumRules (line 3576) | type EnumRules = Message<"buf.validate.EnumRules"> & {
type RepeatedRules (line 3698) | type RepeatedRules = Message<"buf.validate.RepeatedRules"> & {
type MapRules (line 3786) | type MapRules = Message<"buf.validate.MapRules"> & {
type AnyRules (line 3873) | type AnyRules = Message<"buf.validate.AnyRules"> & {
type DurationRules (line 3921) | type DurationRules = Message<"buf.validate.DurationRules"> & {
type TimestampRules (line 4096) | type TimestampRules = Message<"buf.validate.TimestampRules"> & {
type Violations (line 4278) | type Violations = Message<"buf.validate.Violations"> & {
type Violation (line 4341) | type Violation = Message<"buf.validate.Violation"> & {
type FieldPath (line 4440) | type FieldPath = Message<"buf.validate.FieldPath"> & {
type FieldPathElement (line 4465) | type FieldPathElement = Message<"buf.validate.FieldPathElement"> & {
type Ignore (line 4575) | enum Ignore {
type KnownRegex (line 4685) | enum KnownRegex {
FILE: server/src/generated/ito_pb.ts
type Empty (line 22) | type Empty = Message<"ito.Empty"> & {
type ClientError (line 35) | type ClientError = Message<"ito.ClientError"> & {
type AudioChunk (line 76) | type AudioChunk = Message<"ito.AudioChunk"> & {
type ContextInfo (line 97) | type ContextInfo = Message<"ito.ContextInfo"> & {
type StreamConfig (line 133) | type StreamConfig = Message<"ito.StreamConfig"> & {
type TranscribeStreamRequest (line 168) | type TranscribeStreamRequest = Message<"ito.TranscribeStreamRequest"> & {
type TranscriptionResponse (line 203) | type TranscriptionResponse = Message<"ito.TranscriptionResponse"> & {
type Note (line 228) | type Note = Message<"ito.Note"> & {
type CreateNoteRequest (line 275) | type CreateNoteRequest = Message<"ito.CreateNoteRequest"> & {
type GetNoteRequest (line 302) | type GetNoteRequest = Message<"ito.GetNoteRequest"> & {
type ListNotesRequest (line 319) | type ListNotesRequest = Message<"ito.ListNotesRequest"> & {
type ListNotesResponse (line 338) | type ListNotesResponse = Message<"ito.ListNotesResponse"> & {
type UpdateNoteRequest (line 355) | type UpdateNoteRequest = Message<"ito.UpdateNoteRequest"> & {
type DeleteNoteRequest (line 377) | type DeleteNoteRequest = Message<"ito.DeleteNoteRequest"> & {
type Interaction (line 397) | type Interaction = Message<"ito.Interaction"> & {
type CreateInteractionRequest (line 474) | type CreateInteractionRequest = Message<"ito.CreateInteractionRequest"> & {
type GetInteractionRequest (line 520) | type GetInteractionRequest = Message<"ito.GetInteractionRequest"> & {
type ListInteractionsRequest (line 537) | type ListInteractionsRequest = Message<"ito.ListInteractionsRequest"> & {
type ListInteractionsResponse (line 556) | type ListInteractionsResponse = Message<"ito.ListInteractionsResponse"> & {
type UpdateInteractionRequest (line 573) | type UpdateInteractionRequest = Message<"ito.UpdateInteractionRequest"> & {
type DeleteInteractionRequest (line 595) | type DeleteInteractionRequest = Message<"ito.DeleteInteractionRequest"> & {
type DictionaryItem (line 615) | type DictionaryItem = Message<"ito.DictionaryItem"> & {
type CreateDictionaryItemRequest (line 662) | type CreateDictionaryItemRequest = Message<"ito.CreateDictionaryItemRequ...
type ListDictionaryItemsRequest (line 689) | type ListDictionaryItemsRequest = Message<"ito.ListDictionaryItemsReques...
type ListDictionaryItemsResponse (line 708) | type ListDictionaryItemsResponse = Message<"ito.ListDictionaryItemsRespo...
type UpdateDictionaryItemRequest (line 725) | type UpdateDictionaryItemRequest = Message<"ito.UpdateDictionaryItemRequ...
type DeleteDictionaryItemRequest (line 752) | type DeleteDictionaryItemRequest = Message<"ito.DeleteDictionaryItemRequ...
type DeleteUserDataRequest (line 774) | type DeleteUserDataRequest = Message<"ito.DeleteUserDataRequest"> & {
type LlmSettings (line 787) | type LlmSettings = Message<"ito.LlmSettings"> & {
type AdvancedSettings (line 849) | type AdvancedSettings = Message<"ito.AdvancedSettings"> & {
type GetAdvancedSettingsRequest (line 893) | type GetAdvancedSettingsRequest = Message<"ito.GetAdvancedSettingsReques...
type UpdateAdvancedSettingsRequest (line 906) | type UpdateAdvancedSettingsRequest = Message<"ito.UpdateAdvancedSettings...
type TimingEvent (line 926) | type TimingEvent = Message<"ito.TimingEvent"> & {
type TimingReport (line 958) | type TimingReport = Message<"ito.TimingReport"> & {
type SubmitTimingReportsRequest (line 1015) | type SubmitTimingReportsRequest = Message<"ito.SubmitTimingReportsReques...
type SubmitTimingReportsResponse (line 1034) | type SubmitTimingReportsResponse = Message<"ito.SubmitTimingReportsRespo...
type ItoMode (line 1047) | enum ItoMode {
type ClientProvider (line 1071) | enum ClientProvider {
type ErrorType (line 1092) | enum ErrorType {
FILE: server/src/migrations/schema/initial.js
constant INITIAL_SCHEMA_UP (line 1) | const INITIAL_SCHEMA_UP = `
constant INITIAL_SCHEMA_DOWN (line 34) | const INITIAL_SCHEMA_DOWN = `
FILE: server/src/prompts/transcription.ts
function estimateTokenCount (line 4) | function estimateTokenCount(text: string): number {
function createTranscriptionPrompt (line 11) | function createTranscriptionPrompt(vocabulary: string[]): string {
FILE: server/src/services/__tests__/helpers.ts
type AnyObject (line 4) | type AnyObject = Record<string, any>
function createTestApp (line 6) | function createTestApp(): FastifyInstance {
function addAuthHook (line 10) | function addAuthHook(
function createTestAppWithAuth (line 19) | function createTestAppWithAuth(
function createEnvReset (line 27) | function createEnvReset() {
FILE: server/src/services/auth0.ts
type SendVerificationBody (line 3) | type SendVerificationBody = {
FILE: server/src/services/billing.test.ts
class Stripe (line 26) | class Stripe {
method constructor (line 30) | constructor(_apiKey: string) {
FILE: server/src/services/billing.ts
type Options (line 9) | type Options = {
function getEnv (line 13) | function getEnv(name: string): string {
function renderDeepLinkHtml (line 19) | function renderDeepLinkHtml(targetUrl: string): string {
FILE: server/src/services/cloudWatchLogger.ts
type CloudWatchLogEntry (line 8) | interface CloudWatchLogEntry {
class CloudWatchLogger (line 16) | class CloudWatchLogger {
method constructor (line 22) | constructor(logGroupName?: string | null, logStreamNameSuffix?: string) {
method ensureStream (line 33) | async ensureStream(): Promise<void> {
method sendLogs (line 65) | async sendLogs(entries: CloudWatchLogEntry[]): Promise<boolean> {
method isConfigured (line 110) | isConfigured(): boolean {
FILE: server/src/services/ito/audioUtils.ts
function createWavHeader (line 4) | function createWavHeader(
FILE: server/src/services/ito/constants.ts
constant ITO_MODE_PROMPT (line 4) | const ITO_MODE_PROMPT: { [key in ItoMode]: string } = {
constant ITO_MODE_SYSTEM_PROMPT (line 9) | const ITO_MODE_SYSTEM_PROMPT: { [key in ItoMode]: string } = {
constant DEFAULT_ADVANCED_SETTINGS_STRUCT (line 14) | const DEFAULT_ADVANCED_SETTINGS_STRUCT = {
FILE: server/src/services/ito/helpers.ts
function createUserPromptWithContext (line 17) | function createUserPromptWithContext(
function validateAndTransformHeaderValue (line 42) | function validateAndTransformHeaderValue<T>(
function getAdvancedSettingsHeaders (line 61) | function getAdvancedSettingsHeaders(headers: Headers) {
function getItoMode (line 147) | function getItoMode(input: unknown): ItoMode | undefined {
function detectItoMode (line 161) | function detectItoMode(transcript: string): ItoMode {
function getPromptForMode (line 168) | function getPromptForMode(
FILE: server/src/services/ito/itoService.ts
function dbToNotePb (line 39) | function dbToNotePb(dbNote: DbNote): Note {
function dbToInteractionPb (line 51) | function dbToInteractionPb(
function dbToDictionaryItemPb (line 83) | function dbToDictionaryItemPb(
function dbToAdvancedSettingsPb (line 97) | function dbToAdvancedSettingsPb(
method transcribeStreamV2 (line 128) | async transcribeStreamV2(
method transcribeStream (line 139) | async transcribeStream(
method createNote (line 145) | async createNote(request, context: HandlerContext) {
method getNote (line 156) | async getNote(request) {
method listNotes (line 164) | async listNotes(request, context: HandlerContext) {
method updateNote (line 177) | async updateNote(request) {
method deleteNote (line 185) | async deleteNote(request) {
method createInteraction (line 190) | async createInteraction(request, context: HandlerContext) {
method getInteraction (line 245) | async getInteraction(request) {
method listInteractions (line 276) | async listInteractions(request, context: HandlerContext) {
method updateInteraction (line 335) | async updateInteraction(request) {
method deleteInteraction (line 346) | async deleteInteraction(request) {
method createDictionaryItem (line 351) | async createDictionaryItem(request, context: HandlerContext) {
method listDictionaryItems (line 362) | async listDictionaryItems(request, context: HandlerContext) {
method updateDictionaryItem (line 375) | async updateDictionaryItem(request) {
method deleteDictionaryItem (line 386) | async deleteDictionaryItem(request) {
method deleteUserData (line 391) | async deleteUserData(_request, context: HandlerContext) {
method getAdvancedSettings (line 415) | async getAdvancedSettings(_request, context: HandlerContext) {
method updateAdvancedSettings (line 438) | async updateAdvancedSettings(request, context: HandlerContext) {
FILE: server/src/services/ito/timingService.ts
constant TIMING_BUCKET (line 14) | const TIMING_BUCKET = process.env.TIMING_BUCKET
method submitTimingReports (line 34) | async submitTimingReports(
FILE: server/src/services/ito/transcribeStreamHandler.ts
class TranscribeStreamHandler (line 39) | class TranscribeStreamHandler {
method process (line 40) | async process(requests: AsyncIterable<AudioChunk>, context: HandlerCon...
FILE: server/src/services/ito/transcribeStreamV2Handler.ts
class TranscribeStreamV2Handler (line 33) | class TranscribeStreamV2Handler {
method process (line 36) | async process(
method collectStreamData (line 184) | private async collectStreamData(
method applyModeGracePeriod (line 241) | private applyModeGracePeriod(
method extractAsrConfig (line 273) | private extractAsrConfig(mergedConfig: StreamConfig) {
method resolveOrDefault (line 295) | private resolveOrDefault<T extends string | number>(
method prepareAdvancedSettings (line 305) | private prepareAdvancedSettings(
method transcribeAudioData (line 348) | private async transcribeAudioData(
method adjustTranscriptForMode (line 375) | private async adjustTranscriptForMode(
method mergeStreamConfigs (line 410) | private mergeStreamConfigs(
FILE: server/src/services/ito/types.ts
type ItoContext (line 1) | type ItoContext = {
FILE: server/src/services/logging.test.ts
class CreateLogStreamCommand (line 18) | class CreateLogStreamCommand {
method constructor (line 20) | constructor(input: AnyObject) {
class DescribeLogStreamsCommand (line 24) | class DescribeLogStreamsCommand {
method constructor (line 26) | constructor(input: AnyObject) {
class PutLogEventsCommand (line 30) | class PutLogEventsCommand {
method constructor (line 32) | constructor(input: AnyObject) {
class CloudWatchLogsClient (line 36) | class CloudWatchLogsClient {
method send (line 37) | async send(command: any): Promise<any> {
FILE: server/src/services/logging.ts
type LogEvent (line 4) | type LogEvent = {
FILE: server/src/services/stripeWebhook.test.ts
class Stripe (line 23) | class Stripe {
method constructor (line 27) | constructor(_apiKey: string) {
FILE: server/src/services/timing/ServerTimingCollector.ts
type ServerTimingEventName (line 7) | enum ServerTimingEventName {
constant TIMING_BUCKET (line 16) | const TIMING_BUCKET = process.env.TIMING_BUCKET
type TimingEvent (line 31) | interface TimingEvent {
type TimingReport (line 38) | interface TimingReport {
type ActiveTiming (line 45) | interface ActiveTiming {
class ServerTimingCollector (line 55) | class ServerTimingCollector {
method constructor (line 66) | constructor() {
method startInteraction (line 74) | startInteraction(interactionId?: string, userId?: string) {
method startTiming (line 93) | startTiming(eventName: ServerTimingEventName, interactionId?: string) {
method endTiming (line 116) | endTiming(eventName: ServerTimingEventName, interactionId?: string) {
method finalizeInteraction (line 144) | finalizeInteraction(interactionId?: string) {
method clearInteraction (line 206) | clearInteraction(interactionId?: string) {
method flush (line 218) | async flush({ flushAll = false } = {}) {
method scheduleFlush (line 301) | private scheduleFlush() {
method shutdown (line 312) | async shutdown() {
method timeAsync (line 328) | async timeAsync<T>(
FILE: server/src/services/trial.ts
constant TRIAL_DAYS (line 9) | const TRIAL_DAYS = 14
constant MS_PER_DAY (line 10) | const MS_PER_DAY = 24 * 60 * 60 * 1000
function getOrCreateStripeCustomer (line 12) | async function getOrCreateStripeCustomer(
function computeStatusFromStripe (line 62) | function computeStatusFromStripe(subscription: Stripe.Subscription): {
function computeStatus (line 103) | function computeStatus(row: {
FILE: server/src/services/validationInterceptor.ts
function createValidationInterceptor (line 5) | function createValidationInterceptor(): Interceptor {
FILE: server/src/utils/abortUtils.ts
function isAbortError (line 15) | function isAbortError(err: unknown): boolean {
function createAbortError (line 32) | function createAbortError(
FILE: server/src/utils/audio.ts
function enhancePcm16 (line 7) | function enhancePcm16(pcm: Buffer, sampleRate: number): Buffer {
FILE: server/src/utils/audioProcessing.ts
function concatenateAudioChunks (line 10) | function concatenateAudioChunks(audioChunks: Uint8Array[]): Uint8Array {
function prepareAudioForTranscription (line 37) | function prepareAudioForTranscription(audioData: Uint8Array): Buffer {
FILE: server/src/utils/renderCallback.ts
constant ITO_ENV (line 1) | const ITO_ENV = (process.env.ITO_ENV || 'prod').toLowerCase()
constant DEEPLINK_SCHEME (line 2) | const DEEPLINK_SCHEME = ITO_ENV === 'prod' ? 'ito' : `ito-dev`
type CallbackPageParams (line 4) | interface CallbackPageParams {
function renderCallbackPage (line 9) | function renderCallbackPage(params: CallbackPageParams): string {
FILE: server/src/validation/HeaderValidator.ts
class HeaderValidator (line 17) | class HeaderValidator {
method validateAsrModel (line 18) | static validateAsrModel(headerValue: string): string {
method validateAsrProvider (line 29) | static validateAsrProvider(headerValue: string): string {
method validateAsrPrompt (line 40) | static validateAsrPrompt(headerValue: string): string {
method validateLlmProvider (line 51) | static validateLlmProvider(headerValue: string): string {
method validateLlmModel (line 62) | static validateLlmModel(headerValue: string): string {
method validateLlmTemperature (line 73) | static validateLlmTemperature(headerValue: number): number {
method validateTranscriptionPrompt (line 84) | static validateTranscriptionPrompt(headerValue: string): string {
method validateEditingPrompt (line 99) | static validateEditingPrompt(headerValue: string): string {
method validateNoSpeechThreshold (line 110) | static validateNoSpeechThreshold(headerValue: number): number {
method validateVocabulary (line 121) | static validateVocabulary(headerValue: string): string[] {
FILE: server/src/validation/schemas.ts
type ValidatedHeaders (line 90) | type ValidatedHeaders = z.infer<typeof HeaderSchema>
FILE: server/test-client.ts
constant TEST_JWT_TOKEN (line 24) | const TEST_JWT_TOKEN = process.env.TEST_JWT_TOKEN || 'your-jwt-token-here'
function testNotesApi (line 39) | async function testNotesApi() {
function testInteractionsApi (line 112) | async function testInteractionsApi() {
function testDictionaryApi (line 207) | async function testDictionaryApi() {
function testHealthEndpoint (line 287) | async function testHealthEndpoint() {
function runTests (line 302) | async function runTests() {
FILE: shared-constants.js
constant DEFAULT_ADVANCED_SETTINGS (line 6) | const DEFAULT_ADVANCED_SETTINGS = {
FILE: vite-env.d.ts
type ImportMetaEnv (line 1) | interface ImportMetaEnv {
type ImportMeta (line 13) | interface ImportMeta {
Condensed preview — 400 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,391K chars).
[
{
"path": ".claude/settings.local.json",
"chars": 684,
"preview": "{\n \"permissions\": {\n \"allow\": [\n \"WebFetch(domain:github.com)\",\n \"WebFetch(domain:docs.microsoft.com)\",\n "
},
{
"path": ".cursor/rules/always.mdc",
"chars": 502,
"preview": "---\ndescription: \nglobs: \nalwaysApply: true\n---\n## Development cycle\n\n1. Before writing any code, come up with an extrem"
},
{
"path": ".cursor/rules/code-conventions.mdc",
"chars": 1800,
"preview": "---\ndescription: \nglobs: **/*.ts,**/*.tsx\nalwaysApply: false\n---\n## Technologies used\n\n- **React** - Build interactive U"
},
{
"path": ".cursor/rules/react.mdc",
"chars": 517,
"preview": "---\ndescription: \nglobs: **/*.tsx\nalwaysApply: false\n---\nReact Naming Conventions:\n\n- Use kebab-case for files and direc"
},
{
"path": ".cursor/rules/typescript.mdc",
"chars": 1688,
"preview": "---\ndescription: \nglobs: **/*.ts,**/*.tsx\nalwaysApply: false\n---\n# TypeScript Best Practices\n\n## Type System\n- Prefer in"
},
{
"path": ".gitattributes",
"chars": 196,
"preview": "# Mark generated protobuf/buf code as generated\n# This makes GitHub collapse these files by default in PRs\napp/generated"
},
{
"path": ".github/workflows/app-deploy.yml",
"chars": 1815,
"preview": "# .github/workflows/app-deploy.yml\nname: Run Migrations and Deploy App\n\non:\n workflow_call:\n inputs:\n environme"
},
{
"path": ".github/workflows/autolink-pr-to-issue.yml",
"chars": 1717,
"preview": "name: Auto-link PR to Issue\n\non:\n pull_request:\n types: [opened]\n\njobs:\n autolink:\n runs-on: ubuntu-latest\n s"
},
{
"path": ".github/workflows/build-image.yml",
"chars": 1333,
"preview": "name: Build and Push Docker Image\n\non:\n workflow_call:\n inputs:\n environment:\n type: string\n requ"
},
{
"path": ".github/workflows/build.yml",
"chars": 17393,
"preview": "name: Build and Release\n\non:\n release:\n types: [published]\n\npermissions:\n contents: write\n id-token: write # requi"
},
{
"path": ".github/workflows/ci-controller.yml",
"chars": 669,
"preview": "name: CI Controller\n\non:\n push:\n branches:\n - 'main'\n - 'dev'\n pull_request:\n branches:\n - '**'\n\n"
},
{
"path": ".github/workflows/deploy-server.yml",
"chars": 2186,
"preview": "name: Deploy Server Worfklow\n\non:\n workflow_call:\n inputs:\n environment:\n type: string\n required:"
},
{
"path": ".github/workflows/infra-deploy.yml",
"chars": 2337,
"preview": "name: Infra CDK\n\non:\n workflow_call:\n inputs:\n environment:\n type: string\n required: true\n "
},
{
"path": ".github/workflows/native-build-check.yml",
"chars": 2076,
"preview": "name: Native Build Check\n\non:\n workflow_call:\n\njobs:\n build-check-mac:\n runs-on: macos-latest\n\n steps:\n - n"
},
{
"path": ".github/workflows/test-runner.yml",
"chars": 931,
"preview": "name: Test Runner\n\non:\n workflow_call:\n\njobs:\n test:\n runs-on: macos-latest\n env:\n ITO_ENV: dev\n\n steps:"
},
{
"path": ".gitignore",
"chars": 339,
"preview": "out/*\nrelease/*\nnode_modules\n.env\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n*/dist/*\ndist/"
},
{
"path": ".prettierignore",
"chars": 674,
"preview": "node_modules/\ndist/\nbuild/\n.next/\ncoverage/\nout/\n\n# Generated proto files\n**/generated/\nlib/generated/\napp/generated/\nse"
},
{
"path": ".prettierrc.json",
"chars": 86,
"preview": "{\n \"semi\": false,\n \"singleQuote\": true,\n \"arrowParens\": \"avoid\",\n \"tabWidth\": 2\n}\n"
},
{
"path": ".vscode/settings.json",
"chars": 823,
"preview": "{\n \"eslint.validate\": [\n \"javascript\",\n \"javascriptreact\",\n \"typescript\",\n \"typescriptreact\"\n ],\n \"editor"
},
{
"path": "AGENTS.md",
"chars": 2642,
"preview": "# Repository Guidelines\n\n## Project Structure & Module Organization\n\n- `app/` hosts the Electron renderer (React + Tailw"
},
{
"path": "CLAUDE.md",
"chars": 3779,
"preview": "# Claude Context for ITO Project\n\n## Project Overview\n\nThis is the ITO project - an AI assistant application with both c"
},
{
"path": "LICENSE",
"chars": 695,
"preview": "GNU GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007\n\nCopyright (C) 2024 Demox Labs\n\nThis program is free software: you ca"
},
{
"path": "README.md",
"chars": 14142,
"preview": "# [DEPRECATED] - This project is no longer maintained \n\n# Ito\n\n<div align=\"center\">\n <img src=\"resources/build/icon.png"
},
{
"path": "app/app.tsx",
"chars": 2373,
"preview": "import { HashRouter, Routes, Route } from 'react-router-dom'\nimport appIcon from '@/resources/build/icon.png'\nimport Hom"
},
{
"path": "app/assets/.gitignore",
"chars": 0,
"preview": ""
},
{
"path": "app/components/analytics/index.ts",
"chars": 14432,
"preview": "import posthog from 'posthog-js'\nimport log from 'electron-log'\nimport { STORE_KEYS } from '../../../lib/constants/store"
},
{
"path": "app/components/auth/Auth0Provider.tsx",
"chars": 928,
"preview": "import React from 'react'\nimport { Auth0Provider as Auth0ReactProvider } from '@auth0/auth0-react'\nimport { Auth0Config,"
},
{
"path": "app/components/auth/useAuth.ts",
"chars": 23071,
"preview": "import { useAuth0 } from '@auth0/auth0-react'\nimport { useCallback, useEffect, useMemo } from 'react'\nimport { Auth0Conn"
},
{
"path": "app/components/home/HomeKit.tsx",
"chars": 8655,
"preview": "import {\n Home,\n BookOpen,\n FileText,\n CogFour,\n InfoCircle,\n} from '@mynaui/icons-react'\nimport { ItoIcon } from '"
},
{
"path": "app/components/home/ProUpgradeDialog.tsx",
"chars": 4765,
"preview": "import React, { useState, useEffect } from 'react'\nimport { Check } from '@mynaui/icons-react'\nimport { Dialog, DialogCo"
},
{
"path": "app/components/home/contents/AboutContent.tsx",
"chars": 4371,
"preview": "import { Button } from '@/app/components/ui/button'\nimport DiscordIcon from '@/app/components/icons/DiscordIcon'\nimport "
},
{
"path": "app/components/home/contents/DictionaryContent.tsx",
"chars": 17792,
"preview": "import { useEffect, useRef, useState } from 'react'\nimport { ArrowUp, Pencil, Trash, Plus } from '@mynaui/icons-react'\ni"
},
{
"path": "app/components/home/contents/HomeContent.tsx",
"chars": 30188,
"preview": "import React, { useCallback, useEffect, useState } from 'react'\nimport {\n ChartNoAxesColumn,\n InfoCircle,\n Play,\n St"
},
{
"path": "app/components/home/contents/NotesContent.tsx",
"chars": 17834,
"preview": "import { useEffect, useRef, useState } from 'react'\nimport { useNotesStore } from '../../../store/useNotesStore'\nimport "
},
{
"path": "app/components/home/contents/SettingsContent.tsx",
"chars": 2542,
"preview": "import { useMainStore } from '@/app/store/useMainStore'\nimport GeneralSettingsContent from './settings/GeneralSettingsCo"
},
{
"path": "app/components/home/contents/activityMessages.ts",
"chars": 4551,
"preview": "// Activity message categories and levels\nexport interface ActivityMessage {\n text: string\n}\n\nexport interface Activity"
},
{
"path": "app/components/home/contents/settings/AccountSettingsContent.tsx",
"chars": 5221,
"preview": "import React, { useState } from 'react'\nimport { useNotesStore } from '../../../../store/useNotesStore'\nimport { useDict"
},
{
"path": "app/components/home/contents/settings/AdvancedSettingsContent.tsx",
"chars": 12590,
"preview": "import {\n LlmSettings,\n useAdvancedSettingsStore,\n} from '@/app/store/useAdvancedSettingsStore'\nimport {\n ChangeEvent"
},
{
"path": "app/components/home/contents/settings/AudioSettingsContent.tsx",
"chars": 2280,
"preview": "import { Switch } from '@/app/components/ui/switch'\nimport { MicrophoneSelector } from '@/app/components/ui/microphone-s"
},
{
"path": "app/components/home/contents/settings/GeneralSettingsContent.tsx",
"chars": 5469,
"preview": "import { useState } from 'react'\nimport { Switch } from '@/app/components/ui/switch'\nimport { Button } from '@/app/compo"
},
{
"path": "app/components/home/contents/settings/KeyboardSettingsContent.tsx",
"chars": 1734,
"preview": "import { useSettingsStore } from '@/app/store/useSettingsStore'\nimport { ItoMode } from '@/app/generated/ito_pb'\nimport "
},
{
"path": "app/components/home/contents/settings/PricingBillingSettingsContent.tsx",
"chars": 10164,
"preview": "import { useState, useEffect } from 'react'\nimport { Button } from '@/app/components/ui/button'\nimport { Check } from '@"
},
{
"path": "app/components/icons/AppleIcon.tsx",
"chars": 1360,
"preview": "import React from 'react'\n\ninterface AppleIconProps {\n width?: number\n height?: number\n className?: string\n}\n\nexport "
},
{
"path": "app/components/icons/AppleNotesIcon.tsx",
"chars": 2255,
"preview": "import React from 'react'\n\ninterface AppleNotesIconProps extends React.SVGProps<SVGSVGElement> {\n width?: number\n heig"
},
{
"path": "app/components/icons/AsterikIcon.tsx",
"chars": 1143,
"preview": "const AsterikIcon = props => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n data-name=\"Layer 1\"\n viewBox=\"0 0 24"
},
{
"path": "app/components/icons/AudioIcon.tsx",
"chars": 561,
"preview": "export const AudioIcon = () => {\n return (\n <svg\n width=\"24\"\n height=\"16\"\n viewBox=\"0 0 24 16\"\n "
},
{
"path": "app/components/icons/AvatarIcon.tsx",
"chars": 384,
"preview": "const AvatarIcon = (props: any) => (\n <svg\n viewBox=\"0 0 128 128\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/"
},
{
"path": "app/components/icons/ChatGPTIcon.tsx",
"chars": 1932,
"preview": "interface ChatGPTIconProps {\n className?: string\n}\n\nconst ChatGPTIcon = ({ className }: ChatGPTIconProps) => (\n <svg\n "
},
{
"path": "app/components/icons/ClaudeIcon.tsx",
"chars": 2433,
"preview": "import React from 'react'\n\ninterface ClaudeIconProps extends React.SVGProps<SVGSVGElement> {\n width?: number\n height?:"
},
{
"path": "app/components/icons/CodeWindowIcon.tsx",
"chars": 844,
"preview": "const CodeWindowIcon = props => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n data-name=\"Layer 1\"\n viewBox=\"0 0"
},
{
"path": "app/components/icons/ColorSchemeIcon.tsx",
"chars": 1326,
"preview": "const ColorSchemeIcon = props => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n data-name=\"Layer 1\"\n viewBox=\"0 "
},
{
"path": "app/components/icons/CursorIcon.tsx",
"chars": 1815,
"preview": "const CursorIcon = _props => (\n <svg\n height=\"100%\"\n style={{ flex: 'none', lineHeight: 1 }}\n viewBox=\"0 0 24 "
},
{
"path": "app/components/icons/DiscordIcon.tsx",
"chars": 2141,
"preview": "import React from 'react'\n\ninterface DiscordIconProps {\n width?: number\n height?: number\n className?: string\n}\n\nexpor"
},
{
"path": "app/components/icons/FanIcon.tsx",
"chars": 2044,
"preview": "const CodeWindowIcon = props => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n data-name=\"Layer 1\"\n viewBox=\"0 0"
},
{
"path": "app/components/icons/GitHubIcon.tsx",
"chars": 1736,
"preview": "import React from 'react'\n\ninterface GitHubIconProps {\n width?: number\n height?: number\n className?: string\n}\n\nexport"
},
{
"path": "app/components/icons/GmailIcon.tsx",
"chars": 990,
"preview": "interface GmailIconProps {\n className?: string\n}\n\nconst GmailIcon = ({ className }: GmailIconProps) => (\n <svg\n fil"
},
{
"path": "app/components/icons/GoogleIcon.tsx",
"chars": 1409,
"preview": "import React from 'react'\n\ninterface GoogleIconProps {\n width?: number\n height?: number\n className?: string\n}\n\nexport"
},
{
"path": "app/components/icons/IMessageIcon.tsx",
"chars": 1513,
"preview": "import React from 'react'\n\ninterface IMessageIconProps extends React.SVGProps<SVGSVGElement> {\n width?: number\n height"
},
{
"path": "app/components/icons/ItoIcon.tsx",
"chars": 1678,
"preview": "import React from 'react'\n\ninterface ItoIconProps {\n className?: string\n style?: React.CSSProperties\n width?: number\n"
},
{
"path": "app/components/icons/MicrosoftIcon.tsx",
"chars": 898,
"preview": "import React from 'react'\n\ninterface MicrosoftIconProps {\n width?: number\n height?: number\n className?: string\n}\n\nexp"
},
{
"path": "app/components/icons/NotionIcon.tsx",
"chars": 3898,
"preview": "const NotionIcon = ({ className }: { className?: string }) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n x=\"0px"
},
{
"path": "app/components/icons/SlackIcon.tsx",
"chars": 2173,
"preview": "interface SlackIconProps {\n className?: string\n}\n\nconst SlackIcon = ({ className }: SlackIconProps) => (\n <svg\n hei"
},
{
"path": "app/components/icons/SpeedIcon.tsx",
"chars": 1105,
"preview": "export const SpeedIcon = () => {\n return (\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n "
},
{
"path": "app/components/icons/TotalWordsIcon.tsx",
"chars": 1447,
"preview": "export const TotalWordsIcon = () => {\n return (\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n "
},
{
"path": "app/components/icons/VSCodeIcon.tsx",
"chars": 1144,
"preview": "import React from 'react'\n\ninterface VSCodeIconProps extends React.SVGProps<SVGSVGElement> {\n width?: number\n height?:"
},
{
"path": "app/components/icons/XIcon.tsx",
"chars": 578,
"preview": "import React from 'react'\n\ninterface XIconProps {\n width?: number\n height?: number\n className?: string\n}\n\nexport defa"
},
{
"path": "app/components/pill/Pill.tsx",
"chars": 12327,
"preview": "import React, { useState, useEffect, useRef } from 'react'\nimport { useSettingsStore } from '../../store/useSettingsStor"
},
{
"path": "app/components/pill/contents/AudioBars.tsx",
"chars": 1040,
"preview": "import { useEffect, useState } from 'react'\nimport { AudioBarsBase, BAR_COUNT } from './AudioBarsBase'\n\n// A new compone"
},
{
"path": "app/components/pill/contents/AudioBarsBase.tsx",
"chars": 678,
"preview": "interface AudioBarsBaseProps {\n heights: number[]\n barColor: string\n}\n\nexport const BAR_COUNT = 21\n\nexport const Audio"
},
{
"path": "app/components/pill/contents/LoadingAnimation.tsx",
"chars": 1995,
"preview": "import React from 'react'\n\ninterface LoadingAnimationProps {\n color?: string\n}\n\nexport const LoadingAnimation: React.FC"
},
{
"path": "app/components/pill/contents/PreviewAudioBars.tsx",
"chars": 325,
"preview": "import { AudioBarsBase } from './AudioBarsBase'\n\nexport const PreviewAudioBars = () => {\n // Create varied static heigh"
},
{
"path": "app/components/pill/contents/TooltipButton.tsx",
"chars": 966,
"preview": "import {\n Tooltip,\n TooltipTrigger,\n TooltipContent,\n} from '@/app/components/ui/tooltip'\n\ninterface TooltipButtonPro"
},
{
"path": "app/components/ui/animated-checkmark.tsx",
"chars": 1351,
"preview": "import { useState, useEffect, useRef } from 'react'\nimport { Check } from '@mynaui/icons-react'\n\n// AnimatedCheck compon"
},
{
"path": "app/components/ui/app-orbit-image.tsx",
"chars": 7077,
"preview": "import ItoIcon from '../icons/ItoIcon'\nimport GitHubIcon from '../icons/GitHubIcon'\nimport NotionIcon from '../icons/Not"
},
{
"path": "app/components/ui/badge.tsx",
"chars": 1632,
"preview": "import * as React from 'react'\nimport { Slot } from '@radix-ui/react-slot'\nimport { cva, type VariantProps } from 'class"
},
{
"path": "app/components/ui/button.tsx",
"chars": 2124,
"preview": "import * as React from 'react'\nimport { Slot } from '@radix-ui/react-slot'\nimport { cva, type VariantProps } from 'class"
},
{
"path": "app/components/ui/dialog.tsx",
"chars": 3971,
"preview": "import * as React from 'react'\nimport * as DialogPrimitive from '@radix-ui/react-dialog'\nimport { XIcon } from 'lucide-r"
},
{
"path": "app/components/ui/dropdown-menu.tsx",
"chars": 8278,
"preview": "import * as React from 'react'\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'\nimport { CheckIcon"
},
{
"path": "app/components/ui/keyboard-key.tsx",
"chars": 5431,
"preview": "import { ComponentPropsWithoutRef } from 'react'\nimport clsx from 'clsx'\nimport { cx } from 'class-variance-authority'\ni"
},
{
"path": "app/components/ui/keyboard-shortcut-editor.tsx",
"chars": 10357,
"preview": "import { useEffect, useCallback, useRef, useState, useMemo } from 'react'\nimport { Button } from '@/app/components/ui/bu"
},
{
"path": "app/components/ui/microphone-selector.tsx",
"chars": 4952,
"preview": "import { Button } from '@/app/components/ui/button'\nimport { useEffect, useState } from 'react'\nimport {\n getAvailableM"
},
{
"path": "app/components/ui/multi-shortcut-editor.tsx",
"chars": 9400,
"preview": "import { useCallback, useEffect, useRef, useState, useMemo } from 'react'\nimport KeyboardKey from '@/app/components/ui/k"
},
{
"path": "app/components/ui/nav-item.tsx",
"chars": 1124,
"preview": "import { ReactNode } from 'react'\nimport { Tooltip, TooltipTrigger, TooltipContent } from './tooltip'\n\ninterface NavItem"
},
{
"path": "app/components/ui/note.tsx",
"chars": 4133,
"preview": "import React from 'react'\nimport { Pencil, Copy, Trash, Dots } from '@mynaui/icons-react'\nimport type { Note } from '../"
},
{
"path": "app/components/ui/spinner.tsx",
"chars": 1005,
"preview": "import React from 'react'\nimport { cn } from '@/lib/utils'\nimport { VariantProps, cva } from 'class-variance-authority'\n"
},
{
"path": "app/components/ui/status-indicator.tsx",
"chars": 1594,
"preview": "import { useEffect, useState } from 'react'\nimport { Check, X } from '@mynaui/icons-react'\n\ninterface StatusIndicatorPro"
},
{
"path": "app/components/ui/switch.tsx",
"chars": 1165,
"preview": "import * as React from 'react'\nimport * as SwitchPrimitive from '@radix-ui/react-switch'\n\nimport { cn } from '@/lib/util"
},
{
"path": "app/components/ui/tip.tsx",
"chars": 533,
"preview": "import { InfoCircleSolid } from '@mynaui/icons-react'\nimport { cx } from 'class-variance-authority'\n\nexport function Tip"
},
{
"path": "app/components/ui/tooltip.tsx",
"chars": 1738,
"preview": "import * as React from 'react'\nimport * as TooltipPrimitive from '@radix-ui/react-tooltip'\n\nimport { cn } from '@/lib/ut"
},
{
"path": "app/components/welcome/WelcomeKit.tsx",
"chars": 2342,
"preview": "import CreateAccountContent from './contents/CreateAccountContent'\nimport SignInContent from './contents/SignInContent'\n"
},
{
"path": "app/components/welcome/contents/AnyAppContent.tsx",
"chars": 1522,
"preview": "import { Button } from '@/app/components/ui/button'\nimport { useOnboardingStore } from '@/app/store/useOnboardingStore'\n"
},
{
"path": "app/components/welcome/contents/CheckEmailContent.tsx",
"chars": 4719,
"preview": "import { useEffect, useState } from 'react'\nimport { Button } from '@/app/components/ui/button'\nimport { AppOrbitImage }"
},
{
"path": "app/components/welcome/contents/CreateAccountContent.tsx",
"chars": 12504,
"preview": "import { Button } from '@/app/components/ui/button'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHea"
},
{
"path": "app/components/welcome/contents/DataControlContent.tsx",
"chars": 4135,
"preview": "import { Button } from '@/app/components/ui/button'\nimport { CheckCircle, Lock } from '@mynaui/icons-react'\nimport { EXT"
},
{
"path": "app/components/welcome/contents/EmailLoginContent.tsx",
"chars": 4369,
"preview": "import { useMemo, useState } from 'react'\nimport { Button } from '@/app/components/ui/button'\nimport { AppOrbitImage } f"
},
{
"path": "app/components/welcome/contents/EmailSignupContent.tsx",
"chars": 5857,
"preview": "import { useMemo, useState } from 'react'\nimport { Button } from '@/app/components/ui/button'\nimport { AppOrbitImage } f"
},
{
"path": "app/components/welcome/contents/GoodToGoContent.tsx",
"chars": 1412,
"preview": "import { Button } from '@/app/components/ui/button'\nimport { useOnboardingStore } from '@/app/store/useOnboardingStore'\n"
},
{
"path": "app/components/welcome/contents/IntroducingIntelligentModeContent.tsx",
"chars": 3821,
"preview": "import { Button } from '@/app/components/ui/button'\nimport { useOnboardingStore } from '@/app/store/useOnboardingStore'\n"
},
{
"path": "app/components/welcome/contents/KeyboardTestContext.tsx",
"chars": 3338,
"preview": "import { useOnboardingStore } from '@/app/store/useOnboardingStore'\nimport { useSettingsStore } from '@/app/store/useSet"
},
{
"path": "app/components/welcome/contents/MicrophoneTestContent.tsx",
"chars": 5035,
"preview": "import { Button } from '@/app/components/ui/button'\nimport { useEffect, useState } from 'react'\nimport { useOnboardingSt"
},
{
"path": "app/components/welcome/contents/PermissionsContent.tsx",
"chars": 11184,
"preview": "import { Button } from '@/app/components/ui/button'\nimport { InfoCircle } from '@mynaui/icons-react'\nimport {\n Tooltip,"
},
{
"path": "app/components/welcome/contents/ReferralContent.tsx",
"chars": 3224,
"preview": "import { Button } from '@/app/components/ui/button'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n"
},
{
"path": "app/components/welcome/contents/SignInContent.tsx",
"chars": 14789,
"preview": "import { Button } from '@/app/components/ui/button'\nimport {\n Tooltip,\n TooltipTrigger,\n TooltipContent,\n} from '@/ap"
},
{
"path": "app/components/welcome/contents/TryItOutContent.tsx",
"chars": 11456,
"preview": "import { Button } from '@/app/components/ui/button'\nimport { useOnboardingStore } from '@/app/store/useOnboardingStore'\n"
},
{
"path": "app/components/welcome/styles.css",
"chars": 1959,
"preview": ":root {\n --welcome-c-variant: #8aa6cf;\n}\n\n:root:not(.dark) {\n --welcome-c-variant: #43679d;\n}\n\n#era-shape {\n position"
},
{
"path": "app/components/window/OnboardingTitlebar.tsx",
"chars": 2960,
"preview": "import React from 'react'\nimport {\n getOnboardingCategoryIndex,\n useOnboardingStore,\n} from '@/app/store/useOnboarding"
},
{
"path": "app/components/window/Titlebar.tsx",
"chars": 8331,
"preview": "import { useWindowContext } from './WindowContext'\nimport React, { useState, useEffect } from 'react'\nimport { Onboardin"
},
{
"path": "app/components/window/TitlebarContext.tsx",
"chars": 657,
"preview": "import { createContext, useContext } from 'react'\n\nconst TitlebarContext = createContext<TitlebarContextProps | undefine"
},
{
"path": "app/components/window/WindowContext.tsx",
"chars": 1953,
"preview": "import { createContext, useContext, useEffect, useState } from 'react'\nimport { Titlebar, TitlebarProps } from './Titleb"
},
{
"path": "app/generated/buf/validate/validate_pb.ts",
"chars": 225513,
"preview": "// Copyright 2023-2025 Buf Technologies, Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// y"
},
{
"path": "app/generated/ito_connect.ts",
"chars": 6391,
"preview": "// @generated by protoc-gen-connect-es v1.6.1 with parameter \"target=ts,import_extension=.js\"\n// @generated from file it"
},
{
"path": "app/generated/ito_pb.ts",
"chars": 38498,
"preview": "// @generated by protoc-gen-es v2.7.0 with parameter \"target=ts,import_extension=.js\"\n// @generated from file ito.proto "
},
{
"path": "app/hooks/useBillingState.test.ts",
"chars": 16810,
"preview": "import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test'\nimport React from 'react'\nimport { createRo"
},
{
"path": "app/hooks/useBillingState.ts",
"chars": 4777,
"preview": "import { useCallback, useEffect, useMemo, useState } from 'react'\n\nexport type BillingState = {\n proStatus: 'active_pro"
},
{
"path": "app/hooks/useDeviceChangeListener.ts",
"chars": 1245,
"preview": "import { useEffect } from 'react'\nimport log from 'electron-log'\n\n/**\n * A React hook that listens for changes in media "
},
{
"path": "app/hooks/usePlatform.ts",
"chars": 309,
"preview": "import { useState, useEffect } from 'react'\n\ntype Platform = 'darwin' | 'win32'\n\nexport function usePlatform(): Platform"
},
{
"path": "app/index.d.ts",
"chars": 1172,
"preview": "/// <reference types=\"electron-vite/node\" />\n\ndeclare module '*.css' {\n const content: string\n export default content\n"
},
{
"path": "app/index.html",
"chars": 227,
"preview": "<!doctype html>\n<html lang=\"en\" class=\"light\">\n <head>\n <meta charset=\"UTF-8\" />\n <title>Ito</title>\n </head>\n\n "
},
{
"path": "app/media/microphone.ts",
"chars": 3179,
"preview": "import { useSettingsStore } from '../store/useSettingsStore'\n\ntype Microphone = {\n deviceId: string\n label: string\n}\n\n"
},
{
"path": "app/renderer.tsx",
"chars": 319,
"preview": "import './sentry'\nimport React from 'react'\nimport ReactDOM from 'react-dom/client'\nimport App from './app'\n\nif (window."
},
{
"path": "app/sentry.ts",
"chars": 997,
"preview": "import * as Sentry from '@sentry/electron/renderer'\n\nconst dsn = import.meta.env.VITE_SENTRY_DSN as string | undefined\nc"
},
{
"path": "app/store/useAdvancedSettingsStore.ts",
"chars": 2675,
"preview": "import { create } from 'zustand'\nimport { STORE_KEYS } from '../../lib/constants/store-keys'\n\nexport interface LlmSettin"
},
{
"path": "app/store/useAudioStore.ts",
"chars": 1216,
"preview": "import { create } from 'zustand'\nimport log from 'electron-log'\n\ninterface AudioState {\n isRecording: boolean\n isShort"
},
{
"path": "app/store/useAuthStore.ts",
"chars": 4803,
"preview": "import { create } from 'zustand'\nimport type {\n AuthState,\n AuthUser,\n AuthTokens,\n AuthStore,\n} from '../../lib/mai"
},
{
"path": "app/store/useDictionaryStore.ts",
"chars": 3816,
"preview": "import { create } from 'zustand'\nimport { useAuthStore } from './useAuthStore'\nimport type { DictionaryItem } from '../."
},
{
"path": "app/store/useMainStore.ts",
"chars": 1954,
"preview": "import { create } from 'zustand'\nimport { STORE_KEYS } from '../../lib/constants/store-keys'\n\ntype PageType = 'home' | '"
},
{
"path": "app/store/useNotesStore.ts",
"chars": 1845,
"preview": "import { create } from 'zustand'\nimport { useAuthStore } from './useAuthStore'\n\nexport type Note = {\n id: string\n cont"
},
{
"path": "app/store/useOnboardingStore.ts",
"chars": 7540,
"preview": "import { create } from 'zustand'\nimport { analytics, ANALYTICS_EVENTS } from '../components/analytics'\nimport { STORE_KE"
},
{
"path": "app/store/usePermissionsStore.ts",
"chars": 527,
"preview": "import { create } from 'zustand'\n\ninterface PermissionsState {\n isAccessibilityEnabled: boolean\n isMicrophoneEnabled: "
},
{
"path": "app/store/useSettingsStore.ts",
"chars": 11440,
"preview": "import { create } from 'zustand'\nimport {\n analytics,\n ANALYTICS_EVENTS,\n updateAnalyticsFromSettings,\n} from '@/app/"
},
{
"path": "app/store/useShortcutEditingStore.ts",
"chars": 839,
"preview": "import { create } from 'zustand'\n\ninterface ShortcutEditingState {\n activeEditor: string | null\n start: (editorKey: st"
},
{
"path": "app/store/useUserMetadataStore.ts",
"chars": 3057,
"preview": "import { create } from 'zustand'\nimport { useAuthStore } from './useAuthStore'\nimport type { UserMetadata } from '../../"
},
{
"path": "app/styles/app.css",
"chars": 312,
"preview": "@import './globals.css';\n@import './window.css';\n\nbody {\n font-family:\n 'Inter',\n system-ui,\n -apple-system,\n "
},
{
"path": "app/styles/globals.css",
"chars": 3885,
"preview": "@import 'tailwindcss';\n@source '@/app';\n@source '@/lib';\n\n@theme {\n --color-background: var(--background);\n --color-fo"
},
{
"path": "app/styles/window.css",
"chars": 6123,
"preview": ":root {\n color-scheme: light;\n --window-icon-height: 16px;\n --window-title-margin: 42px;\n --window-titlebar-height: "
},
{
"path": "app/utils/audioUtils.ts",
"chars": 2040,
"preview": "export function createStereo48kWavFromMonoPCM(\n pcm16le: Uint8Array,\n srcRate = 16000,\n targetRate = 48000,\n bitsPer"
},
{
"path": "app/utils/healthCheck.test.ts",
"chars": 3249,
"preview": "import { describe, test, expect, mock, beforeEach } from 'bun:test'\nimport { checkLocalServerHealth } from './healthChec"
},
{
"path": "app/utils/healthCheck.ts",
"chars": 859,
"preview": "/**\n * Simple health check utility for the local Ito server using the main process\n */\n\nexport interface HealthCheckResu"
},
{
"path": "app/utils/keyboard.test.ts",
"chars": 7016,
"preview": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\nimport { KeyState } from './keyboard'\nimport type { "
},
{
"path": "app/utils/keyboard.ts",
"chars": 9074,
"preview": "import { KeyEvent } from '@/lib/preload'\nimport { KeyboardShortcutConfig } from '@/lib/main/store'\nimport { ItoMode } fr"
},
{
"path": "app/utils/utils.ts",
"chars": 504,
"preview": "export const isValidEmail = (email: string): boolean => {\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n return ema"
},
{
"path": "build/entitlements.mac.inherit.plist",
"chars": 525,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "build/entitlements.mac.plist",
"chars": 525,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "build-app.sh",
"chars": 11976,
"preview": "#!/bin/bash\n\n# Exit on error\nset -e\n\n# Load environment variables from .env file if it exists\nif [ -f .env ]; then\n exp"
},
{
"path": "build-binaries.sh",
"chars": 6629,
"preview": "#!/bin/bash\n\n# Exit immediately if a command exits with a non-zero status.\nset -e\n\n# --- Color Definitions for pretty pr"
},
{
"path": "commitlint.config.js",
"chars": 93,
"preview": "// commitlint.config.js\nmodule.exports = {\n extends: ['@commitlint/config-conventional'],\n}\n"
},
{
"path": "components.json",
"chars": 429,
"preview": "{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"new-york\",\n \"rsc\": false,\n \"tsx\": true,\n \"tailwind\": "
},
{
"path": "dev-app-update.yml",
"chars": 99,
"preview": "owner: heyito\nrepo: ito\nprovider: s3\nbucket: dev-ito-releases\npath: manual-test/\nregion: us-west-2\n"
},
{
"path": "electron-builder.config.js",
"chars": 3241,
"preview": "// Define the native binaries that are shared across platforms\nconst nativeBinaries = [\n 'global-key-listener',\n 'audi"
},
{
"path": "electron.vite.config.ts",
"chars": 1754,
"preview": "import { sentryVitePlugin } from '@sentry/vite-plugin'\nimport { resolve } from 'path'\nimport react from '@vitejs/plugin-"
},
{
"path": "eslint.config.mjs",
"chars": 3455,
"preview": "import eslint from '@eslint/js'\nimport tseslint from 'typescript-eslint'\nimport reactPlugin from 'eslint-plugin-react'\ni"
},
{
"path": "lib/__tests__/fixtures/auth.ts",
"chars": 4465,
"preview": "// JWT token fixtures for testing\nexport const VALID_JWT_TOKEN =\n 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN"
},
{
"path": "lib/__tests__/fixtures/database.ts",
"chars": 4684,
"preview": "import type {\n Interaction,\n Note,\n DictionaryItem,\n} from '../../main/sqlite/models'\n\n// Test user IDs\nexport const "
},
{
"path": "lib/__tests__/helpers/testUtils.ts",
"chars": 3370,
"preview": "import { mock } from 'bun:test'\nimport { install } from '@sinonjs/fake-timers'\nimport { afterAll, beforeEach } from 'bun"
},
{
"path": "lib/__tests__/mocks/electron.ts",
"chars": 2113,
"preview": "import { mock } from 'bun:test'\n\n// Mock electron app\nexport const mockApp = mock(() => ({\n getPath: mock((name: string"
},
{
"path": "lib/__tests__/mocks/sqlite.ts",
"chars": 3151,
"preview": "import { mock } from 'bun:test'\n\n// Mock SQLite database\nexport const mockSqliteDatabase = mock(() => {\n const db = new"
},
{
"path": "lib/__tests__/setup.ts",
"chars": 6725,
"preview": "import { mock, afterEach, beforeEach, beforeAll } from 'bun:test'\nimport { promises as fs } from 'fs'\n\n// Simple, direct"
},
{
"path": "lib/auth/config.test.ts",
"chars": 986,
"preview": "import { describe, test, expect } from 'bun:test'\nimport { Auth0Connections, RequiredAuth0Fields } from './config'\n\ndesc"
},
{
"path": "lib/auth/config.ts",
"chars": 972,
"preview": "// Auth0 configuration\nexport const Auth0Config = {\n domain: import.meta.env.VITE_AUTH0_DOMAIN,\n clientId: import.meta"
},
{
"path": "lib/auth/events.test.ts",
"chars": 20028,
"preview": "import { describe, test, expect, mock, beforeEach } from 'bun:test'\nimport {\n shouldRefreshToken,\n isTokenExpired,\n e"
},
{
"path": "lib/auth/events.ts",
"chars": 13672,
"preview": "import store, { AuthState, createNewAuthState } from '../main/store'\nimport { STORE_KEYS } from '../constants/store-keys"
},
{
"path": "lib/clients/grpcClient.test.ts",
"chars": 12043,
"preview": "import { describe, test, expect, beforeEach, mock, Mock } from 'bun:test'\nimport { ActiveWindow } from '../media/active-"
},
{
"path": "lib/clients/grpcClient.ts",
"chars": 16966,
"preview": "import {\n ItoService,\n TimingService,\n AudioChunk,\n Note as NotePb,\n Interaction as InteractionPb,\n DictionaryItem"
},
{
"path": "lib/clients/itoHttpClient.ts",
"chars": 2473,
"preview": "import store from '../main/store'\nimport { STORE_KEYS } from '../constants/store-keys'\n\ninterface RequestOptions {\n req"
},
{
"path": "lib/constants/external-links.ts",
"chars": 307,
"preview": "export const EXTERNAL_LINKS = {\n DISCORD: 'https://discord.gg/PME7NH38sn',\n TEAM_CALL: 'https://link.heyito.ai/calendl"
},
{
"path": "lib/constants/generated-defaults.ts",
"chars": 3365,
"preview": "/*\n * AUTO-GENERATED FILE - DO NOT EDIT\n * Generated from /shared-constants.js\n * Run 'bun generate:constants' to regene"
},
{
"path": "lib/constants/keyboard-defaults.ts",
"chars": 1165,
"preview": "import { ItoMode } from '@/app/generated/ito_pb'\n\n// Platform-specific keyboard shortcut defaults\nexport const ITO_MODE_"
},
{
"path": "lib/constants/store-keys.test.ts",
"chars": 714,
"preview": "import { describe, test, expect } from 'bun:test'\nimport { STORE_KEYS } from './store-keys'\n\ndescribe('STORE_KEYS', () ="
},
{
"path": "lib/constants/store-keys.ts",
"chars": 541,
"preview": "// Store key constants to avoid magic strings\n// This file can be imported by both main and renderer processes\nexport co"
},
{
"path": "lib/main/app.ts",
"chars": 6735,
"preview": "import { BrowserWindow, shell, screen, app, protocol, net } from 'electron'\nimport { join } from 'path'\nimport appIcon f"
},
{
"path": "lib/main/appNap.ts",
"chars": 471,
"preview": "import { powerSaveBlocker } from 'electron'\n\nlet powerSaveBlockerId: number | null = null\n\n// Prevent the system from go"
},
{
"path": "lib/main/audio/AudioStreamManager.test.ts",
"chars": 9741,
"preview": "import { describe, test, expect, beforeEach } from 'bun:test'\nimport { AudioStreamManager } from './AudioStreamManager'\n"
},
{
"path": "lib/main/audio/AudioStreamManager.ts",
"chars": 3406,
"preview": "import { AudioChunkSchema } from '@/app/generated/ito_pb'\nimport { create } from '@bufbuild/protobuf'\nimport { audioReco"
},
{
"path": "lib/main/autoUpdaterWrapper.ts",
"chars": 3855,
"preview": "import { app } from 'electron'\nimport log from 'electron-log'\nimport { autoUpdater } from 'electron-updater'\nimport { ma"
},
{
"path": "lib/main/context/ContextGrabber.ts",
"chars": 6401,
"preview": "import { ItoMode } from '@/app/generated/ito_pb'\nimport { DictionaryTable } from '../sqlite/repo'\nimport { getCurrentUse"
},
{
"path": "lib/main/env.ts",
"chars": 462,
"preview": "import { app } from 'electron'\nimport path from 'path'\n\nlet stage = process.env.ITO_ENV || import.meta.env.VITE_ITO_ENV\n"
},
{
"path": "lib/main/grammar/GrammarRulesService.test.ts",
"chars": 8898,
"preview": "import { describe, test, expect } from 'bun:test'\nimport { GrammarRulesService } from './GrammarRulesService'\n\ndescribe("
},
{
"path": "lib/main/grammar/GrammarRulesService.ts",
"chars": 4346,
"preview": "import nlp from 'compromise'\n\nexport class GrammarRulesService {\n private cursorContext: string = ''\n\n public construc"
},
{
"path": "lib/main/index.d.ts",
"chars": 508,
"preview": "/// <reference types=\"electron-vite/node\" />\n\ndeclare module '*.css' {\n const content: string\n export default content\n"
},
{
"path": "lib/main/interactions/InteractionManager.test.ts",
"chars": 8503,
"preview": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\n\n// Mock database utilities\nconst mockDbRun = mock(("
},
{
"path": "lib/main/interactions/InteractionManager.ts",
"chars": 3555,
"preview": "import { InteractionsTable } from '../sqlite/repo'\nimport mainStore from '../store'\nimport { STORE_KEYS } from '../../co"
},
{
"path": "lib/main/itoSessionManager.test.ts",
"chars": 16267,
"preview": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\nimport { ItoMode } from '@/app/generated/ito_pb'\nimp"
},
{
"path": "lib/main/itoSessionManager.ts",
"chars": 9371,
"preview": "import { ItoMode } from '@/app/generated/ito_pb'\nimport { voiceInputService } from './voiceInputService'\nimport { record"
},
{
"path": "lib/main/itoStreamController.test.ts",
"chars": 8698,
"preview": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\nimport { ItoMode } from '@/app/generated/ito_pb'\n\nco"
},
{
"path": "lib/main/itoStreamController.ts",
"chars": 8148,
"preview": "import {\n ItoMode,\n TranscribeStreamRequest,\n TranscribeStreamRequestSchema,\n StreamConfigSchema,\n ContextInfoSchem"
},
{
"path": "lib/main/logger.ts",
"chars": 6691,
"preview": "import log from 'electron-log'\nimport { app } from 'electron'\nimport os from 'os'\nimport store, { getCurrentUserId } fro"
},
{
"path": "lib/main/main.ts",
"chars": 5841,
"preview": "import './env'\nimport './sentry'\nimport { app, protocol } from 'electron'\nimport { electronApp, optimizer } from '@elect"
},
{
"path": "lib/main/recordingStateNotifier.ts",
"chars": 1687,
"preview": "import { ItoMode } from '@/app/generated/ito_pb'\nimport { getPillWindow, mainWindow } from './app'\nimport {\n IPC_EVENTS"
},
{
"path": "lib/main/sentry.ts",
"chars": 853,
"preview": "import * as Sentry from '@sentry/electron/main'\n\nconst dsn = (import.meta as any).env?.VITE_SENTRY_DSN as string | undef"
},
{
"path": "lib/main/sqlite/db.test.ts",
"chars": 11489,
"preview": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\n\n// Mock electron BEFORE importing db.ts\nmock.module"
},
{
"path": "lib/main/sqlite/db.ts",
"chars": 6753,
"preview": "import sqlite3 from 'sqlite3'\nimport { app } from 'electron'\nimport path from 'path'\nimport { promises as fs } from 'fs'"
},
{
"path": "lib/main/sqlite/migrations.ts",
"chars": 2010,
"preview": "export interface Migration {\n id: string\n up: string\n down: string\n}\n\nexport const MIGRATIONS: Migration[] = [\n {\n "
},
{
"path": "lib/main/sqlite/models.ts",
"chars": 1086,
"preview": "export interface Interaction {\n id: string\n user_id: string | null\n title: string | null\n asr_output: any\n llm_outp"
},
{
"path": "lib/main/sqlite/repo.test.ts",
"chars": 19978,
"preview": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\n\n// Mock the database utilities\nconst mockRun = mock"
},
{
"path": "lib/main/sqlite/repo.ts",
"chars": 19698,
"preview": "import { run, get, all } from './utils'\nimport type { Interaction, Note, DictionaryItem, UserMetadata } from './models'\n"
},
{
"path": "lib/main/sqlite/schema.ts",
"chars": 845,
"preview": "export const INITIAL_SCHEMA = `\n CREATE TABLE interactions (\n id TEXT PRIMARY KEY,\n user_id TEXT,\n title TEXT,"
},
{
"path": "lib/main/sqlite/utils.ts",
"chars": 922,
"preview": "import { getDb } from './db'\n\nexport const run = (query: string, params: any[] = []): Promise<void> =>\n new Promise((re"
},
{
"path": "lib/main/store.test.ts",
"chars": 2871,
"preview": "import { describe, test, expect, beforeEach, mock } from 'bun:test'\n\n// Mock crypto module, preserving Node's built-ins "
},
{
"path": "lib/main/store.ts",
"chars": 12008,
"preview": "import crypto from 'crypto'\nimport { STORE_KEYS } from '../constants/store-keys'\nimport type { LlmSettings } from '@/app"
},
{
"path": "lib/main/syncService.test.ts",
"chars": 22534,
"preview": "import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'\n\n// Mock external boundaries only - let i"
}
]
// ... and 200 more files (download for full content)
About this extraction
This page contains the full source code of the heyito/ito GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 400 files (2.2 MB), approximately 591.6k tokens, and a symbol index with 1208 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.