diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index d9b0c38d9..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,34 +0,0 @@ -module.exports = { - root: true, - env: { - browser: true, - es2021: true, - node: true, - }, - extends: [ - 'eslint:recommended', - 'plugin:react/recommended', - 'plugin:react-hooks/recommended', - 'plugin:@typescript-eslint/recommended', - ], - parser: '@typescript-eslint/parser', - ignorePatterns: ['src/api/generated.ts'], - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - ecmaVersion: 'latest', - sourceType: 'module', - }, - plugins: ['react', '@typescript-eslint'], - rules: { - 'react/react-in-jsx-scope': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'warn', - }, - settings: { - react: { - version: 'detect', - }, - }, -}; \ No newline at end of file diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 000000000..26adfb673 --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,21 @@ +name: Code quality + +on: + pull_request: + +jobs: + quality: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + persist-credentials: false + - name: Setup Biome + uses: biomejs/setup-biome@v2 + with: + version: latest + - name: Run Biome + run: biome ci . \ No newline at end of file diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml new file mode 100644 index 000000000..69de775c2 --- /dev/null +++ b/.github/workflows/typecheck.yml @@ -0,0 +1,26 @@ +name: Typecheck + +on: + pull_request: + +jobs: + typecheck: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + persist-credentials: false + - name: Setup pnpm + uses: pnpm/action-setup@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + cache: 'pnpm' + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Run type check + run: pnpm run typecheck + \ No newline at end of file diff --git a/.node-version b/.node-version new file mode 100644 index 000000000..8fdd954df --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +22 \ No newline at end of file diff --git a/biome.jsonc b/biome.jsonc new file mode 100644 index 000000000..a33a378b0 --- /dev/null +++ b/biome.jsonc @@ -0,0 +1,57 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "nursery": { + "useSortedClasses": "error" + } + }, + "domains": { + "react": "recommended" + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + }, + "overrides": [ + { + // Files using unknownAtRules + "includes": ["src/renderer/styles/globals.css"], + "linter": { + "enabled": false + } + }, + { + // API client generates a a lot of Any + "includes": ["src/api/generated.ts"], + "linter": { + "enabled": false + } + } + ] +} diff --git a/mprocs.yaml b/mprocs.yaml index e0c0c9c79..2c9ade94a 100644 --- a/mprocs.yaml +++ b/mprocs.yaml @@ -9,10 +9,16 @@ procs: cwd: . color: green + preload: + cmd: ["pnpm", "run", "dev:preload"] + cwd: . + color: cyan + electron: cmd: ["pnpm", "run", "dev:electron"] cwd: . color: yellow depends_on: - vite - - node \ No newline at end of file + - node + - preload \ No newline at end of file diff --git a/package.json b/package.json index 9f2f55995..841d03c9e 100644 --- a/package.json +++ b/package.json @@ -12,14 +12,17 @@ "dev": "mprocs", "dev:vite": "vite", "dev:node": "tsc -p tsconfig.node.json --watch", + "dev:preload": "tsc -p tsconfig.preload.json --watch", "dev:electron": "wait-on tcp:5173 && wait-on dist/main/index.js && electron . --dev", "build": "pnpm run build:node && pnpm run build:vite && pnpm run build:electron", - "build:node": "tsc -p tsconfig.node.json", + "build:node": "tsc -p tsconfig.node.json && tsc -p tsconfig.preload.json && cp src/main/package.json dist/main/package.json", "build:vite": "vite build", "build:electron": "electron-builder", "preview": "vite preview", "typecheck": "tsc -p tsconfig.node.json --noEmit && tsc -p tsconfig.web.json --noEmit", - "lint": "eslint src --ext .ts,.tsx", + "lint": "biome check --fix", + "format": "biome format --fix", + "check": "pnpm run lint && pnpm run typecheck", "generate-client": "tsx scripts/update-openapi-client.ts" }, "keywords": [ @@ -31,6 +34,7 @@ "author": "PostHog", "license": "MIT", "devDependencies": { + "@biomejs/biome": "2.2.4", "@types/node": "^20.11.5", "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", @@ -89,4 +93,4 @@ "target": "nsis" } } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc4a8739f..cb43e2324 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,9 @@ importers: specifier: ^4.5.0 version: 4.5.7(@types/react@18.3.24)(react@18.3.1) devDependencies: + '@biomejs/biome': + specifier: 2.2.4 + version: 2.2.4 '@types/node': specifier: ^20.11.5 version: 20.19.17 @@ -240,6 +243,59 @@ packages: resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} engines: {node: '>=6.9.0'} + '@biomejs/biome@2.2.4': + resolution: {integrity: sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.2.4': + resolution: {integrity: sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.2.4': + resolution: {integrity: sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.2.4': + resolution: {integrity: sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@2.2.4': + resolution: {integrity: sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@2.2.4': + resolution: {integrity: sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@2.2.4': + resolution: {integrity: sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@2.2.4': + resolution: {integrity: sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.2.4': + resolution: {integrity: sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@develar/schema-utils@2.6.5': resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} engines: {node: '>= 8.9.0'} @@ -4107,6 +4163,41 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@biomejs/biome@2.2.4': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.2.4 + '@biomejs/cli-darwin-x64': 2.2.4 + '@biomejs/cli-linux-arm64': 2.2.4 + '@biomejs/cli-linux-arm64-musl': 2.2.4 + '@biomejs/cli-linux-x64': 2.2.4 + '@biomejs/cli-linux-x64-musl': 2.2.4 + '@biomejs/cli-win32-arm64': 2.2.4 + '@biomejs/cli-win32-x64': 2.2.4 + + '@biomejs/cli-darwin-arm64@2.2.4': + optional: true + + '@biomejs/cli-darwin-x64@2.2.4': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.2.4': + optional: true + + '@biomejs/cli-linux-arm64@2.2.4': + optional: true + + '@biomejs/cli-linux-x64-musl@2.2.4': + optional: true + + '@biomejs/cli-linux-x64@2.2.4': + optional: true + + '@biomejs/cli-win32-arm64@2.2.4': + optional: true + + '@biomejs/cli-win32-x64@2.2.4': + optional: true + '@develar/schema-utils@2.6.5': dependencies: ajv: 6.12.6 diff --git a/postcss.config.js b/postcss.config.js index 8567b4c40..12a703d90 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -3,4 +3,4 @@ module.exports = { tailwindcss: {}, autoprefixer: {}, }, -}; \ No newline at end of file +}; diff --git a/scripts/update-openapi-client.ts b/scripts/update-openapi-client.ts index 139aeccf3..61d9f1660 100644 --- a/scripts/update-openapi-client.ts +++ b/scripts/update-openapi-client.ts @@ -9,96 +9,98 @@ const TEMP_SCHEMA_PATH = "temp-openapi.yaml"; const OUTPUT_PATH = "src/api/generated.ts"; const INCLUDED_ENDPOINT_PREFIXES = [ - "/api/projects/{project_id}/tasks", - "/api/projects/{project_id}/workflows", - "/api/users/", + "/api/projects/{project_id}/tasks", + "/api/projects/{project_id}/workflows", + "/api/users/", ]; async function fetchSchema() { - console.log("Fetching OpenAPI schema from PostHog API..."); + console.log("Fetching OpenAPI schema from PostHog API..."); - try { - const response = await fetch(SCHEMA_URL); - if (!response.ok) { - throw new Error(`Failed to fetch schema: ${response.status} ${response.statusText}`); - } + try { + const response = await fetch(SCHEMA_URL); + if (!response.ok) { + throw new Error( + `Failed to fetch schema: ${response.status} ${response.statusText}`, + ); + } - const schemaText = await response.text(); - const schema = yaml.parse(schemaText); + const schemaText = await response.text(); + const schema = yaml.parse(schemaText); - filterEndpoints(schema); + filterEndpoints(schema); - fs.writeFileSync(TEMP_SCHEMA_PATH, yaml.stringify(schema), "utf-8"); - console.log(`✓ Schema saved to ${TEMP_SCHEMA_PATH}`); + fs.writeFileSync(TEMP_SCHEMA_PATH, yaml.stringify(schema), "utf-8"); + console.log(`✓ Schema saved to ${TEMP_SCHEMA_PATH}`); - return true; - } catch (error) { - console.error("Error fetching schema:", error); - return false; - } + return true; + } catch (error) { + console.error("Error fetching schema:", error); + return false; + } } -function filterEndpoints(schema: any) { - if (!schema.paths) return; +function filterEndpoints(schema: { paths?: Record }) { + if (!schema.paths) return; - const filteredPaths: Record = {}; + const filteredPaths: Record = {}; - for (const [path, pathItem] of Object.entries(schema.paths)) { - if (INCLUDED_ENDPOINT_PREFIXES.some((prefix) => path.startsWith(prefix))) { - filteredPaths[path] = pathItem; - } - } + for (const [path, pathItem] of Object.entries(schema.paths)) { + if (INCLUDED_ENDPOINT_PREFIXES.some((prefix) => path.startsWith(prefix))) { + filteredPaths[path] = pathItem; + } + } - schema.paths = filteredPaths; - console.log(`✓ Filtered to ${Object.keys(filteredPaths).length} endpoints`); + schema.paths = filteredPaths; + console.log(`✓ Filtered to ${Object.keys(filteredPaths).length} endpoints`); } function generateClient() { - console.log("Generating TypeScript client..."); - - try { - execSync(`pnpm typed-openapi ${TEMP_SCHEMA_PATH} --output ${OUTPUT_PATH}`, { - stdio: "inherit", - }); - console.log(`✓ Client generated at ${OUTPUT_PATH}`); - return true; - } catch (error) { - console.error("Error generating client:", error); - return false; - } + console.log("Generating TypeScript client..."); + + try { + execSync(`pnpm typed-openapi ${TEMP_SCHEMA_PATH} --output ${OUTPUT_PATH}`, { + stdio: "inherit", + }); + console.log(`✓ Client generated at ${OUTPUT_PATH}`); + return true; + } catch (error) { + console.error("Error generating client:", error); + return false; + } } function cleanup() { - try { - if (fs.existsSync(TEMP_SCHEMA_PATH)) { - fs.unlinkSync(TEMP_SCHEMA_PATH); - console.log("✓ Cleaned up temporary schema file"); - } - } catch (error) { - console.error("Warning: Could not clean up temporary file:", error); - } + try { + if (fs.existsSync(TEMP_SCHEMA_PATH)) { + fs.unlinkSync(TEMP_SCHEMA_PATH); + console.log("✓ Cleaned up temporary schema file"); + } + } catch (error) { + console.error("Warning: Could not clean up temporary file:", error); + } } async function main() { - console.log("Starting OpenAPI client update...\n"); + console.log("Starting OpenAPI client update...\n"); - const schemaFetched = await fetchSchema(); - if (!schemaFetched) { - process.exit(1); - } + const schemaFetched = await fetchSchema(); + if (!schemaFetched) { + process.exit(1); + } - const clientGenerated = generateClient(); + const clientGenerated = generateClient(); - cleanup(); + cleanup(); - if (!clientGenerated) { - process.exit(1); - } + if (!clientGenerated) { + process.exit(1); + } - console.log("\n✅ OpenAPI client successfully updated!"); + console.log("\n✅ OpenAPI client successfully updated!"); } main().catch((error) => { - console.error("Unexpected error:", error); - process.exit(1); + console.error("Unexpected error:", error); + process.exit(1); }); diff --git a/src/api/fetcher.ts b/src/api/fetcher.ts index ab939aff1..854eadd56 100644 --- a/src/api/fetcher.ts +++ b/src/api/fetcher.ts @@ -1,48 +1,50 @@ import type { createApiClient } from "./generated"; -export const buildApiFetcher: (config: { apiToken: string }) => Parameters[0] = ( - config, -) => { - return { - fetch: async (input) => { - const headers = new Headers(); - headers.set("Authorization", `Bearer ${config.apiToken}`); - - if (input.urlSearchParams) { - input.url.search = input.urlSearchParams.toString(); - } - - const body = ["post", "put", "patch", "delete"].includes(input.method.toLowerCase()) - ? JSON.stringify(input.parameters?.body) - : undefined; - - if (body) { - headers.set("Content-Type", "application/json"); - } - - if (input.parameters?.header) { - for (const [key, value] of Object.entries(input.parameters.header)) { - if (value != null) { - headers.set(key, String(value)); - } - } - } - - const response = await fetch(input.url, { - method: input.method.toUpperCase(), - ...(body && { body }), - headers, - ...input.overrides, - }); - - if (!response.ok) { - const errorResponse = await response.json(); - throw new Error( - `Failed request: [${response.status}] ${JSON.stringify(errorResponse)}`, - ); - } - - return response; - }, - }; +export const buildApiFetcher: (config: { + apiToken: string; +}) => Parameters[0] = (config) => { + return { + fetch: async (input) => { + const headers = new Headers(); + headers.set("Authorization", `Bearer ${config.apiToken}`); + + if (input.urlSearchParams) { + input.url.search = input.urlSearchParams.toString(); + } + + const body = ["post", "put", "patch", "delete"].includes( + input.method.toLowerCase(), + ) + ? JSON.stringify(input.parameters?.body) + : undefined; + + if (body) { + headers.set("Content-Type", "application/json"); + } + + if (input.parameters?.header) { + for (const [key, value] of Object.entries(input.parameters.header)) { + if (value != null) { + headers.set(key, String(value)); + } + } + } + + const response = await fetch(input.url, { + method: input.method.toUpperCase(), + ...(body && { body }), + headers, + ...input.overrides, + }); + + if (!response.ok) { + const errorResponse = await response.json(); + throw new Error( + `Failed request: [${response.status}] ${JSON.stringify(errorResponse)}`, + ); + } + + return response; + }, + }; }; diff --git a/src/api/generated.ts b/src/api/generated.ts index be6fd40ca..6814aafa9 100644 --- a/src/api/generated.ts +++ b/src/api/generated.ts @@ -40,7 +40,9 @@ export namespace Schemas { email: string; is_email_verified?: (boolean | null) | undefined; hedgehog_config: Record | null; - role_at_organization?: ((RoleAtOrganizationEnum | BlankEnum | NullEnum) | null) | undefined; + role_at_organization?: + | ((RoleAtOrganizationEnum | BlankEnum | NullEnum) | null) + | undefined; }; export type Action = { id: number; @@ -92,14 +94,18 @@ export namespace Schemas { label?: (string | null) | undefined; operator?: (PropertyOperator | null) | undefined; type?: string | undefined; - value?: (Array | string | number | boolean | null) | undefined; + value?: + | (Array | string | number | boolean | null) + | undefined; }; export type PersonPropertyFilter = { key: string; label?: (string | null) | undefined; operator: PropertyOperator; type?: string | undefined; - value?: (Array | string | number | boolean | null) | undefined; + value?: + | (Array | string | number | boolean | null) + | undefined; }; export type Key = "tag_name" | "text" | "href" | "selector"; export type ElementPropertyFilter = { @@ -107,21 +113,27 @@ export namespace Schemas { label?: (string | null) | undefined; operator: PropertyOperator; type?: string | undefined; - value?: (Array | string | number | boolean | null) | undefined; + value?: + | (Array | string | number | boolean | null) + | undefined; }; export type EventMetadataPropertyFilter = { key: string; label?: (string | null) | undefined; operator: PropertyOperator; type?: string | undefined; - value?: (Array | string | number | boolean | null) | undefined; + value?: + | (Array | string | number | boolean | null) + | undefined; }; export type SessionPropertyFilter = { key: string; label?: (string | null) | undefined; operator: PropertyOperator; type?: string | undefined; - value?: (Array | string | number | boolean | null) | undefined; + value?: + | (Array | string | number | boolean | null) + | undefined; }; export type CohortPropertyFilter = { cohort_name?: (string | null) | undefined; @@ -137,14 +149,18 @@ export namespace Schemas { label?: (string | null) | undefined; operator: PropertyOperator; type?: string | undefined; - value?: (Array | string | number | boolean | null) | undefined; + value?: + | (Array | string | number | boolean | null) + | undefined; }; export type LogEntryPropertyFilter = { key: string; label?: (string | null) | undefined; operator: PropertyOperator; type?: string | undefined; - value?: (Array | string | number | boolean | null) | undefined; + value?: + | (Array | string | number | boolean | null) + | undefined; }; export type GroupPropertyFilter = { group_type_index?: (number | null) | undefined; @@ -152,14 +168,18 @@ export namespace Schemas { label?: (string | null) | undefined; operator: PropertyOperator; type?: string | undefined; - value?: (Array | string | number | boolean | null) | undefined; + value?: + | (Array | string | number | boolean | null) + | undefined; }; export type FeaturePropertyFilter = { key: string; label?: (string | null) | undefined; operator: PropertyOperator; type?: string | undefined; - value?: (Array | string | number | boolean | null) | undefined; + value?: + | (Array | string | number | boolean | null) + | undefined; }; export type FlagPropertyFilter = { key: string; @@ -172,7 +192,9 @@ export namespace Schemas { key: string; label?: (string | null) | undefined; type?: string | undefined; - value?: (Array | string | number | boolean | null) | undefined; + value?: + | (Array | string | number | boolean | null) + | undefined; }; export type EmptyPropertyFilter = Partial<{}>; export type DataWarehousePropertyFilter = { @@ -180,35 +202,45 @@ export namespace Schemas { label?: (string | null) | undefined; operator: PropertyOperator; type?: string | undefined; - value?: (Array | string | number | boolean | null) | undefined; + value?: + | (Array | string | number | boolean | null) + | undefined; }; export type DataWarehousePersonPropertyFilter = { key: string; label?: (string | null) | undefined; operator: PropertyOperator; type?: string | undefined; - value?: (Array | string | number | boolean | null) | undefined; + value?: + | (Array | string | number | boolean | null) + | undefined; }; export type ErrorTrackingIssueFilter = { key: string; label?: (string | null) | undefined; operator: PropertyOperator; type?: string | undefined; - value?: (Array | string | number | boolean | null) | undefined; + value?: + | (Array | string | number | boolean | null) + | undefined; }; export type LogPropertyFilter = { key: string; label?: (string | null) | undefined; operator: PropertyOperator; type?: string | undefined; - value?: (Array | string | number | boolean | null) | undefined; + value?: + | (Array | string | number | boolean | null) + | undefined; }; export type RevenueAnalyticsPropertyFilter = { key: string; label?: (string | null) | undefined; operator: PropertyOperator; type?: string | undefined; - value?: (Array | string | number | boolean | null) | undefined; + value?: + | (Array | string | number | boolean | null) + | undefined; }; export type BaseMathType = | "total" @@ -218,8 +250,20 @@ export namespace Schemas { | "unique_session" | "first_time_for_user" | "first_matching_event_for_user"; - export type FunnelMathType = "total" | "first_time_for_user" | "first_time_for_user_with_filters"; - export type PropertyMathType = "avg" | "sum" | "min" | "max" | "median" | "p75" | "p90" | "p95" | "p99"; + export type FunnelMathType = + | "total" + | "first_time_for_user" + | "first_time_for_user_with_filters"; + export type PropertyMathType = + | "avg" + | "sum" + | "min" + | "max" + | "median" + | "p75" + | "p90" + | "p95" + | "p99"; export type CountPerActorMathType = | "avg_count_per_actor" | "min_count_per_actor" @@ -394,7 +438,10 @@ export namespace Schemas { | "YER" | "ZAR" | "ZMW"; - export type RevenueCurrencyPropertyConfig = Partial<{ property: string | null; static: CurrencyCode | null }>; + export type RevenueCurrencyPropertyConfig = Partial<{ + property: string | null; + static: CurrencyCode | null; + }>; export type ActionsNode = { custom_name?: (string | null) | undefined; fixedProperties?: @@ -438,7 +485,9 @@ export namespace Schemas { math_hogql?: (string | null) | undefined; math_multiplier?: (number | null) | undefined; math_property?: (string | null) | undefined; - math_property_revenue_currency?: (RevenueCurrencyPropertyConfig | null) | undefined; + math_property_revenue_currency?: + | (RevenueCurrencyPropertyConfig | null) + | undefined; math_property_type?: (string | null) | undefined; name?: (string | null) | undefined; optionalInFunnel?: (boolean | null) | undefined; @@ -467,7 +516,10 @@ export namespace Schemas { response?: (Record | null) | undefined; version?: (number | null) | undefined; }; - export type ActionsPie = Partial<{ disableHoverOffset: boolean | null; hideAggregation: boolean | null }>; + export type ActionsPie = Partial<{ + disableHoverOffset: boolean | null; + hideAggregation: boolean | null; + }>; export type ActivityLog = { id: string; user: UserBasic; @@ -481,7 +533,10 @@ export namespace Schemas { detail?: (unknown | null) | undefined; created_at?: string | undefined; }; - export type BounceRatePageViewMode = "count_pageviews" | "uniq_urls" | "uniq_page_screen_autocaptures"; + export type BounceRatePageViewMode = + | "count_pageviews" + | "uniq_urls" + | "uniq_page_screen_autocaptures"; export type FilterLogicalOperator = "AND" | "OR"; export type CustomChannelField = | "utm_source" @@ -518,8 +573,16 @@ export namespace Schemas { table_name: string; timestamp_field: string; }; - export type InCohortVia = "auto" | "leftjoin" | "subquery" | "leftjoin_conjoined"; - export type MaterializationMode = "auto" | "legacy_null_as_string" | "legacy_null_as_null" | "disabled"; + export type InCohortVia = + | "auto" + | "leftjoin" + | "subquery" + | "leftjoin_conjoined"; + export type MaterializationMode = + | "auto" + | "legacy_null_as_string" + | "legacy_null_as_null" + | "disabled"; export type PersonsArgMaxVersion = "auto" | "v1" | "v2"; export type PersonsJoinMode = "inner" | "left"; export type PersonsOnEventsMode = @@ -579,7 +642,10 @@ export namespace Schemas { task_id?: (string | null) | undefined; team_id: number; }; - export type ResolvedDateRangeResponse = { date_from: string; date_to: string }; + export type ResolvedDateRangeResponse = { + date_from: string; + date_to: string; + }; export type ActorsPropertyTaxonomyResponse = { sample_count: number; sample_values: Array; @@ -591,10 +657,15 @@ export namespace Schemas { modifiers?: (HogQLQueryModifiers | null) | undefined; query_status?: (QueryStatus | null) | undefined; resolved_date_range?: (ResolvedDateRangeResponse | null) | undefined; - results: ActorsPropertyTaxonomyResponse | Array; + results: + | ActorsPropertyTaxonomyResponse + | Array; timings?: (Array | null) | undefined; }; - export type QueryLogTags = Partial<{ productKey: string | null; scene: string | null }>; + export type QueryLogTags = Partial<{ + productKey: string | null; + scene: string | null; + }>; export type ActorsPropertyTaxonomyQuery = { groupTypeIndex?: (number | null) | undefined; kind?: string | undefined; @@ -682,11 +753,21 @@ export namespace Schemas { breakdown_type: BreakdownType | null; breakdowns: Array | null; }>; - export type CompareFilter = Partial<{ compare: boolean | null; compare_to: string | null }>; + export type CompareFilter = Partial<{ + compare: boolean | null; + compare_to: string | null; + }>; export type CustomEventConversionGoal = { customEventName: string }; - export type DateRange = Partial<{ date_from: string | null; date_to: string | null; explicitDate: boolean | null }>; + export type DateRange = Partial<{ + date_from: string | null; + date_to: string | null; + explicitDate: boolean | null; + }>; export type IntervalType = "minute" | "hour" | "day" | "week" | "month"; - export type PropertyGroupFilter = { type: FilterLogicalOperator; values: Array }; + export type PropertyGroupFilter = { + type: FilterLogicalOperator; + values: Array; + }; export type TrendsQueryResponse = { error?: (string | null) | undefined; hasMore?: (boolean | null) | undefined; @@ -810,7 +891,9 @@ export namespace Schemas { math_hogql?: (string | null) | undefined; math_multiplier?: (number | null) | undefined; math_property?: (string | null) | undefined; - math_property_revenue_currency?: (RevenueCurrencyPropertyConfig | null) | undefined; + math_property_revenue_currency?: + | (RevenueCurrencyPropertyConfig | null) + | undefined; math_property_type?: (string | null) | undefined; name?: (string | null) | undefined; optionalInFunnel?: (boolean | null) | undefined; @@ -862,7 +945,10 @@ export namespace Schemas { | "ActionsTable" | "WorldMap" | "CalendarHeatmap"; - export type TrendsFormulaNode = { custom_name?: (string | null) | undefined; formula: string }; + export type TrendsFormulaNode = { + custom_name?: (string | null) | undefined; + formula: string; + }; export type GoalLine = { borderColor?: (string | null) | undefined; displayIfCrossed?: (boolean | null) | undefined; @@ -915,7 +1001,10 @@ export namespace Schemas { minDecimalPlaces: number | null; movingAverageIntervals: number | null; resultCustomizationBy: ResultCustomizationBy | null; - resultCustomizations: Record | Record | null; + resultCustomizations: + | Record + | Record + | null; showAlertThresholdLines: boolean | null; showConfidenceIntervals: boolean | null; showLabelsOnSeries: boolean | null; @@ -932,7 +1021,9 @@ export namespace Schemas { aggregation_group_type_index?: (number | null) | undefined; breakdownFilter?: (BreakdownFilter | null) | undefined; compareFilter?: (CompareFilter | null) | undefined; - conversionGoal?: (ActionConversionGoal | CustomEventConversionGoal | null) | undefined; + conversionGoal?: + | (ActionConversionGoal | CustomEventConversionGoal | null) + | undefined; dataColorTheme?: (number | null) | undefined; dateRange?: (DateRange | null) | undefined; filterTestAccounts?: (boolean | null) | undefined; @@ -972,7 +1063,11 @@ export namespace Schemas { trendsFilter?: (TrendsFilter | null) | undefined; version?: (number | null) | undefined; }; - export type BreakdownAttributionType = "first_touch" | "last_touch" | "all_events" | "step"; + export type BreakdownAttributionType = + | "first_touch" + | "last_touch" + | "all_events" + | "step"; export type FunnelExclusionEventsNode = { custom_name?: (string | null) | undefined; event?: (string | null) | undefined; @@ -1019,7 +1114,9 @@ export namespace Schemas { math_hogql?: (string | null) | undefined; math_multiplier?: (number | null) | undefined; math_property?: (string | null) | undefined; - math_property_revenue_currency?: (RevenueCurrencyPropertyConfig | null) | undefined; + math_property_revenue_currency?: + | (RevenueCurrencyPropertyConfig | null) + | undefined; math_property_type?: (string | null) | undefined; name?: (string | null) | undefined; optionalInFunnel?: (boolean | null) | undefined; @@ -1094,7 +1191,9 @@ export namespace Schemas { math_hogql?: (string | null) | undefined; math_multiplier?: (number | null) | undefined; math_property?: (string | null) | undefined; - math_property_revenue_currency?: (RevenueCurrencyPropertyConfig | null) | undefined; + math_property_revenue_currency?: + | (RevenueCurrencyPropertyConfig | null) + | undefined; math_property_type?: (string | null) | undefined; name?: (string | null) | undefined; optionalInFunnel?: (boolean | null) | undefined; @@ -1126,13 +1225,21 @@ export namespace Schemas { export type StepOrderValue = "strict" | "unordered" | "ordered"; export type FunnelStepReference = "total" | "previous"; export type FunnelVizType = "steps" | "time_to_convert" | "trends"; - export type FunnelConversionWindowTimeUnit = "second" | "minute" | "hour" | "day" | "week" | "month"; + export type FunnelConversionWindowTimeUnit = + | "second" + | "minute" + | "hour" + | "day" + | "week" + | "month"; export type FunnelLayout = "horizontal" | "vertical"; export type FunnelsFilter = Partial<{ binCount: number | null; breakdownAttributionType: BreakdownAttributionType | null; breakdownAttributionValue: number | null; - exclusions: Array | null; + exclusions: Array< + FunnelExclusionEventsNode | FunnelExclusionActionsNode + > | null; funnelAggregateByHogQL: string | null; funnelFromStep: number | null; funnelOrderType: StepOrderValue | null; @@ -1200,7 +1307,10 @@ export namespace Schemas { tags?: (QueryLogTags | null) | undefined; version?: (number | null) | undefined; }; - export type RetentionValue = { count: number; label?: (string | null) | undefined }; + export type RetentionValue = { + count: number; + label?: (string | null) | undefined; + }; export type RetentionResult = { breakdown_value?: (string | number | null) | undefined; date: string; @@ -1216,13 +1326,23 @@ export namespace Schemas { results: Array; timings?: (Array | null) | undefined; }; - export type RetentionDashboardDisplayType = "table_only" | "graph_only" | "all"; + export type RetentionDashboardDisplayType = + | "table_only" + | "graph_only" + | "all"; export type MeanRetentionCalculation = "simple" | "weighted" | "none"; export type RetentionPeriod = "Hour" | "Day" | "Week" | "Month"; export type RetentionReference = "total" | "previous"; - export type RetentionType = "retention_recurring" | "retention_first_time" | "retention_first_ever_occurrence"; + export type RetentionType = + | "retention_recurring" + | "retention_first_time" + | "retention_first_ever_occurrence"; export type RetentionEntityKind = "ActionsNode" | "EventsNode"; - export type EntityType = "actions" | "events" | "data_warehouse" | "new_entity"; + export type EntityType = + | "actions" + | "events" + | "data_warehouse" + | "new_entity"; export type RetentionEntity = Partial<{ custom_name: string | null; id: string | number | null; @@ -1306,14 +1426,21 @@ export namespace Schemas { tags?: (QueryLogTags | null) | undefined; version?: (number | null) | undefined; }; - export type FunnelPathType = "funnel_path_before_step" | "funnel_path_between_steps" | "funnel_path_after_step"; + export type FunnelPathType = + | "funnel_path_before_step" + | "funnel_path_between_steps" + | "funnel_path_after_step"; export type FunnelPathsFilter = { funnelPathType?: (FunnelPathType | null) | undefined; funnelSource: FunnelsQuery; funnelStep?: (number | null) | undefined; }; export type PathType = "$pageview" | "$screen" | "custom_event" | "hogql"; - export type PathCleaningFilter = Partial<{ alias: string | null; order: number | null; regex: string | null }>; + export type PathCleaningFilter = Partial<{ + alias: string | null; + order: number | null; + regex: string | null; + }>; export type PathsFilter = Partial<{ edgeLimit: number | null; endPoint: string | null; @@ -1331,7 +1458,12 @@ export namespace Schemas { startPoint: string | null; stepLimit: number | null; }>; - export type PathsLink = { average_conversion_time: number; source: string; target: string; value: number }; + export type PathsLink = { + average_conversion_time: number; + source: string; + target: string; + value: number; + }; export type PathsQueryResponse = { error?: (string | null) | undefined; hogql?: (string | null) | undefined; @@ -1392,13 +1524,19 @@ export namespace Schemas { }; export type StickinessComputationMode = "non_cumulative" | "cumulative"; export type StickinessOperator = "gte" | "lte" | "exact"; - export type StickinessCriteria = { operator: StickinessOperator; value: number }; + export type StickinessCriteria = { + operator: StickinessOperator; + value: number; + }; export type StickinessFilter = Partial<{ computedAs: StickinessComputationMode | null; display: ChartDisplayType | null; hiddenLegendIndexes: Array | null; resultCustomizationBy: ResultCustomizationBy | null; - resultCustomizations: Record | Record | null; + resultCustomizations: + | Record + | Record + | null; showLegend: boolean | null; showMultipleYAxes: boolean | null; showValuesOnSeries: boolean | null; @@ -1446,7 +1584,11 @@ export namespace Schemas { tags?: (QueryLogTags | null) | undefined; version?: (number | null) | undefined; }; - export type LifecycleToggle = "new" | "resurrecting" | "returning" | "dormant"; + export type LifecycleToggle = + | "new" + | "resurrecting" + | "returning" + | "dormant"; export type LifecycleFilter = Partial<{ showLegend: boolean | null; showValuesOnSeries: boolean | null; @@ -1513,7 +1655,13 @@ export namespace Schemas { modifiers?: (HogQLQueryModifiers | null) | undefined; response?: (ActorsQueryResponse | null) | undefined; series?: (number | null) | undefined; - source: TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery; + source: + | TrendsQuery + | FunnelsQuery + | RetentionQuery + | PathsQuery + | StickinessQuery + | LifecycleQuery; status?: (string | null) | undefined; tags?: (QueryLogTags | null) | undefined; version?: (number | null) | undefined; @@ -1521,7 +1669,9 @@ export namespace Schemas { export type FunnelsActorsQuery = { funnelCustomSteps?: (Array | null) | undefined; funnelStep?: (number | null) | undefined; - funnelStepBreakdown?: (number | string | number | Array | null) | undefined; + funnelStepBreakdown?: + | (number | string | number | Array | null) + | undefined; funnelTrendsDropOff?: (boolean | null) | undefined; funnelTrendsEntrancePeriodStart?: (string | null) | undefined; includeRecordings?: (boolean | null) | undefined; @@ -1532,9 +1682,16 @@ export namespace Schemas { tags?: (QueryLogTags | null) | undefined; version?: (number | null) | undefined; }; - export type FunnelCorrelationResultsType = "events" | "properties" | "event_with_properties"; + export type FunnelCorrelationResultsType = + | "events" + | "properties" + | "event_with_properties"; export type CorrelationType = "success" | "failure"; - export type EventDefinition = { elements: Array; event: string; properties: Record }; + export type EventDefinition = { + elements: Array; + event: string; + properties: Record; + }; export type EventOddsRatioSerialized = { correlation_type: CorrelationType; event: EventDefinition; @@ -1542,7 +1699,10 @@ export namespace Schemas { odds_ratio: number; success_count: number; }; - export type FunnelCorrelationResult = { events: Array; skewed: boolean }; + export type FunnelCorrelationResult = { + events: Array; + skewed: boolean; + }; export type FunnelCorrelationResponse = { columns?: (Array | null) | undefined; error?: (string | null) | undefined; @@ -1558,7 +1718,9 @@ export namespace Schemas { types?: (Array | null) | undefined; }; export type FunnelCorrelationQuery = { - funnelCorrelationEventExcludePropertyNames?: (Array | null) | undefined; + funnelCorrelationEventExcludePropertyNames?: + | (Array | null) + | undefined; funnelCorrelationEventNames?: (Array | null) | undefined; funnelCorrelationExcludeEventNames?: (Array | null) | undefined; funnelCorrelationExcludeNames?: (Array | null) | undefined; @@ -1571,7 +1733,9 @@ export namespace Schemas { }; export type FunnelCorrelationActorsQuery = { funnelCorrelationPersonConverted?: (boolean | null) | undefined; - funnelCorrelationPersonEntity?: (EventsNode | ActionsNode | DataWarehouseNode | null) | undefined; + funnelCorrelationPersonEntity?: + | (EventsNode | ActionsNode | DataWarehouseNode | null) + | undefined; funnelCorrelationPropertyValues?: | (Array< | EventPropertyFilter @@ -1694,7 +1858,10 @@ export namespace Schemas { }; export type ActorsQuery = Partial<{ fixedProperties: Array< - PersonPropertyFilter | CohortPropertyFilter | HogQLPropertyFilter | EmptyPropertyFilter + | PersonPropertyFilter + | CohortPropertyFilter + | HogQLPropertyFilter + | EmptyPropertyFilter > | null; kind: string; limit: number | null; @@ -1702,7 +1869,12 @@ export namespace Schemas { offset: number | null; orderBy: Array | null; properties: - | Array + | Array< + | PersonPropertyFilter + | CohortPropertyFilter + | HogQLPropertyFilter + | EmptyPropertyFilter + > | PropertyGroupFilterValue | null; response: ActorsQueryResponse | null; @@ -1728,7 +1900,12 @@ export namespace Schemas { }; export type AgentListResponse = { results: Array }; export type CreationTypeEnum = "USR" | "GIT"; - export type AnnotationScopeEnum = "dashboard_item" | "dashboard" | "project" | "organization" | "recording"; + export type AnnotationScopeEnum = + | "dashboard_item" + | "dashboard" + | "project" + | "organization" + | "recording"; export type Annotation = { id: number; content?: (string | null) | undefined; @@ -2020,11 +2197,28 @@ export namespace Schemas { batch_export: string; }; export type BreakdownItem = { label: string; value: string | number }; - export type ByweekdayEnum = "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday"; + export type ByweekdayEnum = + | "monday" + | "tuesday" + | "wednesday" + | "thursday" + | "friday" + | "saturday" + | "sunday"; export type CalendarHeatmapFilter = Partial<{ dummy: string | null }>; - export type EventsHeatMapColumnAggregationResult = { column: number; value: number }; - export type EventsHeatMapDataResult = { column: number; row: number; value: number }; - export type EventsHeatMapRowAggregationResult = { row: number; value: number }; + export type EventsHeatMapColumnAggregationResult = { + column: number; + value: number; + }; + export type EventsHeatMapDataResult = { + column: number; + row: number; + value: number; + }; + export type EventsHeatMapRowAggregationResult = { + row: number; + value: number; + }; export type EventsHeatMapStructuredResult = { allAggregations: number; columnAggregations: Array; @@ -2044,7 +2238,9 @@ export namespace Schemas { export type CalendarHeatmapQuery = { aggregation_group_type_index?: (number | null) | undefined; calendarHeatmapFilter?: (CalendarHeatmapFilter | null) | undefined; - conversionGoal?: (ActionConversionGoal | CustomEventConversionGoal | null) | undefined; + conversionGoal?: + | (ActionConversionGoal | CustomEventConversionGoal | null) + | undefined; dataColorTheme?: (number | null) | undefined; dateRange?: (DateRange | null) | undefined; filterTestAccounts?: (boolean | null) | undefined; @@ -2099,8 +2295,14 @@ export namespace Schemas { style: Style | null; suffix: string | null; }>; - export type Settings = Partial<{ display: ChartSettingsDisplay | null; formatting: ChartSettingsFormatting | null }>; - export type ChartAxis = { column: string; settings?: (Settings | null) | undefined }; + export type Settings = Partial<{ + display: ChartSettingsDisplay | null; + formatting: ChartSettingsFormatting | null; + }>; + export type ChartAxis = { + column: string; + settings?: (Settings | null) | undefined; + }; export type Scale = "linear" | "logarithmic"; export type YAxisSettings = Partial<{ scale: Scale | null; @@ -2133,7 +2335,11 @@ export namespace Schemas { elements: string; elements_chain: string; }; - export type CohortTypeEnum = "static" | "person_property" | "behavioral" | "analytical"; + export type CohortTypeEnum = + | "static" + | "person_property" + | "behavioral" + | "analytical"; export type Cohort = { id: number; name?: (string | null) | undefined; @@ -2156,7 +2362,12 @@ export namespace Schemas { }; export type ColorMode = "light" | "dark"; export type CompareItem = { label: string; value: string }; - export type ConclusionEnum = "won" | "lost" | "inconclusive" | "stopped_early" | "invalid"; + export type ConclusionEnum = + | "won" + | "lost" + | "inconclusive" + | "stopped_early" + | "invalid"; export type ConditionalFormattingRule = { bytecode: Array; color: string; @@ -2212,7 +2423,9 @@ export namespace Schemas { math_hogql?: (string | null) | undefined; math_multiplier?: (number | null) | undefined; math_property?: (string | null) | undefined; - math_property_revenue_currency?: (RevenueCurrencyPropertyConfig | null) | undefined; + math_property_revenue_currency?: + | (RevenueCurrencyPropertyConfig | null) + | undefined; math_property_type?: (string | null) | undefined; name?: (string | null) | undefined; optionalInFunnel?: (boolean | null) | undefined; @@ -2288,7 +2501,9 @@ export namespace Schemas { math_hogql?: (string | null) | undefined; math_multiplier?: (number | null) | undefined; math_property?: (string | null) | undefined; - math_property_revenue_currency?: (RevenueCurrencyPropertyConfig | null) | undefined; + math_property_revenue_currency?: + | (RevenueCurrencyPropertyConfig | null) + | undefined; math_property_type?: (string | null) | undefined; name?: (string | null) | undefined; optionalInFunnel?: (boolean | null) | undefined; @@ -2366,7 +2581,9 @@ export namespace Schemas { math_hogql?: (string | null) | undefined; math_multiplier?: (number | null) | undefined; math_property?: (string | null) | undefined; - math_property_revenue_currency?: (RevenueCurrencyPropertyConfig | null) | undefined; + math_property_revenue_currency?: + | (RevenueCurrencyPropertyConfig | null) + | undefined; math_property_type?: (string | null) | undefined; name?: (string | null) | undefined; optionalInFunnel?: (boolean | null) | undefined; @@ -2520,10 +2737,16 @@ export namespace Schemas { created_by?: (number | null) | undefined; image_url?: (string | null) | undefined; team_id: number | null; - scope?: ((DashboardTemplateScopeEnum | BlankEnum | NullEnum) | null) | undefined; + scope?: + | ((DashboardTemplateScopeEnum | BlankEnum | NullEnum) | null) + | undefined; availability_contexts?: (Array | null) | undefined; }; - export type DashboardTileBasic = { id: number; dashboard_id: number; deleted?: (boolean | null) | undefined }; + export type DashboardTileBasic = { + id: number; + dashboard_id: number; + deleted?: (boolean | null) | undefined; + }; export type DataColorTheme = { id: number; name: string; @@ -2532,7 +2755,9 @@ export namespace Schemas { created_at: string | null; created_by: UserBasic & unknown; }; - export type DataTableNodeViewPropsContextType = "event_definition" | "team_columns"; + export type DataTableNodeViewPropsContextType = + | "event_definition" + | "team_columns"; export type DataTableNodeViewPropsContext = { eventDefinitionId?: (string | null) | undefined; type: DataTableNodeViewPropsContextType; @@ -2599,7 +2824,11 @@ export namespace Schemas { timings?: (Array | null) | undefined; types?: (Array | null) | undefined; }; - export type WebAnalyticsItemKind = "unit" | "duration_s" | "percentage" | "currency"; + export type WebAnalyticsItemKind = + | "unit" + | "duration_s" + | "percentage" + | "currency"; export type WebOverviewItem = { changeFromPreviousPct?: (number | null) | undefined; isIncreaseBad?: (boolean | null) | undefined; @@ -2609,7 +2838,10 @@ export namespace Schemas { usedPreAggregatedTables?: (boolean | null) | undefined; value?: (number | null) | undefined; }; - export type SamplingRate = { denominator?: (number | null) | undefined; numerator: number }; + export type SamplingRate = { + denominator?: (number | null) | undefined; + numerator: number; + }; export type Response4 = { dateFrom?: (string | null) | undefined; dateTo?: (string | null) | undefined; @@ -2654,7 +2886,10 @@ export namespace Schemas { timings?: (Array | null) | undefined; types?: (Array | null) | undefined; }; - export type WebVitalsPathBreakdownResultItem = { path: string; value: number }; + export type WebVitalsPathBreakdownResultItem = { + path: string; + value: number; + }; export type WebVitalsPathBreakdownResult = { good: Array; needs_improvements: Array; @@ -2720,8 +2955,14 @@ export namespace Schemas { results: Array; timings?: (Array | null) | undefined; }; - export type RevenueAnalyticsOverviewItemKey = "revenue" | "paying_customer_count" | "avg_revenue_per_customer"; - export type RevenueAnalyticsOverviewItem = { key: RevenueAnalyticsOverviewItemKey; value: number }; + export type RevenueAnalyticsOverviewItemKey = + | "revenue" + | "paying_customer_count" + | "avg_revenue_per_customer"; + export type RevenueAnalyticsOverviewItem = { + key: RevenueAnalyticsOverviewItemKey; + value: number; + }; export type Response13 = { error?: (string | null) | undefined; hogql?: (string | null) | undefined; @@ -2788,7 +3029,10 @@ export namespace Schemas { volume_buckets: Array; }; export type ErrorTrackingIssueAssigneeType = "user" | "role"; - export type ErrorTrackingIssueAssignee = { id: string | number; type: ErrorTrackingIssueAssigneeType }; + export type ErrorTrackingIssueAssignee = { + id: string | number; + type: ErrorTrackingIssueAssigneeType; + }; export type IntegrationKind = | "slack" | "salesforce" @@ -2807,20 +3051,39 @@ export namespace Schemas { | "meta-ads" | "clickup" | "reddit-ads"; - export type ErrorTrackingExternalReferenceIntegration = { display_name: string; id: number; kind: IntegrationKind }; + export type ErrorTrackingExternalReferenceIntegration = { + display_name: string; + id: number; + kind: IntegrationKind; + }; export type ErrorTrackingExternalReference = { external_url: string; id: string; integration: ErrorTrackingExternalReferenceIntegration; }; - export type FirstEvent = { properties: string; timestamp: string; uuid: string }; - export type LastEvent = { properties: string; timestamp: string; uuid: string }; - export type Status = "archived" | "active" | "resolved" | "pending_release" | "suppressed"; + export type FirstEvent = { + properties: string; + timestamp: string; + uuid: string; + }; + export type LastEvent = { + properties: string; + timestamp: string; + uuid: string; + }; + export type Status = + | "archived" + | "active" + | "resolved" + | "pending_release" + | "suppressed"; export type ErrorTrackingIssue = { aggregations?: (ErrorTrackingIssueAggregations | null) | undefined; assignee?: (ErrorTrackingIssueAssignee | null) | undefined; description?: (string | null) | undefined; - external_issues?: (Array | null) | undefined; + external_issues?: + | (Array | null) + | undefined; first_event?: (FirstEvent | null) | undefined; first_seen: string; id: string; @@ -2843,12 +3106,19 @@ export namespace Schemas { results: Array; timings?: (Array | null) | undefined; }; - export type Population = { both: number; exception_only: number; neither: number; success_only: number }; + export type Population = { + both: number; + exception_only: number; + neither: number; + success_only: number; + }; export type ErrorTrackingCorrelatedIssue = { assignee?: (ErrorTrackingIssueAssignee | null) | undefined; description?: (string | null) | undefined; event: string; - external_issues?: (Array | null) | undefined; + external_issues?: + | (Array | null) + | undefined; first_seen: string; id: string; last_seen: string; @@ -2877,7 +3147,11 @@ export namespace Schemas { | "low_win_probability" | "high_loss" | "high_p_value"; - export type ExperimentVariantFunnelsBaseStats = { failure_count: number; key: string; success_count: number }; + export type ExperimentVariantFunnelsBaseStats = { + failure_count: number; + key: string; + success_count: number; + }; export type Response20 = { credible_intervals: Record>; expected_loss: number; @@ -3140,7 +3414,9 @@ export namespace Schemas { modifiers?: (HogQLQueryModifiers | null) | undefined; offset?: (number | null) | undefined; orderBy?: (Array | null) | undefined; - properties?: (Array | null) | undefined; + properties?: + | (Array | null) + | undefined; response?: (GroupsQueryResponse | null) | undefined; search?: (string | null) | undefined; select?: (Array | null) | undefined; @@ -3175,18 +3451,27 @@ export namespace Schemas { timings?: (Array | null) | undefined; usedPreAggregatedTables?: (boolean | null) | undefined; }; - export type WebAnalyticsSampling = Partial<{ enabled: boolean | null; forceSamplingRate: SamplingRate | null }>; + export type WebAnalyticsSampling = Partial<{ + enabled: boolean | null; + forceSamplingRate: SamplingRate | null; + }>; export type WebOverviewQuery = { compareFilter?: (CompareFilter | null) | undefined; - conversionGoal?: (ActionConversionGoal | CustomEventConversionGoal | null) | undefined; + conversionGoal?: + | (ActionConversionGoal | CustomEventConversionGoal | null) + | undefined; dateRange?: (DateRange | null) | undefined; doPathCleaning?: (boolean | null) | undefined; filterTestAccounts?: (boolean | null) | undefined; includeRevenue?: (boolean | null) | undefined; kind?: string | undefined; modifiers?: (HogQLQueryModifiers | null) | undefined; - orderBy?: (Array | null) | undefined; - properties: Array; + orderBy?: + | (Array | null) + | undefined; + properties: Array< + EventPropertyFilter | PersonPropertyFilter | SessionPropertyFilter + >; response?: (WebOverviewQueryResponse | null) | undefined; sampling?: (WebAnalyticsSampling | null) | undefined; tags?: (QueryLogTags | null) | undefined; @@ -3237,7 +3522,9 @@ export namespace Schemas { export type WebStatsTableQuery = { breakdownBy: WebStatsBreakdown; compareFilter?: (CompareFilter | null) | undefined; - conversionGoal?: (ActionConversionGoal | CustomEventConversionGoal | null) | undefined; + conversionGoal?: + | (ActionConversionGoal | CustomEventConversionGoal | null) + | undefined; dateRange?: (DateRange | null) | undefined; doPathCleaning?: (boolean | null) | undefined; filterTestAccounts?: (boolean | null) | undefined; @@ -3248,8 +3535,12 @@ export namespace Schemas { limit?: (number | null) | undefined; modifiers?: (HogQLQueryModifiers | null) | undefined; offset?: (number | null) | undefined; - orderBy?: (Array | null) | undefined; - properties: Array; + orderBy?: + | (Array | null) + | undefined; + properties: Array< + EventPropertyFilter | PersonPropertyFilter | SessionPropertyFilter + >; response?: (WebStatsTableQueryResponse | null) | undefined; sampling?: (WebAnalyticsSampling | null) | undefined; tags?: (QueryLogTags | null) | undefined; @@ -3273,7 +3564,9 @@ export namespace Schemas { }; export type WebExternalClicksTableQuery = { compareFilter?: (CompareFilter | null) | undefined; - conversionGoal?: (ActionConversionGoal | CustomEventConversionGoal | null) | undefined; + conversionGoal?: + | (ActionConversionGoal | CustomEventConversionGoal | null) + | undefined; dateRange?: (DateRange | null) | undefined; doPathCleaning?: (boolean | null) | undefined; filterTestAccounts?: (boolean | null) | undefined; @@ -3281,8 +3574,12 @@ export namespace Schemas { kind?: string | undefined; limit?: (number | null) | undefined; modifiers?: (HogQLQueryModifiers | null) | undefined; - orderBy?: (Array | null) | undefined; - properties: Array; + orderBy?: + | (Array | null) + | undefined; + properties: Array< + EventPropertyFilter | PersonPropertyFilter | SessionPropertyFilter + >; response?: (WebExternalClicksTableQueryResponse | null) | undefined; sampling?: (WebAnalyticsSampling | null) | undefined; stripQueryParams?: (boolean | null) | undefined; @@ -3307,7 +3604,9 @@ export namespace Schemas { }; export type WebGoalsQuery = { compareFilter?: (CompareFilter | null) | undefined; - conversionGoal?: (ActionConversionGoal | CustomEventConversionGoal | null) | undefined; + conversionGoal?: + | (ActionConversionGoal | CustomEventConversionGoal | null) + | undefined; dateRange?: (DateRange | null) | undefined; doPathCleaning?: (boolean | null) | undefined; filterTestAccounts?: (boolean | null) | undefined; @@ -3315,8 +3614,12 @@ export namespace Schemas { kind?: string | undefined; limit?: (number | null) | undefined; modifiers?: (HogQLQueryModifiers | null) | undefined; - orderBy?: (Array | null) | undefined; - properties: Array; + orderBy?: + | (Array | null) + | undefined; + properties: Array< + EventPropertyFilter | PersonPropertyFilter | SessionPropertyFilter + >; response?: (WebGoalsQueryResponse | null) | undefined; sampling?: (WebAnalyticsSampling | null) | undefined; tags?: (QueryLogTags | null) | undefined; @@ -3325,18 +3628,30 @@ export namespace Schemas { }; export type WebVitalsQuery = { compareFilter?: (CompareFilter | null) | undefined; - conversionGoal?: (ActionConversionGoal | CustomEventConversionGoal | null) | undefined; + conversionGoal?: + | (ActionConversionGoal | CustomEventConversionGoal | null) + | undefined; dateRange?: (DateRange | null) | undefined; doPathCleaning?: (boolean | null) | undefined; filterTestAccounts?: (boolean | null) | undefined; includeRevenue?: (boolean | null) | undefined; kind?: string | undefined; modifiers?: (HogQLQueryModifiers | null) | undefined; - orderBy?: (Array | null) | undefined; - properties: Array; + orderBy?: + | (Array | null) + | undefined; + properties: Array< + EventPropertyFilter | PersonPropertyFilter | SessionPropertyFilter + >; response?: (WebGoalsQueryResponse | null) | undefined; sampling?: (WebAnalyticsSampling | null) | undefined; - source: TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery; + source: + | TrendsQuery + | FunnelsQuery + | RetentionQuery + | PathsQuery + | StickinessQuery + | LifecycleQuery; tags?: (QueryLogTags | null) | undefined; useSessionsTable?: (boolean | null) | undefined; version?: (number | null) | undefined; @@ -3354,7 +3669,9 @@ export namespace Schemas { }; export type WebVitalsPathBreakdownQuery = { compareFilter?: (CompareFilter | null) | undefined; - conversionGoal?: (ActionConversionGoal | CustomEventConversionGoal | null) | undefined; + conversionGoal?: + | (ActionConversionGoal | CustomEventConversionGoal | null) + | undefined; dateRange?: (DateRange | null) | undefined; doPathCleaning?: (boolean | null) | undefined; filterTestAccounts?: (boolean | null) | undefined; @@ -3362,9 +3679,13 @@ export namespace Schemas { kind?: string | undefined; metric: WebVitalsMetric; modifiers?: (HogQLQueryModifiers | null) | undefined; - orderBy?: (Array | null) | undefined; + orderBy?: + | (Array | null) + | undefined; percentile: WebVitalsPercentile; - properties: Array; + properties: Array< + EventPropertyFilter | PersonPropertyFilter | SessionPropertyFilter + >; response?: (WebVitalsPathBreakdownQueryResponse | null) | undefined; sampling?: (WebAnalyticsSampling | null) | undefined; tags?: (QueryLogTags | null) | undefined; @@ -3372,7 +3693,10 @@ export namespace Schemas { useSessionsTable?: (boolean | null) | undefined; version?: (number | null) | undefined; }; - export type Filters = Partial<{ dateRange: DateRange | null; properties: Array | null }>; + export type Filters = Partial<{ + dateRange: DateRange | null; + properties: Array | null; + }>; export type SessionAttributionGroupBy = | "ChannelType" | "Medium" @@ -3406,7 +3730,10 @@ export namespace Schemas { tags?: (QueryLogTags | null) | undefined; version?: (number | null) | undefined; }; - export type RevenueAnalyticsBreakdown = { property: string; type?: string | undefined }; + export type RevenueAnalyticsBreakdown = { + property: string; + type?: string | undefined; + }; export type SimpleIntervalType = "day" | "month"; export type RevenueAnalyticsGrossRevenueQueryResponse = { columns?: (Array | null) | undefined; @@ -3574,10 +3901,19 @@ export namespace Schemas { }; export type MarketingAnalyticsTableQuery = { compareFilter?: (CompareFilter | null) | undefined; - conversionGoal?: (ActionConversionGoal | CustomEventConversionGoal | null) | undefined; + conversionGoal?: + | (ActionConversionGoal | CustomEventConversionGoal | null) + | undefined; dateRange?: (DateRange | null) | undefined; doPathCleaning?: (boolean | null) | undefined; - draftConversionGoal?: (ConversionGoalFilter1 | ConversionGoalFilter2 | ConversionGoalFilter3 | null) | undefined; + draftConversionGoal?: + | ( + | ConversionGoalFilter1 + | ConversionGoalFilter2 + | ConversionGoalFilter3 + | null + ) + | undefined; filterTestAccounts?: (boolean | null) | undefined; includeAllConversions?: (boolean | null) | undefined; includeRevenue?: (boolean | null) | undefined; @@ -3585,8 +3921,12 @@ export namespace Schemas { limit?: (number | null) | undefined; modifiers?: (HogQLQueryModifiers | null) | undefined; offset?: (number | null) | undefined; - orderBy?: (Array> | null) | undefined; - properties: Array; + orderBy?: + | (Array> | null) + | undefined; + properties: Array< + EventPropertyFilter | PersonPropertyFilter | SessionPropertyFilter + >; response?: (MarketingAnalyticsTableQueryResponse | null) | undefined; sampling?: (WebAnalyticsSampling | null) | undefined; select?: (Array | null) | undefined; @@ -3594,7 +3934,12 @@ export namespace Schemas { useSessionsTable?: (boolean | null) | undefined; version?: (number | null) | undefined; }; - export type OrderBy = "last_seen" | "first_seen" | "occurrences" | "users" | "sessions"; + export type OrderBy = + | "last_seen" + | "first_seen" + | "occurrences" + | "users" + | "sessions"; export type OrderDirection = "ASC" | "DESC"; export type ErrorTrackingQueryResponse = { columns?: (Array | null) | undefined; @@ -3609,7 +3954,13 @@ export namespace Schemas { results: Array; timings?: (Array | null) | undefined; }; - export type Status2 = "archived" | "active" | "resolved" | "pending_release" | "suppressed" | "all"; + export type Status2 = + | "archived" + | "active" + | "resolved" + | "pending_release" + | "suppressed" + | "all"; export type ErrorTrackingQuery = { assignee?: (ErrorTrackingIssueAssignee | null) | undefined; dateRange: DateRange; @@ -3837,7 +4188,9 @@ export namespace Schemas { showHogQLEditor?: (boolean | null) | undefined; showOpenEditorButton?: (boolean | null) | undefined; showPersistentColumnConfigurator?: (boolean | null) | undefined; - showPropertyFilter?: (boolean | Array | null) | undefined; + showPropertyFilter?: + | (boolean | Array | null) + | undefined; showReload?: (boolean | null) | undefined; showResultsTable?: (boolean | null) | undefined; showSavedFilters?: (boolean | null) | undefined; @@ -3887,7 +4240,12 @@ export namespace Schemas { tableSettings?: (TableSettings | null) | undefined; version?: (number | null) | undefined; }; - export type DataWarehouseSavedQueryStatusEnum = "Cancelled" | "Modified" | "Completed" | "Failed" | "Running"; + export type DataWarehouseSavedQueryStatusEnum = + | "Cancelled" + | "Modified" + | "Completed" + | "Failed" + | "Running"; export type DataWarehouseSavedQuery = { id: string; deleted?: (boolean | null) | undefined; @@ -4054,7 +4412,10 @@ export namespace Schemas { row_count?: (number | null) | undefined; type?: string | undefined; }; - export type DatabaseSchemaQueryResponse = { joins: Array; tables: Record }; + export type DatabaseSchemaQueryResponse = { + joins: Array; + tables: Record; + }; export type DatabaseSchemaQuery = Partial<{ kind: string; modifiers: HogQLQueryModifiers | null; @@ -4103,10 +4464,18 @@ export namespace Schemas { ensure_experience_continuity?: (boolean | null) | undefined; has_encrypted_payloads?: (boolean | null) | undefined; version?: (number | null) | undefined; - evaluation_runtime?: ((EvaluationRuntimeEnum | BlankEnum | NullEnum) | null) | undefined; + evaluation_runtime?: + | ((EvaluationRuntimeEnum | BlankEnum | NullEnum) | null) + | undefined; evaluation_tags: Array; }; - export type StageEnum = "draft" | "concept" | "alpha" | "beta" | "general-availability" | "archived"; + export type StageEnum = + | "draft" + | "concept" + | "alpha" + | "beta" + | "general-availability" + | "archived"; export type EarlyAccessFeature = { id: string; feature_flag: MinimalFeatureFlag & unknown; @@ -4147,7 +4516,10 @@ export namespace Schemas { order_key: number; disabled_data?: (unknown | null) | undefined; }; - export type ErrorTrackingFingerprint = { fingerprint: string; issue_id: string }; + export type ErrorTrackingFingerprint = { + fingerprint: string; + issue_id: string; + }; export type ErrorTrackingGroupingRule = { id: string; filters: unknown; @@ -4164,7 +4536,11 @@ export namespace Schemas { version: string; project: string; }; - export type ErrorTrackingSuppressionRule = { id: string; filters: unknown; order_key: number }; + export type ErrorTrackingSuppressionRule = { + id: string; + filters: unknown; + order_key: number; + }; export type ErrorTrackingSymbolSet = { id: string; ref: string; @@ -4173,7 +4549,11 @@ export namespace Schemas { storage_ptr?: (string | null) | undefined; failure_reason?: (string | null) | undefined; }; - export type EventTaxonomyItem = { property: string; sample_count: number; sample_values: Array }; + export type EventTaxonomyItem = { + property: string; + sample_count: number; + sample_values: Array; + }; export type EventTaxonomyQueryResponse = { error?: (string | null) | undefined; hogql?: (string | null) | undefined; @@ -4308,7 +4688,9 @@ export namespace Schemas { math_hogql?: (string | null) | undefined; math_multiplier?: (number | null) | undefined; math_property?: (string | null) | undefined; - math_property_revenue_currency?: (RevenueCurrencyPropertyConfig | null) | undefined; + math_property_revenue_currency?: + | (RevenueCurrencyPropertyConfig | null) + | undefined; math_property_type?: (string | null) | undefined; name?: (string | null) | undefined; optionalInFunnel?: (boolean | null) | undefined; @@ -4380,7 +4762,11 @@ export namespace Schemas { name: string; updated_at?: (string | null) | undefined; }; - export type ExperimentExposureTimeSeries = { days: Array; exposure_counts: Array; variant: string }; + export type ExperimentExposureTimeSeries = { + days: Array; + exposure_counts: Array; + variant: string; + }; export type ExperimentExposureQueryResponse = { date_range: DateRange; kind?: string | undefined; @@ -4404,7 +4790,9 @@ export namespace Schemas { export type ExperimentMetricGoal = "increase" | "decrease"; export type ExperimentFunnelMetric = { conversion_window?: (number | null) | undefined; - conversion_window_unit?: (FunnelConversionWindowTimeUnit | null) | undefined; + conversion_window_unit?: + | (FunnelConversionWindowTimeUnit | null) + | undefined; fingerprint?: (string | null) | undefined; funnel_order_type?: (StepOrderValue | null) | undefined; goal?: (ExperimentMetricGoal | null) | undefined; @@ -4418,7 +4806,9 @@ export namespace Schemas { }; export type ExperimentMeanMetric = { conversion_window?: (number | null) | undefined; - conversion_window_unit?: (FunnelConversionWindowTimeUnit | null) | undefined; + conversion_window_unit?: + | (FunnelConversionWindowTimeUnit | null) + | undefined; fingerprint?: (string | null) | undefined; goal?: (ExperimentMetricGoal | null) | undefined; kind?: string | undefined; @@ -4433,7 +4823,9 @@ export namespace Schemas { }; export type ExperimentRatioMetric = { conversion_window?: (number | null) | undefined; - conversion_window_unit?: (FunnelConversionWindowTimeUnit | null) | undefined; + conversion_window_unit?: + | (FunnelConversionWindowTimeUnit | null) + | undefined; denominator: EventsNode | ActionsNode | ExperimentDataWarehouseNode; fingerprint?: (string | null) | undefined; goal?: (ExperimentMetricGoal | null) | undefined; @@ -4445,7 +4837,11 @@ export namespace Schemas { uuid?: (string | null) | undefined; version?: (number | null) | undefined; }; - export type SessionData = { event_uuid: string; person_id: string; session_id: string }; + export type SessionData = { + event_uuid: string; + person_id: string; + session_id: string; + }; export type ExperimentStatsValidationFailure = | "not-enough-exposures" | "baseline-mean-is-zero" @@ -4460,7 +4856,9 @@ export namespace Schemas { step_sessions?: (Array> | null) | undefined; sum: number; sum_squares: number; - validation_failures?: (Array | null) | undefined; + validation_failures?: + | (Array | null) + | undefined; }; export type ExperimentVariantResultFrequentist = { confidence_interval?: (Array | null) | undefined; @@ -4476,7 +4874,9 @@ export namespace Schemas { step_sessions?: (Array> | null) | undefined; sum: number; sum_squares: number; - validation_failures?: (Array | null) | undefined; + validation_failures?: + | (Array | null) + | undefined; }; export type ExperimentVariantResultBayesian = { chance_to_win?: (number | null) | undefined; @@ -4492,26 +4892,41 @@ export namespace Schemas { step_sessions?: (Array> | null) | undefined; sum: number; sum_squares: number; - validation_failures?: (Array | null) | undefined; + validation_failures?: + | (Array | null) + | undefined; }; export type ExperimentQueryResponse = Partial<{ baseline: ExperimentStatsBaseValidated | null; credible_intervals: Record> | null; insight: Array> | null; kind: string; - metric: ExperimentMeanMetric | ExperimentFunnelMetric | ExperimentRatioMetric | null; + metric: + | ExperimentMeanMetric + | ExperimentFunnelMetric + | ExperimentRatioMetric + | null; p_value: number | null; probability: Record | null; significance_code: ExperimentSignificanceCode | null; significant: boolean | null; stats_version: number | null; - variant_results: Array | Array | null; - variants: Array | Array | null; + variant_results: + | Array + | Array + | null; + variants: + | Array + | Array + | null; }>; export type ExperimentQuery = { experiment_id?: (number | null) | undefined; kind?: string | undefined; - metric: ExperimentMeanMetric | ExperimentFunnelMetric | ExperimentRatioMetric; + metric: + | ExperimentMeanMetric + | ExperimentFunnelMetric + | ExperimentRatioMetric; modifiers?: (HogQLQueryModifiers | null) | undefined; name?: (string | null) | undefined; response?: (ExperimentQueryResponse | null) | undefined; @@ -4549,8 +4964,13 @@ export namespace Schemas { expires_after?: (string | null) | undefined; exception: string | null; }; - export type ExternalQueryErrorCode = "platform_access_required" | "query_execution_failed"; - export type ExternalQueryError = { code: ExternalQueryErrorCode; detail: string }; + export type ExternalQueryErrorCode = + | "platform_access_required" + | "query_execution_failed"; + export type ExternalQueryError = { + code: ExternalQueryErrorCode; + detail: string; + }; export type ExternalQueryStatus = "success" | "error"; export type FeatureFlag = { id: number; @@ -4582,7 +5002,9 @@ export namespace Schemas { is_remote_configuration?: (boolean | null) | undefined; has_encrypted_payloads?: (boolean | null) | undefined; status: string; - evaluation_runtime?: ((EvaluationRuntimeEnum | BlankEnum | NullEnum) | null) | undefined; + evaluation_runtime?: + | ((EvaluationRuntimeEnum | BlankEnum | NullEnum) | null) + | undefined; _create_in_folder?: string | undefined; _should_create_usage_dashboard?: boolean | undefined; }; @@ -4763,7 +5185,12 @@ export namespace Schemas { status: (HogFunctionStatus & (unknown | null)) | null; execution_order: number | null; }; - export type HogLanguage = "hog" | "hogJson" | "hogQL" | "hogQLExpr" | "hogTemplate"; + export type HogLanguage = + | "hog" + | "hogJson" + | "hogQL" + | "hogQLExpr" + | "hogTemplate"; export type HogQLASTQuery = { explain?: (boolean | null) | undefined; filters?: (HogQLFilters | null) | undefined; @@ -4798,7 +5225,11 @@ export namespace Schemas { export type InsightActorsQueryOptions = { kind?: string | undefined; response?: (InsightActorsQueryOptionsResponse | null) | undefined; - source: InsightActorsQuery | FunnelsActorsQuery | FunnelCorrelationActorsQuery | StickinessActorsQuery; + source: + | InsightActorsQuery + | FunnelsActorsQuery + | FunnelCorrelationActorsQuery + | StickinessActorsQuery; version?: (number | null) | undefined; }; export type TimelineEntry = { @@ -4854,7 +5285,9 @@ export namespace Schemas { }; export type WebPageURLSearchQuery = { compareFilter?: (CompareFilter | null) | undefined; - conversionGoal?: (ActionConversionGoal | CustomEventConversionGoal | null) | undefined; + conversionGoal?: + | (ActionConversionGoal | CustomEventConversionGoal | null) + | undefined; dateRange?: (DateRange | null) | undefined; doPathCleaning?: (boolean | null) | undefined; filterTestAccounts?: (boolean | null) | undefined; @@ -4862,8 +5295,12 @@ export namespace Schemas { kind?: string | undefined; limit?: (number | null) | undefined; modifiers?: (HogQLQueryModifiers | null) | undefined; - orderBy?: (Array | null) | undefined; - properties: Array; + orderBy?: + | (Array | null) + | undefined; + properties: Array< + EventPropertyFilter | PersonPropertyFilter | SessionPropertyFilter + >; response?: (WebPageURLSearchQueryResponse | null) | undefined; sampling?: (WebAnalyticsSampling | null) | undefined; searchTerm?: (string | null) | undefined; @@ -4910,7 +5347,9 @@ export namespace Schemas { }; export type WebTrendsQuery = { compareFilter?: (CompareFilter | null) | undefined; - conversionGoal?: (ActionConversionGoal | CustomEventConversionGoal | null) | undefined; + conversionGoal?: + | (ActionConversionGoal | CustomEventConversionGoal | null) + | undefined; dateRange?: (DateRange | null) | undefined; doPathCleaning?: (boolean | null) | undefined; filterTestAccounts?: (boolean | null) | undefined; @@ -4921,8 +5360,12 @@ export namespace Schemas { metrics: Array; modifiers?: (HogQLQueryModifiers | null) | undefined; offset?: (number | null) | undefined; - orderBy?: (Array | null) | undefined; - properties: Array; + orderBy?: + | (Array | null) + | undefined; + properties: Array< + EventPropertyFilter | PersonPropertyFilter | SessionPropertyFilter + >; response?: (WebTrendsQueryResponse | null) | undefined; sampling?: (WebAnalyticsSampling | null) | undefined; tags?: (QueryLogTags | null) | undefined; @@ -4937,7 +5380,9 @@ export namespace Schemas { export type WebAnalyticsExternalSummaryQuery = { dateRange: DateRange; kind?: string | undefined; - properties: Array; + properties: Array< + EventPropertyFilter | PersonPropertyFilter | SessionPropertyFilter + >; response?: (WebAnalyticsExternalSummaryQueryResponse | null) | undefined; version?: (number | null) | undefined; }; @@ -4955,7 +5400,13 @@ export namespace Schemas { results: unknown; timings?: (Array | null) | undefined; }; - export type LogSeverityLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal"; + export type LogSeverityLevel = + | "trace" + | "debug" + | "info" + | "warn" + | "error" + | "fatal"; export type LogsQuery = { dateRange: DateRange; filterGroup: PropertyGroupFilter; @@ -4984,7 +5435,10 @@ export namespace Schemas { | "activity_score"; export type RecordingOrderDirection = "ASC" | "DESC"; export type MatchedRecordingEvent = { uuid: string }; - export type MatchedRecording = { events: Array; session_id?: (string | null) | undefined }; + export type MatchedRecording = { + events: Array; + session_id?: (string | null) | undefined; + }; export type PersonType = { created_at?: (string | null) | undefined; distinct_ids: Array; @@ -5023,7 +5477,10 @@ export namespace Schemas { viewed: boolean; viewers: Array; }; - export type RecordingsQueryResponse = { has_next: boolean; results: Array }; + export type RecordingsQueryResponse = { + has_next: boolean; + results: Array; + }; export type RecordingsQuery = Partial<{ actions: Array> | null; comment_text: RecordingPropertyFilter | null; @@ -5265,7 +5722,10 @@ export namespace Schemas { hideSizeColumn: boolean | null; useSmallLayout: boolean | null; }>; - export type VizSpecificOptions = Partial<{ ActionsPie: ActionsPie | null; RETENTION: RETENTION | null }>; + export type VizSpecificOptions = Partial<{ + ActionsPie: ActionsPie | null; + RETENTION: RETENTION | null; + }>; export type InsightVizNode = { embedded?: (boolean | null) | undefined; full?: (boolean | null) | undefined; @@ -5279,7 +5739,13 @@ export namespace Schemas { showLastComputationRefresh?: (boolean | null) | undefined; showResults?: (boolean | null) | undefined; showTable?: (boolean | null) | undefined; - source: TrendsQuery | FunnelsQuery | RetentionQuery | PathsQuery | StickinessQuery | LifecycleQuery; + source: + | TrendsQuery + | FunnelsQuery + | RetentionQuery + | PathsQuery + | StickinessQuery + | LifecycleQuery; suppressSessionAnalysisWarning?: (boolean | null) | undefined; version?: (number | null) | undefined; vizSpecificOptions?: (VizSpecificOptions | null) | undefined; @@ -5390,7 +5856,9 @@ export namespace Schemas { allow_publicly_shared_resources?: boolean | undefined; member_count: string; is_ai_data_processing_approved?: (boolean | null) | undefined; - default_experiment_stats_method?: ((DefaultExperimentStatsMethodEnum | BlankEnum | NullEnum) | null) | undefined; + default_experiment_stats_method?: + | ((DefaultExperimentStatsMethodEnum | BlankEnum | NullEnum) | null) + | undefined; default_role_id?: (string | null) | undefined; }; export type OrganizationBasic = { @@ -5481,7 +5949,10 @@ export namespace Schemas { previous?: (string | null) | undefined; results: Array; }; - export type PaginatedClickhouseEventList = Partial<{ next: string | null; results: Array }>; + export type PaginatedClickhouseEventList = Partial<{ + next: string | null; + results: Array; + }>; export type PaginatedCohortList = { count: number; next?: (string | null) | undefined; @@ -5683,7 +6154,12 @@ export namespace Schemas { results: Array; }>; export type PluginLogEntrySourceEnum = "SYSTEM" | "PLUGIN" | "CONSOLE"; - export type PluginLogEntryTypeEnum = "DEBUG" | "LOG" | "INFO" | "WARN" | "ERROR"; + export type PluginLogEntryTypeEnum = + | "DEBUG" + | "LOG" + | "INFO" + | "WARN" + | "ERROR"; export type PluginLogEntry = { id: string; team_id: number; @@ -6317,12 +6793,19 @@ export namespace Schemas { previous?: (string | null) | undefined; results: Array; }; - export type PropertyTypeEnum = "DateTime" | "String" | "Numeric" | "Boolean" | "Duration"; + export type PropertyTypeEnum = + | "DateTime" + | "String" + | "Numeric" + | "Boolean" + | "Duration"; export type PropertyDefinition = { id: string; name: string; is_numerical?: boolean | undefined; - property_type?: ((PropertyTypeEnum | BlankEnum | NullEnum) | null) | undefined; + property_type?: + | ((PropertyTypeEnum | BlankEnum | NullEnum) | null) + | undefined; tags?: Array | undefined; is_seen_on_filtered_events: string; }; @@ -6495,7 +6978,9 @@ export namespace Schemas { current_iteration?: (number | null) | undefined; current_iteration_start_date?: (string | null) | undefined; response_sampling_start_date?: (string | null) | undefined; - response_sampling_interval_type?: ((ResponseSamplingIntervalTypeEnum | BlankEnum | NullEnum) | null) | undefined; + response_sampling_interval_type?: + | ((ResponseSamplingIntervalTypeEnum | BlankEnum | NullEnum) | null) + | undefined; response_sampling_interval?: (number | null) | undefined; response_sampling_limit?: (number | null) | undefined; response_sampling_daily_limits?: (unknown | null) | undefined; @@ -6507,7 +6992,13 @@ export namespace Schemas { previous?: (string | null) | undefined; results: Array; }; - export type TableFormatEnum = "CSV" | "CSVWithNames" | "Parquet" | "JSONEachRow" | "Delta" | "DeltaS3Wrapper"; + export type TableFormatEnum = + | "CSV" + | "CSVWithNames" + | "Parquet" + | "JSONEachRow" + | "Delta" + | "DeltaS3Wrapper"; export type SourceTypeEnum = | "Stripe" | "Hubspot" @@ -6654,7 +7145,10 @@ export namespace Schemas { results: Array; }; export type ToolbarModeEnum = "disabled" | "toolbar"; - export type ScenePersonalisationBasic = { scene: string; dashboard?: (number | null) | undefined }; + export type ScenePersonalisationBasic = { + scene: string; + dashboard?: (number | null) | undefined; + }; export type ThemeModeEnum = "light" | "dark" | "system"; export type User = { date_joined: string; @@ -6667,7 +7161,9 @@ export namespace Schemas { is_email_verified: boolean | null; notification_settings?: Record | undefined; anonymize_data?: (boolean | null) | undefined; - toolbar_mode?: ((ToolbarModeEnum | BlankEnum | NullEnum) | null) | undefined; + toolbar_mode?: + | ((ToolbarModeEnum | BlankEnum | NullEnum) | null) + | undefined; has_password: boolean; id: number; is_staff?: boolean | undefined; @@ -6736,7 +7232,9 @@ export namespace Schemas { creation_context: string; _create_in_folder: string; }>; - export type PatchedAddPersonsToStaticCohortRequest = Partial<{ person_ids: Array }>; + export type PatchedAddPersonsToStaticCohortRequest = Partial<{ + person_ids: Array; + }>; export type PatchedAnnotation = Partial<{ id: number; content: string | null; @@ -6922,7 +7420,11 @@ export namespace Schemas { version: string; project: string; }>; - export type PatchedErrorTrackingSuppressionRule = Partial<{ id: string; filters: unknown; order_key: number }>; + export type PatchedErrorTrackingSuppressionRule = Partial<{ + id: string; + filters: unknown; + order_key: number; + }>; export type PatchedErrorTrackingSymbolSet = Partial<{ id: string; ref: string; @@ -7149,7 +7651,9 @@ export namespace Schemas { allow_publicly_shared_resources: boolean; member_count: string; is_ai_data_processing_approved: boolean | null; - default_experiment_stats_method: (DefaultExperimentStatsMethodEnum | BlankEnum | NullEnum) | null; + default_experiment_stats_method: + | (DefaultExperimentStatsMethodEnum | BlankEnum | NullEnum) + | null; default_role_id: string | null; }>; export type PatchedOrganizationDomain = Partial<{ @@ -7198,7 +7702,9 @@ export namespace Schemas { name: string; product_description: string | null; created_at: string; - effective_membership_level: (EffectiveMembershipLevelEnum & (unknown | null)) | null; + effective_membership_level: + | (EffectiveMembershipLevelEnum & (unknown | null)) + | null; has_group_types: boolean; group_types: Array>; live_events_token: string | null; @@ -7368,7 +7874,9 @@ export namespace Schemas { current_iteration: number | null; current_iteration_start_date: string | null; response_sampling_start_date: string | null; - response_sampling_interval_type: (ResponseSamplingIntervalTypeEnum | BlankEnum | NullEnum) | null; + response_sampling_interval_type: + | (ResponseSamplingIntervalTypeEnum | BlankEnum | NullEnum) + | null; response_sampling_interval: number | null; response_sampling_limit: number | null; response_sampling_daily_limits: unknown | null; @@ -7406,7 +7914,9 @@ export namespace Schemas { updated_at: string; }>; export type PatchedTaskUpdatePositionRequest = Partial<{ position: number }>; - export type PatchedTaskUpdateStageRequest = Partial<{ current_stage: string }>; + export type PatchedTaskUpdateStageRequest = Partial<{ + current_stage: string; + }>; export type PatchedTaskWorkflow = Partial<{ id: string; name: string; @@ -7428,7 +7938,10 @@ export namespace Schemas { goals: unknown; filter_test_accounts: boolean; }>; - export type TeamMarketingAnalyticsConfig = Partial<{ sources_map: unknown; conversion_goals: unknown }>; + export type TeamMarketingAnalyticsConfig = Partial<{ + sources_map: unknown; + conversion_goals: unknown; + }>; export type PatchedTeam = Partial<{ id: number; uuid: string; @@ -7481,7 +7994,9 @@ export namespace Schemas { primary_dashboard: number | null; live_events_columns: Array | null; recording_domains: Array | null; - cookieless_server_hash_mode: (CookielessServerHashModeEnum | NullEnum) | null; + cookieless_server_hash_mode: + | (CookielessServerHashModeEnum | NullEnum) + | null; human_friendly_comparison_periods: boolean | null; inject_web_apps: boolean | null; extra_settings: unknown | null; @@ -7499,7 +8014,9 @@ export namespace Schemas { onboarding_tasks: unknown | null; base_currency: BaseCurrencyEnum & unknown; web_analytics_pre_aggregated_tables_enabled: boolean | null; - effective_membership_level: (EffectiveMembershipLevelEnum & (unknown | null)) | null; + effective_membership_level: + | (EffectiveMembershipLevelEnum & (unknown | null)) + | null; has_group_types: boolean; group_types: Array>; live_events_token: string | null; @@ -7576,7 +8093,9 @@ export namespace Schemas { name?: string | undefined; product_description?: (string | null) | undefined; created_at: string; - effective_membership_level: (EffectiveMembershipLevelEnum & (unknown | null)) | null; + effective_membership_level: + | (EffectiveMembershipLevelEnum & (unknown | null)) + | null; has_group_types: boolean; group_types: Array>; live_events_token: string | null; @@ -7605,9 +8124,13 @@ export namespace Schemas { capture_performance_opt_in?: (boolean | null) | undefined; session_recording_opt_in?: boolean | undefined; session_recording_sample_rate?: (string | null) | undefined; - session_recording_minimum_duration_milliseconds?: (number | null) | undefined; + session_recording_minimum_duration_milliseconds?: + | (number | null) + | undefined; session_recording_linked_flag?: (unknown | null) | undefined; - session_recording_network_payload_capture_config?: (unknown | null) | undefined; + session_recording_network_payload_capture_config?: + | (unknown | null) + | undefined; session_recording_masking_config?: (unknown | null) | undefined; session_replay_config?: (unknown | null) | undefined; survey_config?: (unknown | null) | undefined; @@ -7657,7 +8180,10 @@ export namespace Schemas { operator?: ((OperatorEnum | BlankEnum | NullEnum) | null) | undefined; type?: (PropertyItemTypeEnum | BlankEnum) | undefined; }; - export type Property = { type?: (PropertyTypeEnum & unknown) | undefined; values: Array }; + export type Property = { + type?: (PropertyTypeEnum & unknown) | undefined; + values: Array; + }; export type SavedInsightNode = { allowSorting?: (boolean | null) | undefined; context?: (DataTableNodeViewPropsContext | null) | undefined; @@ -7683,7 +8209,9 @@ export namespace Schemas { showLastComputationRefresh?: (boolean | null) | undefined; showOpenEditorButton?: (boolean | null) | undefined; showPersistentColumnConfigurator?: (boolean | null) | undefined; - showPropertyFilter?: (boolean | Array | null) | undefined; + showPropertyFilter?: + | (boolean | Array | null) + | undefined; showReload?: (boolean | null) | undefined; showResults?: (boolean | null) | undefined; showResultsTable?: (boolean | null) | undefined; @@ -7787,7 +8315,9 @@ export namespace Schemas { | TraceQuery | VectorSearchQuery; refresh?: (RefreshType | null) | undefined; - variables_override?: (Record> | null) | undefined; + variables_override?: + | (Record> | null) + | undefined; }; export type QueryResponseAlternative1 = { columns: Array; @@ -7960,14 +8490,24 @@ export namespace Schemas { credible_intervals: Record> | null; insight: Array> | null; kind: string; - metric: ExperimentMeanMetric | ExperimentFunnelMetric | ExperimentRatioMetric | null; + metric: + | ExperimentMeanMetric + | ExperimentFunnelMetric + | ExperimentRatioMetric + | null; p_value: number | null; probability: Record | null; significance_code: ExperimentSignificanceCode | null; significant: boolean | null; stats_version: number | null; - variant_results: Array | Array | null; - variants: Array | Array | null; + variant_results: + | Array + | Array + | null; + variants: + | Array + | Array + | null; }>; export type QueryResponseAlternative18 = { date_range: DateRange; @@ -8427,7 +8967,10 @@ export namespace Schemas { timings?: (Array | null) | undefined; types?: (Array | null) | undefined; }; - export type QueryResponseAlternative62 = { joins: Array; tables: Record }; + export type QueryResponseAlternative62 = { + joins: Array; + tables: Record; + }; export type QueryResponseAlternative63 = { columns?: (Array | null) | undefined; error?: (string | null) | undefined; @@ -8466,7 +9009,9 @@ export namespace Schemas { modifiers?: (HogQLQueryModifiers | null) | undefined; query_status?: (QueryStatus | null) | undefined; resolved_date_range?: (ResolvedDateRangeResponse | null) | undefined; - results: ActorsPropertyTaxonomyResponse | Array; + results: + | ActorsPropertyTaxonomyResponse + | Array; timings?: (Array | null) | undefined; }; export type QueryResponseAlternative68 = { @@ -8679,7 +9224,10 @@ export namespace Schemas { | TraceQuery | VectorSearchQuery; }; - export type SessionSummaries = { session_ids: Array; focus_area?: string | undefined }; + export type SessionSummaries = { + session_ids: Array; + focus_area?: string | undefined; + }; export type SharingConfiguration = { created_at: string; enabled?: boolean | undefined; @@ -8688,7 +9236,11 @@ export namespace Schemas { password_required?: boolean | undefined; share_passwords: string; }; - export type Status219Enum = "started" | "in_progress" | "completed" | "failed"; + export type Status219Enum = + | "started" + | "in_progress" + | "completed" + | "failed"; export type SurveySerializerCreateUpdateOnly = { id: string; name: string; @@ -8717,15 +9269,22 @@ export namespace Schemas { current_iteration?: (number | null) | undefined; current_iteration_start_date?: (string | null) | undefined; response_sampling_start_date?: (string | null) | undefined; - response_sampling_interval_type?: ((ResponseSamplingIntervalTypeEnum | BlankEnum | NullEnum) | null) | undefined; + response_sampling_interval_type?: + | ((ResponseSamplingIntervalTypeEnum | BlankEnum | NullEnum) | null) + | undefined; response_sampling_interval?: (number | null) | undefined; response_sampling_limit?: (number | null) | undefined; response_sampling_daily_limits?: (unknown | null) | undefined; enable_partial_responses?: (boolean | null) | undefined; _create_in_folder?: string | undefined; }; - export type TaskBulkReorderRequest = { columns: Record> }; - export type TaskBulkReorderResponse = { updated: number; tasks: Array> }; + export type TaskBulkReorderRequest = { + columns: Record>; + }; + export type TaskBulkReorderResponse = { + updated: number; + tasks: Array>; + }; export type TaskProgressResponse = { has_progress: boolean; id?: string | undefined; @@ -8755,7 +9314,10 @@ export namespace Schemas { updated_at: string; workflow_id: string; }; - export type TaskProgressStreamResponse = { progress_updates: Array; server_time: string }; + export type TaskProgressStreamResponse = { + progress_updates: Array; + server_time: string; + }; export type Team = { id: number; uuid: string; @@ -8793,22 +9355,36 @@ export namespace Schemas { capture_performance_opt_in?: (boolean | null) | undefined; session_recording_opt_in?: boolean | undefined; session_recording_sample_rate?: (string | null) | undefined; - session_recording_minimum_duration_milliseconds?: (number | null) | undefined; + session_recording_minimum_duration_milliseconds?: + | (number | null) + | undefined; session_recording_linked_flag?: (unknown | null) | undefined; - session_recording_network_payload_capture_config?: (unknown | null) | undefined; + session_recording_network_payload_capture_config?: + | (unknown | null) + | undefined; session_recording_masking_config?: (unknown | null) | undefined; - session_recording_url_trigger_config?: (Array | null) | undefined; - session_recording_url_blocklist_config?: (Array | null) | undefined; - session_recording_event_trigger_config?: (Array | null) | undefined; + session_recording_url_trigger_config?: + | (Array | null) + | undefined; + session_recording_url_blocklist_config?: + | (Array | null) + | undefined; + session_recording_event_trigger_config?: + | (Array | null) + | undefined; session_recording_trigger_match_type_config?: (string | null) | undefined; - session_recording_retention_period?: SessionRecordingRetentionPeriodEnum | undefined; + session_recording_retention_period?: + | SessionRecordingRetentionPeriodEnum + | undefined; session_replay_config?: (unknown | null) | undefined; survey_config?: (unknown | null) | undefined; week_start_day?: ((WeekStartDayEnum | NullEnum) | null) | undefined; primary_dashboard?: (number | null) | undefined; live_events_columns?: (Array | null) | undefined; recording_domains?: (Array | null) | undefined; - cookieless_server_hash_mode?: ((CookielessServerHashModeEnum | NullEnum) | null) | undefined; + cookieless_server_hash_mode?: + | ((CookielessServerHashModeEnum | NullEnum) | null) + | undefined; human_friendly_comparison_periods?: (boolean | null) | undefined; inject_web_apps?: (boolean | null) | undefined; extra_settings?: (unknown | null) | undefined; @@ -8826,13 +9402,18 @@ export namespace Schemas { onboarding_tasks?: (unknown | null) | undefined; base_currency?: (BaseCurrencyEnum & unknown) | undefined; web_analytics_pre_aggregated_tables_enabled?: (boolean | null) | undefined; - effective_membership_level: (EffectiveMembershipLevelEnum & (unknown | null)) | null; + effective_membership_level: + | (EffectiveMembershipLevelEnum & (unknown | null)) + | null; has_group_types: boolean; group_types: Array>; live_events_token: string | null; product_intents: string; }; - export type WebAnalyticsBreakdownResponse = { next?: (string | null) | undefined; results: Array }; + export type WebAnalyticsBreakdownResponse = { + next?: (string | null) | undefined; + results: Array; + }; export type WebAnalyticsOverviewResponse = { visitors: number; views: number; @@ -8960,7 +9541,10 @@ export namespace Endpoints { body: Schemas.TaskBulkReorderRequest; }; - responses: { 200: Schemas.TaskBulkReorderResponse; 400: Schemas.ErrorResponse }; + responses: { + 200: Schemas.TaskBulkReorderResponse; + 400: Schemas.ErrorResponse; + }; }; export type get_Workflows_list = { method: "GET"; @@ -9030,7 +9614,11 @@ export namespace Endpoints { parameters: { path: { id: string; project_id: string }; }; - responses: { 200: Schemas.WorkflowDeactivateResponse; 400: Schemas.ErrorResponse; 404: unknown }; + responses: { + 200: Schemas.WorkflowDeactivateResponse; + 400: Schemas.ErrorResponse; + 404: unknown; + }; }; export type post_Workflows_set_default_create = { method: "POST"; @@ -9125,7 +9713,12 @@ export namespace Endpoints { path: "/api/users/"; requestFormat: "json"; parameters: { - query: Partial<{ email: string; is_staff: boolean; limit: number; offset: number }>; + query: Partial<{ + email: string; + is_staff: boolean; + limit: number; + offset: number; + }>; }; responses: { 200: Schemas.PaginatedUserList }; }; @@ -9406,8 +9999,13 @@ export type Endpoint = { }; export interface Fetcher { - decodePathParams?: (path: string, pathParams: Record) => string; - encodeSearchParams?: (searchParams: Record | undefined) => URLSearchParams; + decodePathParams?: ( + path: string, + pathParams: Record, + ) => string; + encodeSearchParams?: ( + searchParams: Record | undefined, + ) => URLSearchParams; // fetch: (input: { method: Method; @@ -9422,38 +10020,60 @@ export interface Fetcher { } export const successStatusCodes = [ - 200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308, + 200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, + 305, 306, 307, 308, ] as const; export type SuccessStatusCode = (typeof successStatusCodes)[number]; export const errorStatusCodes = [ - 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424, - 425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511, + 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, + 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451, 500, + 501, 502, 503, 504, 505, 506, 507, 508, 510, 511, ] as const; export type ErrorStatusCode = (typeof errorStatusCodes)[number]; // Taken from https://github.com/unjs/fetchdts/blob/ec4eaeab5d287116171fc1efd61f4a1ad34e4609/src/fetch.ts#L3 -export interface TypedHeaders | unknown> - extends Omit { +export interface TypedHeaders< + TypedHeaderValues extends Record | unknown, +> extends Omit< + Headers, + "append" | "delete" | "get" | "getSetCookie" | "has" | "set" | "forEach" + > { /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) */ - append: | (string & {})>( + append: < + Name extends Extract | (string & {}), + >( name: Name, - value: Lowercase extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase] : string, + value: Lowercase extends keyof TypedHeaderValues + ? TypedHeaderValues[Lowercase] + : string, ) => void; /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/delete) */ - delete: | (string & {})>(name: Name) => void; + delete: < + Name extends Extract | (string & {}), + >( + name: Name, + ) => void; /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get) */ get: | (string & {})>( name: Name, - ) => (Lowercase extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase] : string) | null; + ) => + | (Lowercase extends keyof TypedHeaderValues + ? TypedHeaderValues[Lowercase] + : string) + | null; /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie) */ getSetCookie: () => string[]; /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/has) */ - has: | (string & {})>(name: Name) => boolean; + has: | (string & {})>( + name: Name, + ) => boolean; /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) */ set: | (string & {})>( name: Name, - value: Lowercase extends keyof TypedHeaderValues ? TypedHeaderValues[Lowercase] : string, + value: Lowercase extends keyof TypedHeaderValues + ? TypedHeaderValues[Lowercase] + : string, ) => void; forEach: ( callbackfn: ( @@ -9487,23 +10107,47 @@ export interface TypedErrorResponse json: () => Promise; } -export type TypedApiResponse = {}, THeaders = {}> = { +export type TypedApiResponse< + TAllResponses extends Record = {}, + THeaders = {}, +> = { [K in keyof TAllResponses]: K extends string ? K extends `${infer TStatusCode extends number}` ? TStatusCode extends SuccessStatusCode - ? TypedSuccessResponse - : TypedErrorResponse + ? TypedSuccessResponse< + TAllResponses[K], + TStatusCode, + K extends keyof THeaders ? THeaders[K] : never + > + : TypedErrorResponse< + TAllResponses[K], + TStatusCode, + K extends keyof THeaders ? THeaders[K] : never + > : never : K extends number ? K extends SuccessStatusCode - ? TypedSuccessResponse - : TypedErrorResponse + ? TypedSuccessResponse< + TAllResponses[K], + K, + K extends keyof THeaders ? THeaders[K] : never + > + : TypedErrorResponse< + TAllResponses[K], + K, + K extends keyof THeaders ? THeaders[K] : never + > : never; }[keyof TAllResponses]; -export type SafeApiResponse = TEndpoint extends { responses: infer TResponses } +export type SafeApiResponse = TEndpoint extends { + responses: infer TResponses; +} ? TResponses extends Record - ? TypedApiResponse + ? TypedApiResponse< + TResponses, + TEndpoint extends { responseHeaders: infer THeaders } ? THeaders : never + > : never : never; @@ -9516,7 +10160,9 @@ type RequiredKeys = { [P in keyof T]-?: undefined extends T[P] ? never : P; }[keyof T]; -type MaybeOptionalArg = RequiredKeys extends never ? [config?: T] : [config: T]; +type MaybeOptionalArg = RequiredKeys extends never + ? [config?: T] + : [config: T]; type NotNever = [T] extends [never] ? false : true; // @@ -9551,14 +10197,22 @@ export class ApiClient { * Replace path parameters in URL * Supports both OpenAPI format {param} and Express format :param */ - defaultDecodePathParams = (url: string, params: Record): string => { + defaultDecodePathParams = ( + url: string, + params: Record, + ): string => { return url .replace(/{(\w+)}/g, (_, key: string) => params[key] || `{${key}}`) - .replace(/:([a-zA-Z0-9_]+)/g, (_, key: string) => params[key] || `:${key}`); + .replace( + /:([a-zA-Z0-9_]+)/g, + (_, key: string) => params[key] || `:${key}`, + ); }; /** Uses URLSearchParams, skips null/undefined values */ - defaultEncodeSearchParams = (queryParams: Record | undefined): URLSearchParams | undefined => { + defaultEncodeSearchParams = ( + queryParams: Record | undefined, + ): URLSearchParams | undefined => { if (!queryParams) return; const searchParams = new URLSearchParams(); @@ -9566,7 +10220,9 @@ export class ApiClient { if (value != null) { // Skip null/undefined values if (Array.isArray(value)) { - value.forEach((val) => val != null && searchParams.append(key, String(val))); + value.forEach( + (val) => val != null && searchParams.append(key, String(val)), + ); } else { searchParams.append(key, String(value)); } @@ -9607,20 +10263,49 @@ export class ApiClient { ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true - ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } + ? UParams & { + overrides?: RequestInit; + withResponse?: false; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: false; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: false; + throwOnStatusError?: boolean; + } > - ): Promise, { data: {} }>["data"]>; + ): Promise< + Extract< + InferResponseByStatus, + { data: {} } + >["data"] + >; get( path: Path, ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true - ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } + ? UParams & { + overrides?: RequestInit; + withResponse?: true; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: true; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: true; + throwOnStatusError?: boolean; + } > ): Promise>; @@ -9638,27 +10323,56 @@ export class ApiClient { ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true - ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } + ? UParams & { + overrides?: RequestInit; + withResponse?: false; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: false; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: false; + throwOnStatusError?: boolean; + } > - ): Promise, { data: {} }>["data"]>; + ): Promise< + Extract< + InferResponseByStatus, + { data: {} } + >["data"] + >; post( path: Path, ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true - ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } + ? UParams & { + overrides?: RequestInit; + withResponse?: true; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: true; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: true; + throwOnStatusError?: boolean; + } > ): Promise>; - post( - path: Path, - ...params: MaybeOptionalArg - ): Promise { + post< + Path extends keyof PostEndpoints, + _TEndpoint extends PostEndpoints[Path], + >(path: Path, ...params: MaybeOptionalArg): Promise { return this.request("post", path, ...params); } // @@ -9669,20 +10383,49 @@ export class ApiClient { ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true - ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } + ? UParams & { + overrides?: RequestInit; + withResponse?: false; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: false; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: false; + throwOnStatusError?: boolean; + } > - ): Promise, { data: {} }>["data"]>; + ): Promise< + Extract< + InferResponseByStatus, + { data: {} } + >["data"] + >; put( path: Path, ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true - ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } + ? UParams & { + overrides?: RequestInit; + withResponse?: true; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: true; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: true; + throwOnStatusError?: boolean; + } > ): Promise>; @@ -9695,63 +10438,133 @@ export class ApiClient { // // - patch( + patch< + Path extends keyof PatchEndpoints, + TEndpoint extends PatchEndpoints[Path], + >( path: Path, ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true - ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } + ? UParams & { + overrides?: RequestInit; + withResponse?: false; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: false; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: false; + throwOnStatusError?: boolean; + } > - ): Promise, { data: {} }>["data"]>; + ): Promise< + Extract< + InferResponseByStatus, + { data: {} } + >["data"] + >; - patch( + patch< + Path extends keyof PatchEndpoints, + TEndpoint extends PatchEndpoints[Path], + >( path: Path, ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true - ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } + ? UParams & { + overrides?: RequestInit; + withResponse?: true; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: true; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: true; + throwOnStatusError?: boolean; + } > ): Promise>; - patch( - path: Path, - ...params: MaybeOptionalArg - ): Promise { + patch< + Path extends keyof PatchEndpoints, + _TEndpoint extends PatchEndpoints[Path], + >(path: Path, ...params: MaybeOptionalArg): Promise { return this.request("patch", path, ...params); } // // - delete( + delete< + Path extends keyof DeleteEndpoints, + TEndpoint extends DeleteEndpoints[Path], + >( path: Path, ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true - ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } + ? UParams & { + overrides?: RequestInit; + withResponse?: false; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: false; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: false; + throwOnStatusError?: boolean; + } > - ): Promise, { data: {} }>["data"]>; + ): Promise< + Extract< + InferResponseByStatus, + { data: {} } + >["data"] + >; - delete( + delete< + Path extends keyof DeleteEndpoints, + TEndpoint extends DeleteEndpoints[Path], + >( path: Path, ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true - ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } + ? UParams & { + overrides?: RequestInit; + withResponse?: true; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: true; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: true; + throwOnStatusError?: boolean; + } > ): Promise>; - delete( - path: Path, - ...params: MaybeOptionalArg - ): Promise { + delete< + Path extends keyof DeleteEndpoints, + _TEndpoint extends DeleteEndpoints[Path], + >(path: Path, ...params: MaybeOptionalArg): Promise { return this.request("delete", path, ...params); } // @@ -9770,11 +10583,28 @@ export class ApiClient { ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true - ? UParams & { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: false; throwOnStatusError?: boolean } + ? UParams & { + overrides?: RequestInit; + withResponse?: false; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: false; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: false; + throwOnStatusError?: boolean; + } > - ): Promise, { data: {} }>["data"]>; + ): Promise< + Extract< + InferResponseByStatus, + { data: {} } + >["data"] + >; request< TMethod extends keyof EndpointByMethod, @@ -9786,9 +10616,21 @@ export class ApiClient { ...params: MaybeOptionalArg< TEndpoint extends { parameters: infer UParams } ? NotNever extends true - ? UParams & { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } - : { overrides?: RequestInit; withResponse?: true; throwOnStatusError?: boolean } + ? UParams & { + overrides?: RequestInit; + withResponse?: true; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: true; + throwOnStatusError?: boolean; + } + : { + overrides?: RequestInit; + withResponse?: true; + throwOnStatusError?: boolean; + } > ): Promise>; @@ -9796,28 +10638,40 @@ export class ApiClient { TMethod extends keyof EndpointByMethod, TPath extends keyof EndpointByMethod[TMethod], TEndpoint extends EndpointByMethod[TMethod][TPath], - >(method: TMethod, path: TPath, ...params: MaybeOptionalArg): Promise { + >( + method: TMethod, + path: TPath, + ...params: MaybeOptionalArg + ): Promise { const requestParams = params[0]; const withResponse = requestParams?.withResponse; const { withResponse: _, - throwOnStatusError = withResponse ? false : true, + throwOnStatusError = !withResponse, overrides, ...fetchParams } = requestParams || {}; const parametersToSend: EndpointParameters = {}; - if (requestParams?.body !== undefined) (parametersToSend as any).body = requestParams.body; - if (requestParams?.query !== undefined) (parametersToSend as any).query = requestParams.query; - if (requestParams?.header !== undefined) (parametersToSend as any).header = requestParams.header; - if (requestParams?.path !== undefined) (parametersToSend as any).path = requestParams.path; + if (requestParams?.body !== undefined) + (parametersToSend as any).body = requestParams.body; + if (requestParams?.query !== undefined) + (parametersToSend as any).query = requestParams.query; + if (requestParams?.header !== undefined) + (parametersToSend as any).header = requestParams.header; + if (requestParams?.path !== undefined) + (parametersToSend as any).path = requestParams.path; - const resolvedPath = (this.fetcher.decodePathParams ?? this.defaultDecodePathParams)( + const resolvedPath = ( + this.fetcher.decodePathParams ?? this.defaultDecodePathParams + )( this.baseUrl + (path as string), (parametersToSend.path ?? {}) as Record, ); const url = new URL(resolvedPath); - const urlSearchParams = (this.fetcher.encodeSearchParams ?? this.defaultEncodeSearchParams)(parametersToSend.query); + const urlSearchParams = ( + this.fetcher.encodeSearchParams ?? this.defaultEncodeSearchParams + )(parametersToSend.query); const promise = this.fetcher .fetch({ @@ -9830,20 +10684,28 @@ export class ApiClient { throwOnStatusError, }) .then(async (response) => { - const data = await (this.fetcher.parseResponseData ?? this.defaultParseResponseData)(response); + const data = await ( + this.fetcher.parseResponseData ?? this.defaultParseResponseData + )(response); const typedResponse = Object.assign(response, { data: data, json: () => Promise.resolve(data), }) as SafeApiResponse; - if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) { + if ( + throwOnStatusError && + errorStatusCodes.includes(response.status as never) + ) { throw new TypedStatusError(typedResponse as never); } return withResponse ? typedResponse : data; }); - return promise as Extract, { data: {} }>["data"]; + return promise as Extract< + InferResponseByStatus, + { data: {} } + >["data"]; } // } diff --git a/src/api/posthogClient.ts b/src/api/posthogClient.ts index 164c59c80..f0b75edc7 100644 --- a/src/api/posthogClient.ts +++ b/src/api/posthogClient.ts @@ -1,137 +1,142 @@ - -import { buildApiFetcher } from './fetcher'; -import { createApiClient, type Schemas } from './generated'; +import { buildApiFetcher } from "./fetcher"; +import { createApiClient, type Schemas } from "./generated"; export class PostHogAPIClient { private api: ReturnType; private _teamId: number | null = null; constructor(apiKey: string, apiHost: string) { - const baseUrl = apiHost.endsWith('/') ? apiHost.slice(0, -1) : apiHost; - this.api = createApiClient( - buildApiFetcher({ apiToken: apiKey }), - baseUrl - ); + const baseUrl = apiHost.endsWith("/") ? apiHost.slice(0, -1) : apiHost; + this.api = createApiClient(buildApiFetcher({ apiToken: apiKey }), baseUrl); } - + private async getTeamId(): Promise { if (this._teamId !== null) { return this._teamId; } - + const user = await this.api.get("/api/users/{uuid}/", { - path: {uuid: "@me"} + path: { uuid: "@me" }, }); - + if (user?.team?.id) { this._teamId = user.team.id; return this._teamId; } - - throw new Error('No team found for user'); + + throw new Error("No team found for user"); } - + async getCurrentUser() { - const data = await this.api.get('/api/users/{uuid}/', { - path: {uuid: "@me"} + const data = await this.api.get("/api/users/{uuid}/", { + path: { uuid: "@me" }, }); return data; } - + async getTasks(repositoryOrg?: string, repositoryName?: string) { const teamId = await this.getTeamId(); const params: Record = {}; - + if (repositoryOrg && repositoryName) { - params['repository_config__organization'] = repositoryOrg; - params['repository_config__repository'] = repositoryName; + params.repository_config__organization = repositoryOrg; + params.repository_config__repository = repositoryName; } - + const data = await this.api.get(`/api/projects/{project_id}/tasks/`, { - path: {project_id: teamId.toString()}, - query: params + path: { project_id: teamId.toString() }, + query: params, }); - - return data.results ?? [] + + return data.results ?? []; } - + async getTask(taskId: string) { const teamId = await this.getTeamId(); const data = await this.api.get(`/api/projects/{project_id}/tasks/{id}/`, { - path: {project_id: teamId.toString(), id: taskId} + path: { project_id: teamId.toString(), id: taskId }, }); return data; } - + async createTask( title: string, description: string, - repositoryConfig?: { organization: string; repository: string } + repositoryConfig?: { organization: string; repository: string }, ) { const teamId = await this.getTeamId(); const payload = { title, description, - origin_product: 'user_created' as const, + origin_product: "user_created" as const, ...(repositoryConfig && { repository_config: repositoryConfig }), }; const data = await this.api.post(`/api/projects/{project_id}/tasks/`, { - path: {project_id: teamId.toString()}, - body: payload as Schemas.Task + path: { project_id: teamId.toString() }, + body: payload as Schemas.Task, }); return data; } - + async updateTask(taskId: string, updates: Partial) { const teamId = await this.getTeamId(); - const data = await this.api.patch(`/api/projects/{project_id}/tasks/{id}/`, { - path: {project_id: teamId.toString(), id: taskId}, - body: updates - }); + const data = await this.api.patch( + `/api/projects/{project_id}/tasks/{id}/`, + { + path: { project_id: teamId.toString(), id: taskId }, + body: updates, + }, + ); return data; } - + async runTask(taskId: string) { // TODO: Pull this out and handle local and API calls const teamId = await this.getTeamId(); const data = await this.api.patch( `/api/projects/{project_id}/tasks/{id}/update_stage/`, - { path: {project_id: teamId.toString(), id: taskId}, body: { current_stage: "running" } } + { + path: { project_id: teamId.toString(), id: taskId }, + body: { current_stage: "running" }, + }, ); return data; } - + async getTaskLogs(taskId: string): Promise { const teamId = await this.getTeamId(); const data = await this.api.get( `/api/projects/{project_id}/tasks/{id}/progress/`, - { path: {project_id: teamId.toString(), id: taskId}} + { path: { project_id: teamId.toString(), id: taskId } }, ); return data.output_log ?? ""; } - + async getWorkflows() { const teamId = await this.getTeamId(); const data = await this.api.get(`/api/projects/{project_id}/workflows/`, { path: { project_id: teamId.toString() }, - query: { limit: 100, offset: 0 } + query: { limit: 100, offset: 0 }, }); - - return data.results ?? [] + + return data.results ?? []; } - + async getWorkflowStages(workflowId: string) { const teamId = await this.getTeamId(); - const data = await this.api.get(`/api/projects/{project_id}/workflows/{workflow_id}/stages/`, { - path: { project_id: teamId.toString(), workflow_id: workflowId }, - query: { limit: 100, offset: 0 } - }); - - return data.results ?? [] + const data = await this.api.get( + `/api/projects/{project_id}/workflows/{workflow_id}/stages/`, + { + path: { project_id: teamId.toString(), workflow_id: workflowId }, + query: { limit: 100, offset: 0 }, + }, + ); + + return data.results ?? []; } -} \ No newline at end of file +} diff --git a/src/api/repoDetector.ts b/src/api/repoDetector.ts index d60f79cab..c72df7950 100644 --- a/src/api/repoDetector.ts +++ b/src/api/repoDetector.ts @@ -1,5 +1,5 @@ -import { exec } from 'child_process'; -import { promisify } from 'util'; +import { exec } from "node:child_process"; +import { promisify } from "node:util"; const execAsync = promisify(exec); @@ -14,32 +14,36 @@ export class RepoDetector { async getCurrentRepo(): Promise { try { // Get remote URL - const { stdout: remoteUrl } = await execAsync('git remote get-url origin'); + const { stdout: remoteUrl } = await execAsync( + "git remote get-url origin", + ); const cleanUrl = remoteUrl.trim(); - + // Parse GitHub URL let match = cleanUrl.match(/github\.com[:/](.+?)\/(.+?)(\.git)?$/); if (!match) { // Try SSH format match = cleanUrl.match(/git@github\.com:(.+?)\/(.+?)(\.git)?$/); } - + if (!match) { return null; } - + const organization = match[1]; - const repository = match[2].replace(/\.git$/, ''); - + const repository = match[2].replace(/\.git$/, ""); + // Get current branch let branch: string | undefined; try { - const { stdout: branchName } = await execAsync('git branch --show-current'); + const { stdout: branchName } = await execAsync( + "git branch --show-current", + ); branch = branchName.trim(); } catch { // Ignore branch detection errors } - + return { organization, repository, @@ -50,4 +54,4 @@ export class RepoDetector { return null; } } -} \ No newline at end of file +} diff --git a/src/main/index.ts b/src/main/index.ts index c9c923bac..6857934c7 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,17 +1,23 @@ -import { app, BrowserWindow, Menu, type MenuItemConstructorOptions } from 'electron'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { registerPosthogIpc } from './services/posthog.js'; -import { registerOsIpc } from './services/os.js'; -import { registerAgentIpc } from './services/agent.js'; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + app, + BrowserWindow, + Menu, + type MenuItemConstructorOptions, +} from "electron"; +import { registerAgentIpc, type TaskController } from "./services/agent.js"; +import { registerOsIpc } from "./services/os.js"; +import { registerPosthogIpc } from "./services/posthog.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const isDev = process.env.NODE_ENV === 'development' || process.argv.includes('--dev'); +const isDev = + process.env.NODE_ENV === "development" || process.argv.includes("--dev"); let mainWindow: BrowserWindow | null = null; -const taskControllers = new Map(); +const taskControllers = new Map(); function createWindow(): void { mainWindow = new BrowserWindow({ @@ -19,56 +25,51 @@ function createWindow(): void { height: 600, minWidth: 900, minHeight: 600, - backgroundColor: '#0a0a0a', - titleBarStyle: 'hiddenInset', - show: false, + backgroundColor: "#0a0a0a", + titleBarStyle: "hiddenInset", + show: false, webPreferences: { nodeIntegration: false, contextIsolation: true, - preload: path.join(__dirname, 'preload.js'), + preload: path.join(__dirname, "preload.js"), }, }); - mainWindow.once('ready-to-show', () => { + mainWindow.once("ready-to-show", () => { mainWindow?.maximize(); mainWindow?.show(); }); - // Set up menu for keyboard shortcuts const template: MenuItemConstructorOptions[] = [ { - label: 'Array', - submenu: [ - { role: 'about' }, - { type: 'separator' }, - { role: 'quit' }, - ], + label: "Array", + submenu: [{ role: "about" }, { type: "separator" }, { role: "quit" }], }, { - label: 'Edit', + label: "Edit", submenu: [ - { role: 'undo' }, - { role: 'redo' }, - { type: 'separator' }, - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, - { role: 'selectAll' }, + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "selectAll" }, ], }, { - label: 'View', + label: "View", submenu: [ - { role: 'reload' }, - { role: 'forceReload' }, - { role: 'toggleDevTools' }, - { type: 'separator' }, - { role: 'resetZoom' }, - { role: 'zoomIn' }, - { role: 'zoomOut' }, - { type: 'separator' }, - { role: 'togglefullscreen' }, + { role: "reload" }, + { role: "forceReload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, ], }, ]; @@ -77,25 +78,25 @@ function createWindow(): void { Menu.setApplicationMenu(menu); if (isDev) { - mainWindow.loadURL('http://localhost:5173'); + mainWindow.loadURL("http://localhost:5173"); } else { - mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')); + mainWindow.loadFile(path.join(__dirname, "../renderer/index.html")); } - mainWindow.on('closed', () => { + mainWindow.on("closed", () => { mainWindow = null; }); } app.whenReady().then(createWindow); -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { +app.on("window-all-closed", () => { + if (process.platform !== "darwin") { app.quit(); } }); -app.on('activate', () => { +app.on("activate", () => { if (mainWindow === null) { createWindow(); } @@ -104,4 +105,4 @@ app.on('activate', () => { // Register IPC handlers via services registerPosthogIpc(); registerOsIpc(() => mainWindow); -registerAgentIpc(taskControllers, () => mainWindow); \ No newline at end of file +registerAgentIpc(taskControllers, () => mainWindow); diff --git a/src/main/package.json b/src/main/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/src/main/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/src/main/preload.ts b/src/main/preload.ts index 79cb56fcf..15894e70c 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -1,7 +1,7 @@ -import { contextBridge, ipcRenderer, type IpcRendererEvent } from 'electron'; +import { contextBridge, type IpcRendererEvent, ipcRenderer } from "electron"; interface MessageBoxOptions { - type?: 'info' | 'error' | 'warning' | 'question'; + type?: "info" | "error" | "warning" | "question"; title?: string; message?: string; detail?: string; @@ -16,18 +16,32 @@ interface AgentStartParams { model?: string; } -contextBridge.exposeInMainWorld('electronAPI', { - storeApiKey: (apiKey: string): Promise => ipcRenderer.invoke('store-api-key', apiKey), - retrieveApiKey: (encryptedKey: string): Promise => ipcRenderer.invoke('retrieve-api-key', encryptedKey), - selectDirectory: (): Promise => ipcRenderer.invoke('select-directory'), - validateRepo: (directoryPath: string): Promise => ipcRenderer.invoke('validate-repo', directoryPath), - checkWriteAccess: (directoryPath: string): Promise => ipcRenderer.invoke('check-write-access', directoryPath), - showMessageBox: (options: MessageBoxOptions): Promise<{ response: number }> => ipcRenderer.invoke('show-message-box', options), - agentStart: async (params: AgentStartParams): Promise<{ taskId: string; channel: string }> => ipcRenderer.invoke('agent-start', params), - agentCancel: async (taskId: string): Promise => ipcRenderer.invoke('agent-cancel', taskId), - onAgentEvent: (channel: string, listener: (payload: any) => void): (() => void) => { - const wrapped = (_event: IpcRendererEvent, payload: any) => listener(payload); +contextBridge.exposeInMainWorld("electronAPI", { + storeApiKey: (apiKey: string): Promise => + ipcRenderer.invoke("store-api-key", apiKey), + retrieveApiKey: (encryptedKey: string): Promise => + ipcRenderer.invoke("retrieve-api-key", encryptedKey), + selectDirectory: (): Promise => + ipcRenderer.invoke("select-directory"), + validateRepo: (directoryPath: string): Promise => + ipcRenderer.invoke("validate-repo", directoryPath), + checkWriteAccess: (directoryPath: string): Promise => + ipcRenderer.invoke("check-write-access", directoryPath), + showMessageBox: (options: MessageBoxOptions): Promise<{ response: number }> => + ipcRenderer.invoke("show-message-box", options), + agentStart: async ( + params: AgentStartParams, + ): Promise<{ taskId: string; channel: string }> => + ipcRenderer.invoke("agent-start", params), + agentCancel: async (taskId: string): Promise => + ipcRenderer.invoke("agent-cancel", taskId), + onAgentEvent: ( + channel: string, + listener: (payload: unknown) => void, + ): (() => void) => { + const wrapped = (_event: IpcRendererEvent, payload: unknown) => + listener(payload); ipcRenderer.on(channel, wrapped); return () => ipcRenderer.removeListener(channel, wrapped); }, -}); \ No newline at end of file +}); diff --git a/src/main/services/agent.ts b/src/main/services/agent.ts index 68cb737f9..69e107350 100644 --- a/src/main/services/agent.ts +++ b/src/main/services/agent.ts @@ -1,36 +1,40 @@ -import { ipcMain, BrowserWindow, type IpcMainInvokeEvent } from 'electron'; -import { getCurrentBranch } from './git.js'; -import { createAgent, ClaudeCodeAgent } from '@posthog/code-agent'; +import { type Agent, ClaudeCodeAgent, createAgent } from "@posthog/code-agent"; +import { type BrowserWindow, type IpcMainInvokeEvent, ipcMain } from "electron"; +import { getCurrentBranch } from "./git.js"; interface AgentStartParams { - prompt: string; - repoPath: string; - model?: string; + prompt: string; + repoPath: string; + model?: string; } -interface TaskController { - abortController: AbortController; - agent: any; - channel: string; +export interface TaskController { + abortController: AbortController; + agent: Agent; + channel: string; } -export function registerAgentIpc(taskControllers: Map, getMainWindow: () => BrowserWindow | null): void { - ipcMain.handle('agent-start', async (_event: IpcMainInvokeEvent, { prompt, repoPath }: AgentStartParams): Promise<{ taskId: string; channel: string }> => { - if (!prompt || !repoPath) { - throw new Error('prompt and repoPath are required'); - } +export function registerAgentIpc( + taskControllers: Map, + getMainWindow: () => BrowserWindow | null, +): void { + ipcMain.handle( + "agent-start", + async ( + _event: IpcMainInvokeEvent, + { prompt, repoPath }: AgentStartParams, + ): Promise<{ taskId: string; channel: string }> => { + if (!prompt || !repoPath) { + throw new Error("prompt and repoPath are required"); + } - const agent = createAgent( - new ClaudeCodeAgent({ - permissionMode: 'bypassPermissions' - } as any) - ); + const agent = createAgent(new ClaudeCodeAgent()); - const abortController = new AbortController(); + const abortController = new AbortController(); - const currentBranch = (await getCurrentBranch(repoPath)) || 'unknown'; + const currentBranch = (await getCurrentBranch(repoPath)) || "unknown"; - const fullPrompt = ` + const fullPrompt = ` Repository: ${repoPath} Branch: ${currentBranch} @@ -92,59 +96,63 @@ export function registerAgentIpc(taskControllers: Map, g `; - const { taskId, stream } = await agent.run({ - prompt: fullPrompt, - repoPath, - permissionMode: 'permissive', - }); - - const channel = `agent-event:${taskId}`; - - taskControllers.set(taskId, { abortController, agent, channel }); - - // Forward streaming events to renderer - (async () => { - try { - for await (const ev of stream) { - const win = getMainWindow && getMainWindow(); - if (win && !win.isDestroyed()) { - win.webContents.send(channel, ev); - } - } - const win = getMainWindow && getMainWindow(); - if (win && !win.isDestroyed()) { - win.webContents.send(channel, { type: 'done', success: true }); - } - } catch (err) { - const win = getMainWindow && getMainWindow(); - if (win && !win.isDestroyed()) { - win.webContents.send(channel, { - type: 'error', - message: err instanceof Error ? err.message : String(err), - }); - win.webContents.send(channel, { type: 'done', success: false }); - } - } finally { - taskControllers.delete(taskId); - } - })(); + const { taskId, stream } = await agent.run({ + prompt: fullPrompt, + repoPath, + permissionMode: "permissive", + }); + + const channel = `agent-event:${taskId}`; - return { taskId, channel }; - }); + taskControllers.set(taskId, { abortController, agent, channel }); - ipcMain.handle('agent-cancel', async (_event: IpcMainInvokeEvent, taskId: string): Promise => { - const entry = taskControllers.get(taskId); - if (!entry) return false; + // Forward streaming events to renderer + (async () => { try { - entry.abortController.abort(); - if (entry.agent && typeof entry.agent.cancel === 'function') { - try { - await entry.agent.cancel(taskId); - } catch { } + for await (const ev of stream) { + const win = getMainWindow?.(); + if (win && !win.isDestroyed()) { + win.webContents.send(channel, ev); } - return true; + } + const win = getMainWindow?.(); + if (win && !win.isDestroyed()) { + win.webContents.send(channel, { type: "done", success: true }); + } + } catch (err) { + const win = getMainWindow?.(); + if (win && !win.isDestroyed()) { + win.webContents.send(channel, { + type: "error", + message: err instanceof Error ? err.message : String(err), + }); + win.webContents.send(channel, { type: "done", success: false }); + } } finally { - taskControllers.delete(taskId); + taskControllers.delete(taskId); } - }); -} \ No newline at end of file + })(); + + return { taskId, channel }; + }, + ); + + ipcMain.handle( + "agent-cancel", + async (_event: IpcMainInvokeEvent, taskId: string): Promise => { + const entry = taskControllers.get(taskId); + if (!entry) return false; + try { + entry.abortController.abort(); + if (entry.agent && typeof entry.agent.cancel === "function") { + try { + await entry.agent.cancel(taskId); + } catch {} + } + return true; + } finally { + taskControllers.delete(taskId); + } + }, + ); +} diff --git a/src/main/services/git.ts b/src/main/services/git.ts index 553796d1c..b7a7397d3 100644 --- a/src/main/services/git.ts +++ b/src/main/services/git.ts @@ -1,13 +1,15 @@ -import { exec } from 'child_process'; -import { promisify } from 'util'; +import { exec } from "node:child_process"; +import { promisify } from "node:util"; const execAsync = promisify(exec); export async function getCurrentBranch(cwd: string): Promise { try { - const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd }); + const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", { + cwd, + }); return stdout.trim(); } catch { return null; } -} \ No newline at end of file +} diff --git a/src/main/services/github.ts b/src/main/services/github.ts index dd1827819..b9164ca74 100644 --- a/src/main/services/github.ts +++ b/src/main/services/github.ts @@ -6,5 +6,5 @@ export async function listBranches(): Promise { } export async function createPullRequest(): Promise<{ url: string }> { - return { url: '' }; -} \ No newline at end of file + return { url: "" }; +} diff --git a/src/main/services/os.ts b/src/main/services/os.ts index c67cefb71..fa8d782c9 100644 --- a/src/main/services/os.ts +++ b/src/main/services/os.ts @@ -1,14 +1,19 @@ -import { ipcMain, dialog, BrowserWindow, type IpcMainInvokeEvent } from 'electron'; -import path from 'path'; -import fs from 'fs'; -import { promisify } from 'util'; -import { exec } from 'child_process'; +import { exec } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { promisify } from "node:util"; +import { + type BrowserWindow, + dialog, + type IpcMainInvokeEvent, + ipcMain, +} from "electron"; const fsPromises = fs.promises; const execAsync = promisify(exec); interface MessageBoxOptionsCustom { - type?: 'info' | 'error' | 'warning' | 'question'; + type?: "info" | "error" | "warning" | "question"; title?: string; message?: string; detail?: string; @@ -18,13 +23,17 @@ interface MessageBoxOptionsCustom { } export function registerOsIpc(getMainWindow: () => BrowserWindow | null): void { - ipcMain.handle('select-directory', async (): Promise => { + ipcMain.handle("select-directory", async (): Promise => { const win = getMainWindow(); if (!win) return null; const result = await dialog.showOpenDialog(win, { - title: 'Select a repository folder', - properties: ['openDirectory', 'createDirectory', 'treatPackageAsDirectory'], + title: "Select a repository folder", + properties: [ + "openDirectory", + "createDirectory", + "treatPackageAsDirectory", + ], }); if (result.canceled || !result.filePaths?.length) { return null; @@ -32,42 +41,68 @@ export function registerOsIpc(getMainWindow: () => BrowserWindow | null): void { return result.filePaths[0]; }); - ipcMain.handle('validate-repo', async (_event: IpcMainInvokeEvent, directoryPath: string): Promise => { - if (!directoryPath) return false; - try { - await execAsync('git rev-parse --is-inside-work-tree', { cwd: directoryPath }); - return true; - } catch { - return false; - } - }); + ipcMain.handle( + "validate-repo", + async ( + _event: IpcMainInvokeEvent, + directoryPath: string, + ): Promise => { + if (!directoryPath) return false; + try { + await execAsync("git rev-parse --is-inside-work-tree", { + cwd: directoryPath, + }); + return true; + } catch { + return false; + } + }, + ); - ipcMain.handle('check-write-access', async (_event: IpcMainInvokeEvent, directoryPath: string): Promise => { - if (!directoryPath) return false; - try { - await fsPromises.access(directoryPath, fs.constants.W_OK); - const testFile = path.join(directoryPath, `.agent-write-test-${Date.now()}`); - await fsPromises.writeFile(testFile, 'ok'); - await fsPromises.unlink(testFile).catch(() => {}); - return true; - } catch { - return false; - } - }); + ipcMain.handle( + "check-write-access", + async ( + _event: IpcMainInvokeEvent, + directoryPath: string, + ): Promise => { + if (!directoryPath) return false; + try { + await fsPromises.access(directoryPath, fs.constants.W_OK); + const testFile = path.join( + directoryPath, + `.agent-write-test-${Date.now()}`, + ); + await fsPromises.writeFile(testFile, "ok"); + await fsPromises.unlink(testFile).catch(() => {}); + return true; + } catch { + return false; + } + }, + ); - ipcMain.handle('show-message-box', async (_event: IpcMainInvokeEvent, options: MessageBoxOptionsCustom): Promise<{ response: number }> => { - const win = getMainWindow(); - if (!win) throw new Error('Main window not available'); + ipcMain.handle( + "show-message-box", + async ( + _event: IpcMainInvokeEvent, + options: MessageBoxOptionsCustom, + ): Promise<{ response: number }> => { + const win = getMainWindow(); + if (!win) throw new Error("Main window not available"); - const result = await dialog.showMessageBox(win, { - type: options?.type || 'info', - title: options?.title || 'Array', - message: options?.message || '', - detail: options?.detail, - buttons: Array.isArray(options?.buttons) && options.buttons.length > 0 ? options.buttons : ['OK'], - defaultId: options?.defaultId ?? 0, - cancelId: options?.cancelId ?? 1, - }); - return { response: result.response }; - }); -} \ No newline at end of file + const result = await dialog.showMessageBox(win, { + type: options?.type || "info", + title: options?.title || "Array", + message: options?.message || "", + detail: options?.detail, + buttons: + Array.isArray(options?.buttons) && options.buttons.length > 0 + ? options.buttons + : ["OK"], + defaultId: options?.defaultId ?? 0, + cancelId: options?.cancelId ?? 1, + }); + return { response: result.response }; + }, + ); +} diff --git a/src/main/services/posthog.ts b/src/main/services/posthog.ts index fc587e2f9..635c6eff6 100644 --- a/src/main/services/posthog.ts +++ b/src/main/services/posthog.ts @@ -1,24 +1,33 @@ -import { ipcMain, safeStorage, type IpcMainInvokeEvent } from 'electron'; +import { type IpcMainInvokeEvent, ipcMain, safeStorage } from "electron"; export function registerPosthogIpc(): void { // IPC handlers for secure storage - ipcMain.handle('store-api-key', async (_event: IpcMainInvokeEvent, apiKey: string): Promise => { - if (safeStorage.isEncryptionAvailable()) { - const encrypted = safeStorage.encryptString(apiKey); - return encrypted.toString('base64'); - } - return apiKey; - }); + ipcMain.handle( + "store-api-key", + async (_event: IpcMainInvokeEvent, apiKey: string): Promise => { + if (safeStorage.isEncryptionAvailable()) { + const encrypted = safeStorage.encryptString(apiKey); + return encrypted.toString("base64"); + } + return apiKey; + }, + ); - ipcMain.handle('retrieve-api-key', async (_event: IpcMainInvokeEvent, encryptedKey: string): Promise => { - if (safeStorage.isEncryptionAvailable()) { - try { - const buffer = Buffer.from(encryptedKey, 'base64'); - return safeStorage.decryptString(buffer); - } catch { - return null; + ipcMain.handle( + "retrieve-api-key", + async ( + _event: IpcMainInvokeEvent, + encryptedKey: string, + ): Promise => { + if (safeStorage.isEncryptionAvailable()) { + try { + const buffer = Buffer.from(encryptedKey, "base64"); + return safeStorage.decryptString(buffer); + } catch { + return null; + } } - } - return encryptedKey; - }); -} \ No newline at end of file + return encryptedKey; + }, + ); +} diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 77ea69a1c..2211ca006 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useState } from 'react'; -import { Flex, Text, Spinner } from '@radix-ui/themes'; -import { AuthScreen } from './components/AuthScreen'; -import { MainLayout } from './components/MainLayout'; -import { useAuthStore } from './stores/authStore'; +import { Flex, Spinner, Text } from "@radix-ui/themes"; +import { useEffect, useState } from "react"; +import { AuthScreen } from "./components/AuthScreen"; +import { MainLayout } from "./components/MainLayout"; +import { useAuthStore } from "./stores/authStore"; function App() { const { isAuthenticated, checkAuth } = useAuthStore(); @@ -26,4 +26,4 @@ function App() { return isAuthenticated ? : ; } -export default App; \ No newline at end of file +export default App; diff --git a/src/renderer/components/AsciiArt.tsx b/src/renderer/components/AsciiArt.tsx index 0d4226ac7..d25ec40e3 100644 --- a/src/renderer/components/AsciiArt.tsx +++ b/src/renderer/components/AsciiArt.tsx @@ -1,6 +1,5 @@ -import React from 'react'; -import { Flex, Code } from '@radix-ui/themes'; -import artText from '@shared/art.txt?raw'; +import { Code, Flex } from "@radix-ui/themes"; +import artText from "@shared/art.txt?raw"; interface AsciiArtProps { scale?: number; @@ -9,27 +8,33 @@ interface AsciiArtProps { export function AsciiArt({ scale = 0.6, opacity = 0.2 }: AsciiArtProps) { return ( - + {artText} ); -} \ No newline at end of file +} diff --git a/src/renderer/components/AuthScreen.tsx b/src/renderer/components/AuthScreen.tsx index dbb9ba73c..f3ece7485 100644 --- a/src/renderer/components/AuthScreen.tsx +++ b/src/renderer/components/AuthScreen.tsx @@ -1,25 +1,38 @@ -import React, { useState } from 'react'; -import { Container, Flex, Card, Heading, Text, TextField, Button, Callout, Box } from '@radix-ui/themes'; -import { useAuthStore } from '../stores/authStore'; -import { AsciiArt } from './AsciiArt'; +import { + Box, + Button, + Callout, + Card, + Container, + Flex, + Heading, + Text, + TextField, +} from "@radix-ui/themes"; +import type React from "react"; +import { useId, useState } from "react"; +import { useAuthStore } from "../stores/authStore"; +import { AsciiArt } from "./AsciiArt"; export function AuthScreen() { - const [apiKey, setApiKey] = useState(''); - const [apiHost, setApiHost] = useState('https://app.posthog.com'); - const [error, setError] = useState(''); + const apiKeyId = useId(); + const apiHostId = useId(); + const [apiKey, setApiKey] = useState(""); + const [apiHost, setApiHost] = useState("https://app.posthog.com"); + const [error, setError] = useState(""); const [isLoading, setIsLoading] = useState(false); const { setCredentials } = useAuthStore(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - setError(''); + setError(""); setIsLoading(true); try { await setCredentials(apiKey, apiHost); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to authenticate'); + setError(err instanceof Error ? err.message : "Failed to authenticate"); } finally { setIsLoading(false); } @@ -28,9 +41,14 @@ export function AuthScreen() { return ( {/* Left pane - Auth form */} - + - + @@ -41,11 +59,17 @@ export function AuthScreen() { - + Personal API Key setApiKey(e.target.value)} @@ -58,11 +82,17 @@ export function AuthScreen() { - + PostHog Instance URL setApiHost(e.target.value)} @@ -81,11 +111,11 @@ export function AuthScreen() { @@ -101,4 +131,4 @@ export function AuthScreen() { ); -} \ No newline at end of file +} diff --git a/src/renderer/components/LogView.tsx b/src/renderer/components/LogView.tsx index 3e3e9bdb1..d872a3353 100644 --- a/src/renderer/components/LogView.tsx +++ b/src/renderer/components/LogView.tsx @@ -1,10 +1,10 @@ -import React, { useEffect, useRef } from 'react'; -import { Flex, Box, Heading, Text, Code, IconButton } from '@radix-ui/themes'; -import { TrashIcon } from '@radix-ui/react-icons'; -import { LogEntry, formatTime } from '../types/log'; -import { ToolCallView } from './log/ToolCallView'; -import { DiffView } from './log/DiffView'; -import { MetricView } from './log/MetricView'; +import { TrashIcon } from "@radix-ui/react-icons"; +import { Box, Code, Flex, Heading, IconButton, Text } from "@radix-ui/themes"; +import { useEffect, useRef } from "react"; +import { formatTime, type LogEntry } from "../types/log"; +import { DiffView } from "./log/DiffView"; +import { MetricView } from "./log/MetricView"; +import { ToolCallView } from "./log/ToolCallView"; interface LogViewProps { logs: Array; @@ -20,11 +20,17 @@ export function LogView({ logs, isRunning, onClearLogs }: LogViewProps) { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } - }, [logs]); + }, []); if (logs.length === 0 && !isRunning) { return ( - + No activity yet @@ -34,7 +40,7 @@ export function LogView({ logs, isRunning, onClearLogs }: LogViewProps) { return ( - + Activity Log @@ -43,9 +49,11 @@ export function LogView({ logs, isRunning, onClearLogs }: LogViewProps) { - Running + + Running + )} {logs.length > 0 && onClearLogs && ( @@ -63,120 +71,153 @@ export function LogView({ logs, isRunning, onClearLogs }: LogViewProps) { - + {logs.map((log, index) => { + const key = + typeof log === "string" + ? `str-${index}` + : `${log.type}-${log.ts}-${index}`; + // Backward compat for plain strings - if (typeof log === 'string') { + if (typeof log === "string") { return ( - + {new Date().toLocaleTimeString()} - {log} + + {log} + ); } // Structured entries switch (log.type) { - case 'text': + case "text": return ( - + {formatTime(log.ts)} - + {log.content} ); - case 'status': + case "status": return ( - + {formatTime(log.ts)} - status: {log.phase} + + status: {log.phase} + ); - case 'tool_call': + case "tool_call": return ( - + {formatTime(log.ts)} tool_call - + ); - case 'tool_result': + case "tool_result": return ( - + {formatTime(log.ts)} tool_result - + ); - case 'diff': + case "diff": return ( - + {formatTime(log.ts)} diff - + ); - case 'file_write': + case "file_write": return ( - + {formatTime(log.ts)} file_write: {log.path} - {typeof log.bytes === 'number' && ( - ({log.bytes} bytes) + {typeof log.bytes === "number" && ( + + {" "} + ({log.bytes} bytes) + )} ); - case 'metric': + case "metric": return ( - + {formatTime(log.ts)} metric - + ); - case 'artifact': + case "artifact": return ( - + {formatTime(log.ts)} - artifact + + artifact + ); default: return ( - + {new Date().toLocaleTimeString()} - {JSON.stringify(log)} + + {JSON.stringify(log)} + ); } @@ -184,4 +225,4 @@ export function LogView({ logs, isRunning, onClearLogs }: LogViewProps) { ); -} \ No newline at end of file +} diff --git a/src/renderer/components/MainLayout.tsx b/src/renderer/components/MainLayout.tsx index 9eb71d533..8adfe35ff 100644 --- a/src/renderer/components/MainLayout.tsx +++ b/src/renderer/components/MainLayout.tsx @@ -1,66 +1,74 @@ -import React, { useState } from 'react'; -import { Flex, Box } from '@radix-ui/themes'; -import { TaskList } from './TaskList'; -import { TaskDetail } from './TaskDetail'; -import { WorkflowView } from './WorkflowView'; -import { TabBar } from './TabBar'; -import { StatusBar } from './StatusBar'; -import { CommandMenu } from './command'; -import { useTabStore } from '../stores/tabStore'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { Task } from '@shared/types'; +import { Box, Flex } from "@radix-ui/themes"; +import type { Task } from "@shared/types"; +import { useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; +import { useTabStore } from "../stores/tabStore"; +import { CommandMenu } from "./command"; +import { StatusBar } from "./StatusBar"; +import { TabBar } from "./TabBar"; +import { TaskDetail } from "./TaskDetail"; +import { TaskList } from "./TaskList"; +import { WorkflowView } from "./WorkflowView"; export function MainLayout() { const { activeTabId, tabs, createTab, setActiveTab } = useTabStore(); const [commandMenuOpen, setCommandMenuOpen] = useState(false); - useHotkeys('mod+k', () => setCommandMenuOpen(prev => !prev), { enabled: !commandMenuOpen }); - useHotkeys('mod+t', () => setCommandMenuOpen(prev => !prev), { enabled: !commandMenuOpen }); - useHotkeys('mod+p', () => setCommandMenuOpen(prev => !prev), { enabled: !commandMenuOpen }); + useHotkeys("mod+k", () => setCommandMenuOpen((prev) => !prev), { + enabled: !commandMenuOpen, + }); + useHotkeys("mod+t", () => setCommandMenuOpen((prev) => !prev), { + enabled: !commandMenuOpen, + }); + useHotkeys("mod+p", () => setCommandMenuOpen((prev) => !prev), { + enabled: !commandMenuOpen, + }); const handleSelectTask = (task: Task) => { // Check if task is already open in a tab - const existingTab = tabs.find(tab => - tab.type === 'task-detail' && tab.data?.id === task.id + const existingTab = tabs.find( + (tab) => + tab.type === "task-detail" && + tab.data && + typeof tab.data === "object" && + "id" in tab.data && + tab.data.id === task.id, ); if (existingTab) { setActiveTab(existingTab.id); } else { createTab({ - type: 'task-detail', + type: "task-detail", title: task.title, data: task, }); } }; - const activeTab = tabs.find(tab => tab.id === activeTabId); + const activeTab = tabs.find((tab) => tab.id === activeTabId); return ( - {activeTab?.type === 'task-list' && ( + {activeTab?.type === "task-list" && ( )} - {activeTab?.type === 'task-detail' && activeTab.data && ( - - )} + {activeTab?.type === "task-detail" && activeTab.data ? ( + + ) : null} - {activeTab?.type === 'workflow' && ( + {activeTab?.type === "workflow" && ( )} - + ); -} \ No newline at end of file +} diff --git a/src/renderer/components/StatusBar.tsx b/src/renderer/components/StatusBar.tsx index d55c24bc9..87bc4f3e0 100644 --- a/src/renderer/components/StatusBar.tsx +++ b/src/renderer/components/StatusBar.tsx @@ -1,7 +1,6 @@ -import React from 'react'; -import { Flex, Kbd, Badge, Code, Box } from '@radix-ui/themes'; -import { useStatusBarStore } from '../stores/statusBarStore'; -import { StatusBarMenu } from './StatusBarMenu'; +import { Badge, Box, Code, Flex, Kbd } from "@radix-ui/themes"; +import { useStatusBarStore } from "../stores/statusBarStore"; +import { StatusBarMenu } from "./StatusBarMenu"; interface StatusBarProps { showKeyHints?: boolean; @@ -11,17 +10,16 @@ export function StatusBar({ showKeyHints = true }: StatusBarProps) { const { statusText, keyHints } = useStatusBarStore(); // Determine if we're in development mode - const isDev = process.env.NODE_ENV === 'development'; - const version = '0.1.0'; // You can get this from package.json or env vars + const isDev = import.meta.env.DEV; + const version = "0.1.0"; // You can get this from package.json or env vars return ( - + - {statusText && '- '}{statusText} + {statusText && "- "} + {statusText} @@ -30,30 +28,34 @@ export function StatusBar({ showKeyHints = true }: StatusBarProps) { align="center" gap="3" style={{ - position: 'absolute', - left: '50%', - transform: 'translateX(-50%)' + position: "absolute", + left: "50%", + transform: "translateX(-50%)", }} > - {keyHints.map((hint, index) => ( - - - {hint.keys.join('')} - - {hint.description} + {keyHints.map((hint) => ( + + {hint.keys.join("")} + + {hint.description} + ))} )} - - {isDev ? 'DEV' : 'PROD'} + + + {isDev ? "DEV" : "PROD"} + - v{version} + + v{version} + ); -} \ No newline at end of file +} diff --git a/src/renderer/components/StatusBarMenu.tsx b/src/renderer/components/StatusBarMenu.tsx index ecd862022..3063053d3 100644 --- a/src/renderer/components/StatusBarMenu.tsx +++ b/src/renderer/components/StatusBarMenu.tsx @@ -1,8 +1,14 @@ -import React from 'react'; -import { Flex, Text, DropdownMenu, Switch, Code, Button } from '@radix-ui/themes'; -import { ExitIcon } from '@radix-ui/react-icons'; -import { useThemeStore } from '../stores/themeStore'; -import { useAuthStore } from '../stores/authStore'; +import { ExitIcon } from "@radix-ui/react-icons"; +import { + Button, + Code, + DropdownMenu, + Flex, + Switch, + Text, +} from "@radix-ui/themes"; +import { useAuthStore } from "../stores/authStore"; +import { useThemeStore } from "../stores/themeStore"; export function StatusBarMenu() { const isDarkMode = useThemeStore((state) => state.isDarkMode); @@ -12,21 +18,20 @@ export function StatusBarMenu() { return ( - - + { e.preventDefault(); toggleDarkMode(); }} - className='px-1' + className="px-1" > Dark Mode @@ -38,9 +43,9 @@ export function StatusBarMenu() { - + - + Logout @@ -49,4 +54,4 @@ export function StatusBarMenu() { ); -} \ No newline at end of file +} diff --git a/src/renderer/components/TabBar.tsx b/src/renderer/components/TabBar.tsx index 0647c517a..6f93333ee 100644 --- a/src/renderer/components/TabBar.tsx +++ b/src/renderer/components/TabBar.tsx @@ -1,99 +1,113 @@ -import React, { useCallback, useState } from 'react'; -import { Flex, Box, Text, IconButton, Kbd } from '@radix-ui/themes'; -import { Cross2Icon } from '@radix-ui/react-icons'; -import { useTabStore } from '../stores/tabStore'; -import { useHotkeys } from 'react-hotkeys-hook'; +import { Cross2Icon } from "@radix-ui/react-icons"; +import { Box, Flex, IconButton, Kbd, Text } from "@radix-ui/themes"; +import type React from "react"; +import { useCallback, useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; +import { useTabStore } from "../stores/tabStore"; export function TabBar() { - const { tabs, activeTabId, setActiveTab, closeTab, reorderTabs } = useTabStore(); + const { tabs, activeTabId, setActiveTab, closeTab, reorderTabs } = + useTabStore(); const [draggedTab, setDraggedTab] = useState(null); const [dragOverTab, setDragOverTab] = useState(null); - const [dropPosition, setDropPosition] = useState<'left' | 'right' | null>(null); + const [dropPosition, setDropPosition] = useState<"left" | "right" | null>( + null, + ); // Keyboard navigation handlers const handlePrevTab = useCallback(() => { - const currentIndex = tabs.findIndex(tab => tab.id === activeTabId); + const currentIndex = tabs.findIndex((tab) => tab.id === activeTabId); if (currentIndex > 0) { setActiveTab(tabs[currentIndex - 1].id); } }, [tabs, activeTabId, setActiveTab]); const handleNextTab = useCallback(() => { - const currentIndex = tabs.findIndex(tab => tab.id === activeTabId); + const currentIndex = tabs.findIndex((tab) => tab.id === activeTabId); if (currentIndex < tabs.length - 1) { setActiveTab(tabs[currentIndex + 1].id); } }, [tabs, activeTabId, setActiveTab]); const handleCloseTab = useCallback(() => { - console.log('Closing tab'); + console.log("Closing tab"); if (tabs.length > 1) { closeTab(activeTabId); } }, [tabs, activeTabId, closeTab]); // Tab switching by number handlers - const handleSwitchToTab = useCallback((index: number) => { - if (tabs[index]) { - setActiveTab(tabs[index].id); - } - }, [tabs, setActiveTab]); + const handleSwitchToTab = useCallback( + (index: number) => { + if (tabs[index]) { + setActiveTab(tabs[index].id); + } + }, + [tabs, setActiveTab], + ); const HOTKEYS = { - CTRL_PREV_TAB: 'ctrl+shift+[', - MOD_PREV_TAB: 'mod+shift+[', - CTRL_NEXT_TAB: 'ctrl+shift+]', - MOD_NEXT_TAB: 'mod+shift+]', - CTRL_CLOSE_TAB: 'ctrl+w', - MOD_CLOSE_TAB: 'mod+w', + CTRL_PREV_TAB: "ctrl+shift+[", + MOD_PREV_TAB: "mod+shift+[", + CTRL_NEXT_TAB: "ctrl+shift+]", + MOD_NEXT_TAB: "mod+shift+]", + CTRL_CLOSE_TAB: "ctrl+w", + MOD_CLOSE_TAB: "mod+w", // Cmd/Ctrl+1 through Cmd/Ctrl+9 ...Object.fromEntries( - Array.from({ length: 9 }, (_, i) => [`TAB_${i + 1}`, `mod+${i + 1}, ctrl+${i + 1}`]) + Array.from({ length: 9 }, (_, i) => [ + `TAB_${i + 1}`, + `mod+${i + 1}, ctrl+${i + 1}`, + ]), ), }; - useHotkeys(Object.values(HOTKEYS), (event, { hotkey }) => { - switch (hotkey) { - case HOTKEYS.CTRL_PREV_TAB: - case HOTKEYS.MOD_PREV_TAB: - handlePrevTab(); - break; - case HOTKEYS.CTRL_NEXT_TAB: - case HOTKEYS.MOD_NEXT_TAB: - handleNextTab(); - break; - case HOTKEYS.MOD_CLOSE_TAB: - case HOTKEYS.CTRL_CLOSE_TAB: - handleCloseTab(); - break; - default: { - // Check if it's a tab switching shortcut - const tabMatch = hotkey.match(/[1-9]/); - if (tabMatch) { - const tabIndex = parseInt(tabMatch[0], 10) - 1; - handleSwitchToTab(tabIndex); + useHotkeys( + Object.values(HOTKEYS), + (_event, { hotkey }) => { + switch (hotkey) { + case HOTKEYS.CTRL_PREV_TAB: + case HOTKEYS.MOD_PREV_TAB: + handlePrevTab(); + break; + case HOTKEYS.CTRL_NEXT_TAB: + case HOTKEYS.MOD_NEXT_TAB: + handleNextTab(); + break; + case HOTKEYS.MOD_CLOSE_TAB: + case HOTKEYS.CTRL_CLOSE_TAB: + handleCloseTab(); + break; + default: { + // Check if it's a tab switching shortcut + const tabMatch = hotkey.match(/[1-9]/); + if (tabMatch) { + const tabIndex = parseInt(tabMatch[0], 10) - 1; + handleSwitchToTab(tabIndex); + } + break; } - break; } - } - }, [handlePrevTab, handleNextTab, handleCloseTab, handleSwitchToTab]); + }, + [handlePrevTab, handleNextTab, handleCloseTab, handleSwitchToTab], + ); const handleDragStart = useCallback((e: React.DragEvent, tabId: string) => { setDraggedTab(tabId); - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text/plain', tabId); + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("text/plain", tabId); }, []); const handleDragOver = useCallback((e: React.DragEvent, tabId: string) => { e.preventDefault(); - e.dataTransfer.dropEffect = 'move'; + e.dataTransfer.dropEffect = "move"; const rect = e.currentTarget.getBoundingClientRect(); const midpoint = rect.left + rect.width / 2; const mouseX = e.clientX; setDragOverTab(tabId); - setDropPosition(mouseX < midpoint ? 'left' : 'right'); + setDropPosition(mouseX < midpoint ? "left" : "right"); }, []); const handleDragLeave = useCallback(() => { @@ -101,33 +115,36 @@ export function TabBar() { setDropPosition(null); }, []); - const handleDrop = useCallback((e: React.DragEvent, targetTabId: string) => { - e.preventDefault(); - const sourceTabId = e.dataTransfer.getData('text/plain'); + const handleDrop = useCallback( + (e: React.DragEvent, targetTabId: string) => { + e.preventDefault(); + const sourceTabId = e.dataTransfer.getData("text/plain"); - if (sourceTabId && sourceTabId !== targetTabId) { - const sourceIndex = tabs.findIndex(tab => tab.id === sourceTabId); - let targetIndex = tabs.findIndex(tab => tab.id === targetTabId); + if (sourceTabId && sourceTabId !== targetTabId) { + const sourceIndex = tabs.findIndex((tab) => tab.id === sourceTabId); + let targetIndex = tabs.findIndex((tab) => tab.id === targetTabId); - if (sourceIndex !== -1 && targetIndex !== -1) { - // Adjust target index based on drop position - if (dropPosition === 'right') { - targetIndex = targetIndex + 1; - } + if (sourceIndex !== -1 && targetIndex !== -1) { + // Adjust target index based on drop position + if (dropPosition === "right") { + targetIndex = targetIndex + 1; + } - // If moving to the right, adjust for the source being removed - if (sourceIndex < targetIndex) { - targetIndex = targetIndex - 1; - } + // If moving to the right, adjust for the source being removed + if (sourceIndex < targetIndex) { + targetIndex = targetIndex - 1; + } - reorderTabs(sourceIndex, targetIndex); + reorderTabs(sourceIndex, targetIndex); + } } - } - setDraggedTab(null); - setDragOverTab(null); - setDropPosition(null); - }, [tabs, reorderTabs, dropPosition]); + setDraggedTab(null); + setDragOverTab(null); + setDropPosition(null); + }, + [tabs, reorderTabs, dropPosition], + ); const handleDragEnd = useCallback(() => { setDraggedTab(null); @@ -136,23 +153,28 @@ export function TabBar() { }, []); return ( - + {/* Spacer for macOS window controls */} {tabs.map((tab, index) => { const isDragging = draggedTab === tab.id; const isDragOver = dragOverTab === tab.id; - const showLeftIndicator = isDragOver && dropPosition === 'left'; - const showRightIndicator = isDragOver && dropPosition === 'right'; + const showLeftIndicator = isDragOver && dropPosition === "left"; + const showRightIndicator = isDragOver && dropPosition === "right"; return ( {showLeftIndicator && ( )} {showRightIndicator && ( )} {index < 9 && ( - {navigator.platform.includes('Mac') ? '⌘' : 'Ctrl+'}{index + 1} + {navigator.platform.includes("Mac") ? "⌘" : "Ctrl+"} + {index + 1} )} {tab.title} @@ -194,8 +217,8 @@ export function TabBar() { { e.stopPropagation(); closeTab(tab.id); @@ -209,4 +232,4 @@ export function TabBar() { })} ); -} \ No newline at end of file +} diff --git a/src/renderer/components/TaskDetail.tsx b/src/renderer/components/TaskDetail.tsx index 238bb0276..d091b5a8f 100644 --- a/src/renderer/components/TaskDetail.tsx +++ b/src/renderer/components/TaskDetail.tsx @@ -1,12 +1,21 @@ -import React, { useEffect } from 'react'; -import { Flex, Box, Badge, Button, Link, SegmentedControl, DataList, Code } from '@radix-ui/themes'; -import { Pencil1Icon } from '@radix-ui/react-icons'; -import { Task } from '@shared/types'; -import { format } from 'date-fns'; -import { useStatusBarStore } from '../stores/statusBarStore'; -import { useTaskExecutionStore } from '../stores/taskExecutionStore'; -import { LogView } from './LogView'; -import { AsciiArt } from './AsciiArt'; +import { Pencil1Icon } from "@radix-ui/react-icons"; +import { + Badge, + Box, + Button, + Code, + DataList, + Flex, + Link, + SegmentedControl, +} from "@radix-ui/themes"; +import type { Task } from "@shared/types"; +import { format } from "date-fns"; +import { useEffect } from "react"; +import { useStatusBarStore } from "../stores/statusBarStore"; +import { useTaskExecutionStore } from "../stores/taskExecutionStore"; +import { AsciiArt } from "./AsciiArt"; +import { LogView } from "./LogView"; interface TaskDetailProps { task: Task; @@ -29,18 +38,18 @@ export function TaskDetail({ task }: TaskDetailProps) { useEffect(() => { setStatusBar({ - statusText: isRunning ? 'Agent running...' : 'Task details', + statusText: isRunning ? "Agent running..." : "Task details", keyHints: [ { - keys: [navigator.platform.includes('Mac') ? '⌘' : 'Ctrl', 'K'], - description: 'Command' + keys: [navigator.platform.includes("Mac") ? "⌘" : "Ctrl", "K"], + description: "Command", }, { - keys: [navigator.platform.includes('Mac') ? '⌘' : 'Ctrl', 'R'], - description: 'Refresh' - } + keys: [navigator.platform.includes("Mac") ? "⌘" : "Ctrl", "R"], + description: "Refresh", + }, ], - mode: 'replace' + mode: "replace", }); return () => { @@ -62,7 +71,7 @@ export function TaskDetail({ task }: TaskDetailProps) { }; const handleRunModeChange = (value: string) => { - setStoreRunMode(task.id, value as 'local' | 'cloud'); + setStoreRunMode(task.id, value as "local" | "cloud"); }; const handleClearLogs = () => { @@ -72,7 +81,7 @@ export function TaskDetail({ task }: TaskDetailProps) { return ( {/* Left pane - Task details */} - + {task.title} @@ -82,26 +91,28 @@ export function TaskDetail({ task }: TaskDetailProps) { Status - - {task.current_stage || 'Backlog'} - + {task.current_stage || "Backlog"} - {task.repository_config && ( + {task.repository_config && + typeof task.repository_config === "object" && + "organization" in task.repository_config && + "repository" in task.repository_config ? ( Remote Repository - {task.repository_config.organization}/{task.repository_config.repository} → + {String(task.repository_config.organization)}/ + {String(task.repository_config.repository)} → - )} + ) : null} {task.github_branch && ( @@ -124,14 +135,14 @@ export function TaskDetail({ task }: TaskDetailProps) { Created - {format(new Date(task.created_at), 'PPP p')} + {format(new Date(task.created_at), "PPP p")} Description - {task.description || 'No description provided'} + {task.description || "No description provided"} @@ -145,8 +156,10 @@ export function TaskDetail({ task }: TaskDetailProps) { onClick={handleSelectRepo} className="group cursor-pointer" > - {repoPath} - + + {repoPath} + + ) : ( ) : ( )} @@ -197,7 +209,7 @@ export function TaskDetail({ task }: TaskDetailProps) { onClick={handleCancel} color="red" size="3" - variant='outline' + variant="outline" > Cancel @@ -208,16 +220,24 @@ export function TaskDetail({ task }: TaskDetailProps) { {/* Right pane - Logs */} - + {/* Background ASCII Art */} - + {/* Foreground LogView */} - - + + ); -} \ No newline at end of file +} diff --git a/src/renderer/components/TaskList.tsx b/src/renderer/components/TaskList.tsx index 92ba44b57..abd3d14eb 100644 --- a/src/renderer/components/TaskList.tsx +++ b/src/renderer/components/TaskList.tsx @@ -1,11 +1,19 @@ -import React, { useEffect, useCallback, useState, useRef } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { Flex, Box, Text, TextField, Button, Badge, Spinner } from '@radix-ui/themes'; -import { Task } from '@shared/types'; -import { useTaskStore } from '../stores/taskStore'; -import { useStatusBarStore } from '../stores/statusBarStore'; -import { formatDistanceToNow } from 'date-fns'; -import { AsciiArt } from './AsciiArt'; +import { + Badge, + Box, + Button, + Flex, + Spinner, + Text, + TextField, +} from "@radix-ui/themes"; +import type { Task } from "@shared/types"; +import { formatDistanceToNow } from "date-fns"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; +import { useStatusBarStore } from "../stores/statusBarStore"; +import { useTaskStore } from "../stores/taskStore"; +import { AsciiArt } from "./AsciiArt"; interface TaskListProps { onSelectTask: (task: Task) => void; @@ -15,7 +23,7 @@ export function TaskList({ onSelectTask }: TaskListProps) { const { tasks, fetchTasks, isLoading, error } = useTaskStore(); const { setStatusBar, reset } = useStatusBarStore(); const [selectedIndex, setSelectedIndex] = useState(0); - const [filter, setFilter] = useState(''); + const [filter, setFilter] = useState(""); const listRef = useRef(null); useEffect(() => { @@ -23,34 +31,38 @@ export function TaskList({ onSelectTask }: TaskListProps) { }, [fetchTasks]); const filteredTasks = tasks - .filter(task => - task.title.toLowerCase().includes(filter.toLowerCase()) || - task.description?.toLowerCase().includes(filter.toLowerCase()) + .filter( + (task) => + task.title.toLowerCase().includes(filter.toLowerCase()) || + task.description?.toLowerCase().includes(filter.toLowerCase()), ) - .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); + .sort( + (a, b) => + new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), + ); useEffect(() => { setStatusBar({ - statusText: `${filteredTasks.length} task${filteredTasks.length === 1 ? '' : 's'}`, + statusText: `${filteredTasks.length} task${filteredTasks.length === 1 ? "" : "s"}`, keyHints: [ { - keys: [navigator.platform.includes('Mac') ? '⌘' : 'Ctrl', 'K'], - description: 'Command' + keys: [navigator.platform.includes("Mac") ? "⌘" : "Ctrl", "K"], + description: "Command", }, { - keys: [navigator.platform.includes('Mac') ? '⌘' : 'Ctrl', 'R'], - description: 'Refresh' + keys: [navigator.platform.includes("Mac") ? "⌘" : "Ctrl", "R"], + description: "Refresh", }, { - keys: ['↑', '↓'], - description: 'Navigate' + keys: ["↑", "↓"], + description: "Navigate", }, { - keys: ['Enter'], - description: 'Select' - } + keys: ["Enter"], + description: "Select", + }, ], - mode: 'replace' + mode: "replace", }); return () => { @@ -58,15 +70,18 @@ export function TaskList({ onSelectTask }: TaskListProps) { }; }, [setStatusBar, reset, filteredTasks.length]); - const handleKeyNavigation = useCallback((direction: 'up' | 'down') => { - setSelectedIndex(prev => { - if (direction === 'up') { - return Math.max(0, prev - 1); - } else { - return Math.min(filteredTasks.length - 1, prev + 1); - } - }); - }, [filteredTasks.length]); + const handleKeyNavigation = useCallback( + (direction: "up" | "down") => { + setSelectedIndex((prev) => { + if (direction === "up") { + return Math.max(0, prev - 1); + } else { + return Math.min(filteredTasks.length - 1, prev + 1); + } + }); + }, + [filteredTasks.length], + ); const handleSelectCurrent = useCallback(() => { if (filteredTasks[selectedIndex]) { @@ -75,10 +90,10 @@ export function TaskList({ onSelectTask }: TaskListProps) { }, [filteredTasks, selectedIndex, onSelectTask]); // Keyboard shortcuts - useHotkeys('up', () => handleKeyNavigation('up'), [handleKeyNavigation]); - useHotkeys('down', () => handleKeyNavigation('down'), [handleKeyNavigation]); - useHotkeys('enter', handleSelectCurrent, [handleSelectCurrent]); - useHotkeys('cmd+r, ctrl+r', () => fetchTasks(), [fetchTasks]); + useHotkeys("up", () => handleKeyNavigation("up"), [handleKeyNavigation]); + useHotkeys("down", () => handleKeyNavigation("down"), [handleKeyNavigation]); + useHotkeys("enter", handleSelectCurrent, [handleSelectCurrent]); + useHotkeys("cmd+r, ctrl+r", () => fetchTasks(), [fetchTasks]); // Scroll selected item into view useEffect(() => { @@ -90,9 +105,9 @@ export function TaskList({ onSelectTask }: TaskListProps) { const elementRect = selectedElement.getBoundingClientRect(); if (elementRect.bottom > containerRect.bottom) { - selectedElement.scrollIntoView({ block: 'end', behavior: 'smooth' }); + selectedElement.scrollIntoView({ block: "end", behavior: "smooth" }); } else if (elementRect.top < containerRect.top) { - selectedElement.scrollIntoView({ block: 'start', behavior: 'smooth' }); + selectedElement.scrollIntoView({ block: "start", behavior: "smooth" }); } } }, [selectedIndex]); @@ -115,9 +130,7 @@ export function TaskList({ onSelectTask }: TaskListProps) { {error} - + ); @@ -127,9 +140,9 @@ export function TaskList({ onSelectTask }: TaskListProps) { {/* Left pane - Task list */} - + - + { @@ -144,7 +157,7 @@ export function TaskList({ onSelectTask }: TaskListProps) { {filteredTasks.length === 0 ? ( - {filter ? 'No tasks match your filter' : 'No tasks found'} + {filter ? "No tasks match your filter" : "No tasks found"} ) : ( @@ -183,36 +196,41 @@ function TaskItem({ task, isSelected, onClick }: TaskItemProps) { const timeAgo = formatDistanceToNow(createdAt, { addSuffix: true }); // TODO: Look up stage name from workflow data - const status = 'Backlog'; + const status = "Backlog"; return ( - {isSelected ? '[•]' : '[ ]'} + {isSelected ? "[•]" : "[ ]"} - + {status} - + {task.title} - {task.repository_config && ( + {task.repository_config && + typeof task.repository_config === "object" && + "organization" in task.repository_config && + "repository" in task.repository_config ? ( - {task.repository_config.organization}/{task.repository_config.repository} + {String(task.repository_config.organization)}/ + {String(task.repository_config.repository)} - )} + ) : null} {timeAgo} @@ -221,4 +239,3 @@ function TaskItem({ task, isSelected, onClick }: TaskItemProps) { ); } - diff --git a/src/renderer/components/ThemeWrapper.tsx b/src/renderer/components/ThemeWrapper.tsx index f9fb05065..266770a65 100644 --- a/src/renderer/components/ThemeWrapper.tsx +++ b/src/renderer/components/ThemeWrapper.tsx @@ -1,13 +1,13 @@ -import React from 'react'; -import { Theme } from '@radix-ui/themes'; -import { useThemeStore } from '../stores/themeStore'; +import { Theme } from "@radix-ui/themes"; +import type React from "react"; +import { useThemeStore } from "../stores/themeStore"; export function ThemeWrapper({ children }: { children: React.ReactNode }) { const isDarkMode = useThemeStore((state) => state.isDarkMode); return ( ); -} \ No newline at end of file +} diff --git a/src/renderer/components/WorkflowView.tsx b/src/renderer/components/WorkflowView.tsx index fc4042f86..98239a1c3 100644 --- a/src/renderer/components/WorkflowView.tsx +++ b/src/renderer/components/WorkflowView.tsx @@ -1,21 +1,39 @@ -import React, { useEffect, useState } from 'react'; -import { Flex, Box, Heading, Text, Select, Card, Badge, Spinner } from '@radix-ui/themes'; -import { useWorkflowStore } from '../stores/workflowStore'; -import { useTaskStore } from '../stores/taskStore'; -import { useStatusBarStore } from '../stores/statusBarStore'; -import { Task, WorkflowStage } from '@shared/types'; -import { formatDistanceToNow } from 'date-fns'; -import { AsciiArt } from './AsciiArt'; +import { + Badge, + Box, + Card, + Flex, + Heading, + Select, + Spinner, + Text, +} from "@radix-ui/themes"; +import type { Task, WorkflowStage } from "@shared/types"; +import { formatDistanceToNow } from "date-fns"; +import { useEffect, useState } from "react"; +import { useStatusBarStore } from "../stores/statusBarStore"; +import { useTaskStore } from "../stores/taskStore"; +import { useWorkflowStore } from "../stores/workflowStore"; +import { AsciiArt } from "./AsciiArt"; interface WorkflowViewProps { onSelectTask: (task: Task) => void; } export function WorkflowView({ onSelectTask }: WorkflowViewProps) { - const { workflows, selectedWorkflowId, fetchWorkflows, selectWorkflow, getTasksByStage, isLoading } = useWorkflowStore(); + const { + workflows, + selectedWorkflowId, + fetchWorkflows, + selectWorkflow, + getTasksByStage, + isLoading, + } = useWorkflowStore(); const { fetchTasks } = useTaskStore(); const { setStatusBar, reset } = useStatusBarStore(); - const [tasksByStage, setTasksByStage] = useState>(new Map()); + const [tasksByStage, setTasksByStage] = useState>( + new Map(), + ); useEffect(() => { fetchWorkflows(); @@ -28,24 +46,29 @@ export function WorkflowView({ onSelectTask }: WorkflowViewProps) { } }, [selectedWorkflowId, getTasksByStage]); - const selectedWorkflow = workflows.find(w => w.id === selectedWorkflowId); + const selectedWorkflow = workflows.find((w) => w.id === selectedWorkflowId); useEffect(() => { - const totalTasks = Array.from(tasksByStage.values()).reduce((sum, tasks) => sum + tasks.length, 0); + const totalTasks = Array.from(tasksByStage.values()).reduce( + (sum, tasks) => sum + tasks.length, + 0, + ); setStatusBar({ - statusText: selectedWorkflow ? `Workflow: ${selectedWorkflow.name} (${totalTasks} tasks)` : 'Workflow view', + statusText: selectedWorkflow + ? `Workflow: ${selectedWorkflow.name} (${totalTasks} tasks)` + : "Workflow view", keyHints: [ { - keys: [navigator.platform.includes('Mac') ? '⌘' : 'Ctrl', 'K'], - description: 'Command' + keys: [navigator.platform.includes("Mac") ? "⌘" : "Ctrl", "K"], + description: "Command", }, { - keys: [navigator.platform.includes('Mac') ? '⌘' : 'Ctrl', 'R'], - description: 'Refresh' - } + keys: [navigator.platform.includes("Mac") ? "⌘" : "Ctrl", "R"], + description: "Refresh", + }, ], - mode: 'replace' + mode: "replace", }); return () => { @@ -82,30 +105,32 @@ export function WorkflowView({ onSelectTask }: WorkflowViewProps) { } return ( - + {/* Background ASCII Art */} - + {/* Foreground Content */} - + {/* Workflow selector */} - + Workflow View selectWorkflow(value || null)} > - {workflows.filter(w => w.is_active).map(workflow => ( - - {workflow.name} {workflow.is_default && '(Default)'} - - ))} + {workflows + .filter((w) => w.is_active) + .map((workflow) => ( + + {workflow.name} {workflow.is_default && "(Default)"} + + ))} @@ -119,9 +144,9 @@ export function WorkflowView({ onSelectTask }: WorkflowViewProps) { {selectedWorkflow?.stages - .filter(stage => !stage.is_archived) + .filter((stage) => !stage.is_archived) .sort((a, b) => a.position - b.position) - .map(stage => ( + .map((stage) => ( - - + {stage.name} - {tasks.length} + + {tasks.length} + - {stage.agent_name ? 'Automated by ' + stage.agent_name : '\u00A0'} + {stage.agent_name ? `Automated by ${stage.agent_name}` : "\u00A0"} - - + {tasks.length === 0 ? ( @@ -176,7 +194,7 @@ function WorkflowColumn({ stage, tasks, onSelectTask }: WorkflowColumnProps) { ) : ( - {tasks.map(task => ( + {tasks.map((task) => ( - ); } @@ -211,20 +228,26 @@ function WorkflowTaskCard({ task, onClick }: WorkflowTaskCardProps) { - {timeAgo} + + {timeAgo} + {task.origin_product && ( - {task.origin_product.replace('_', ' ')} + {task.origin_product.replace("_", " ")} )} - {task.repository_config && ( + {task.repository_config && + typeof task.repository_config === "object" && + "organization" in task.repository_config && + "repository" in task.repository_config ? ( - {task.repository_config.organization}/{task.repository_config.repository} + {String(task.repository_config.organization)}/ + {String(task.repository_config.repository)} - )} + ) : null} ); -} \ No newline at end of file +} diff --git a/src/renderer/components/command/Command.tsx b/src/renderer/components/command/Command.tsx index 4aed58c15..d5d0e742a 100644 --- a/src/renderer/components/command/Command.tsx +++ b/src/renderer/components/command/Command.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import { Command as CmdkCommand } from 'cmdk'; -import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; -import { Flex } from '@radix-ui/themes'; +import { MagnifyingGlassIcon } from "@radix-ui/react-icons"; +import { Flex } from "@radix-ui/themes"; +import { Command as CmdkCommand } from "cmdk"; +import React from "react"; interface CommandRootProps extends React.ComponentProps { className?: string; @@ -14,15 +14,16 @@ const CommandRoot = React.forwardRef< return ( ); }); -CommandRoot.displayName = 'CommandRoot'; +CommandRoot.displayName = "CommandRoot"; -interface CommandInputProps extends React.ComponentProps { +interface CommandInputProps + extends React.ComponentProps { className?: string; showIcon?: boolean; autoFocus?: boolean; @@ -34,12 +35,12 @@ const CommandInput = React.forwardRef< >(({ className, showIcon = true, autoFocus = false, ...props }, ref) => { if (showIcon) { return ( - - + + @@ -50,50 +51,62 @@ const CommandInput = React.forwardRef< ); }); -CommandInput.displayName = 'CommandInput'; +CommandInput.displayName = "CommandInput"; -interface CommandListProps extends React.ComponentProps { +interface CommandListProps + extends React.ComponentProps { className?: string; } function CommandList({ className, ...props }: CommandListProps) { return ( ); } -interface CommandItemProps extends React.ComponentProps { +interface CommandItemProps + extends React.ComponentProps { className?: string; } function CommandItem({ className, ...props }: CommandItemProps) { return ( ); } -interface CommandGroupProps extends React.ComponentProps { +interface CommandGroupProps + extends React.ComponentProps { className?: string; heading?: string; } -function CommandGroup({ className, heading, children, ...props }: CommandGroupProps) { +function CommandGroup({ + className, + heading, + children, + ...props +}: CommandGroupProps) { return ( {heading && ( - + {heading} )} @@ -102,14 +115,15 @@ function CommandGroup({ className, heading, children, ...props }: CommandGroupPr ); } -interface CommandEmptyProps extends React.ComponentProps { +interface CommandEmptyProps + extends React.ComponentProps { className?: string; } function CommandEmpty({ className, ...props }: CommandEmptyProps) { return ( ); @@ -122,4 +136,4 @@ export const Command = { Item: CommandItem, Group: CommandGroup, Empty: CommandEmpty, -}; \ No newline at end of file +}; diff --git a/src/renderer/components/command/CommandKeyHints.tsx b/src/renderer/components/command/CommandKeyHints.tsx index ad4e5df1b..fb85bfaff 100644 --- a/src/renderer/components/command/CommandKeyHints.tsx +++ b/src/renderer/components/command/CommandKeyHints.tsx @@ -1,22 +1,34 @@ -import React from 'react'; -import { Flex, Text, Code } from '@radix-ui/themes'; -import { KeyHint } from './KeyHint'; +import { Code, Flex } from "@radix-ui/themes"; +import { KeyHint } from "./KeyHint"; export function CommandKeyHints() { return ( - + - - Navigate + + + Navigate + - - Select + + + Select + - - Close + + + Close + ); -} \ No newline at end of file +} diff --git a/src/renderer/components/command/CommandMenu.tsx b/src/renderer/components/command/CommandMenu.tsx index 1ded4106d..c52d12923 100644 --- a/src/renderer/components/command/CommandMenu.tsx +++ b/src/renderer/components/command/CommandMenu.tsx @@ -1,11 +1,16 @@ -import React, { useEffect, useRef, useCallback } from 'react'; -import { Flex, Text } from '@radix-ui/themes'; -import { Command } from './Command'; -import { CommandKeyHints } from './CommandKeyHints'; -import { useTabStore } from '../../stores/tabStore'; -import { useTaskStore } from '../../stores/taskStore'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { ListBulletIcon, ComponentInstanceIcon, FileTextIcon } from '@radix-ui/react-icons'; +import { + ComponentInstanceIcon, + FileTextIcon, + ListBulletIcon, +} from "@radix-ui/react-icons"; +import { Flex, Text } from "@radix-ui/themes"; +import { useCallback, useEffect, useRef } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; +import type { Task } from "@/shared/types"; +import { useTabStore } from "../../stores/tabStore"; +import { useTaskStore } from "../../stores/taskStore"; +import { Command } from "./Command"; +import { CommandKeyHints } from "./CommandKeyHints"; interface CommandMenuProps { open: boolean; @@ -28,25 +33,25 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { onOpenChange(false); }, [onOpenChange]); - useHotkeys('escape', handleClose, { + useHotkeys("escape", handleClose, { enabled: open, enableOnContentEditable: true, enableOnFormTags: true, - preventDefault: true + preventDefault: true, }); - useHotkeys('mod+k', handleClose, { + useHotkeys("mod+k", handleClose, { enabled: open, enableOnContentEditable: true, enableOnFormTags: true, - preventDefault: true + preventDefault: true, }); - useHotkeys('mod+p', handleClose, { + useHotkeys("mod+p", handleClose, { enabled: open, enableOnContentEditable: true, enableOnFormTags: true, - preventDefault: true + preventDefault: true, }); // Handle click outside @@ -54,54 +59,61 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { if (!open) return; const handleClickOutside = (event: MouseEvent) => { - if (commandRef.current && !commandRef.current.contains(event.target as Node)) { + if ( + commandRef.current && + !commandRef.current.contains(event.target as Node) + ) { onOpenChange(false); } }; - document.addEventListener('mousedown', handleClickOutside); + document.addEventListener("mousedown", handleClickOutside); return () => { - document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener("mousedown", handleClickOutside); }; }, [open, onOpenChange]); const handleNavigateToTasks = () => { - const tasksTab = tabs.find(tab => tab.type === 'task-list'); + const tasksTab = tabs.find((tab) => tab.type === "task-list"); if (tasksTab) { setActiveTab(tasksTab.id); } else { createTab({ - type: 'task-list', - title: 'Tasks', + type: "task-list", + title: "Tasks", }); } onOpenChange(false); }; const handleNavigateToWorkflow = () => { - const workflowTab = tabs.find(tab => tab.type === 'workflow'); + const workflowTab = tabs.find((tab) => tab.type === "workflow"); if (workflowTab) { setActiveTab(workflowTab.id); } else { createTab({ - type: 'workflow', - title: 'Workflow', + type: "workflow", + title: "Workflow", }); } onOpenChange(false); }; - const handleNavigateToTask = (task: { id: string; title: string; description?: string }) => { + const handleNavigateToTask = (task: { + id: string; + title: string; + description?: string; + }) => { // Check if task is already open in a tab - const existingTab = tabs.find(tab => - tab.type === 'task-detail' && tab.data?.id === task.id + const existingTab = tabs.find( + (tab) => tab.type === "task-detail" && (tab.data as Task)?.id === task.id, ); if (existingTab) { setActiveTab(existingTab.id); } else { createTab({ - type: 'task-detail', + type: "task-detail", title: task.title, data: task, }); @@ -112,7 +124,12 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { if (!open) return null; return ( - + - + Tasks - + Workflow @@ -143,11 +160,13 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { onSelect={() => handleNavigateToTask(task)} className="items-start" > - + - {task.title} + + {task.title} + {task.description && ( - + {task.description} )} @@ -162,4 +181,4 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { ); -} \ No newline at end of file +} diff --git a/src/renderer/components/command/KeyHint.tsx b/src/renderer/components/command/KeyHint.tsx index 5635f61d5..11419b5ec 100644 --- a/src/renderer/components/command/KeyHint.tsx +++ b/src/renderer/components/command/KeyHint.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { Kbd } from '@radix-ui/themes'; +import { Kbd } from "@radix-ui/themes"; interface KeyHintProps { keys: string[]; @@ -8,12 +7,12 @@ interface KeyHintProps { export function KeyHint({ keys, className }: KeyHintProps) { return ( -
- {keys.map((key, index) => ( - +
+ {keys.map((key) => ( + {key} ))}
); -} \ No newline at end of file +} diff --git a/src/renderer/components/command/index.ts b/src/renderer/components/command/index.ts index aad6a2171..134ac9ac3 100644 --- a/src/renderer/components/command/index.ts +++ b/src/renderer/components/command/index.ts @@ -1,4 +1,4 @@ -export { CommandMenu } from './CommandMenu'; -export { Command } from './Command'; -export { KeyHint } from './KeyHint'; -export { CommandKeyHints } from './CommandKeyHints'; \ No newline at end of file +export { Command } from "./Command"; +export { CommandKeyHints } from "./CommandKeyHints"; +export { CommandMenu } from "./CommandMenu"; +export { KeyHint } from "./KeyHint"; diff --git a/src/renderer/components/log/DiffView.tsx b/src/renderer/components/log/DiffView.tsx index 71d9f9eb2..0363e8d53 100644 --- a/src/renderer/components/log/DiffView.tsx +++ b/src/renderer/components/log/DiffView.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { Box, Flex, Text, Code } from '@radix-ui/themes'; +import { Box, Code, Flex } from "@radix-ui/themes"; interface DiffViewProps { file: string; @@ -9,40 +8,53 @@ interface DiffViewProps { } function getFileParts(path: string): { dir: string; base: string } { - if (!path) return { dir: '', base: '' }; - const idx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); - if (idx === -1) return { dir: '', base: path }; + if (!path) return { dir: "", base: "" }; + const idx = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")); + if (idx === -1) return { dir: "", base: path }; return { dir: path.slice(0, idx + 1), base: path.slice(idx + 1) }; } export function DiffView({ file, patch, added, removed }: DiffViewProps) { const { dir, base } = getFileParts(file); - const lines = (patch || '').split(/\r?\n/); + const lines = (patch || "").split(/\r?\n/); return ( - + - {dir} - {base} + + {dir} + + + {base} + - {typeof added === 'number' && typeof removed === 'number' ? ( - +{added} / -{removed} + {typeof added === "number" && typeof removed === "number" ? ( + + +{added} / -{removed} + ) : null} - + {lines.map((line, i) => { - let color: string | undefined = undefined; - if (line.startsWith('+++') || line.startsWith('---')) color = 'gray'; - else if (line.startsWith('@@')) color = 'blue'; - else if (line.startsWith('+')) color = 'green'; - else if (line.startsWith('-')) color = 'red'; - else if (line.startsWith('diff ') || line.startsWith('index ')) color = 'gray'; + let color: "gray" | "blue" | "green" | "red" | undefined; + if (line.startsWith("+++") || line.startsWith("---")) color = "gray"; + else if (line.startsWith("@@")) color = "blue"; + else if (line.startsWith("+")) color = "green"; + else if (line.startsWith("-")) color = "red"; + else if (line.startsWith("diff ") || line.startsWith("index ")) + color = "gray"; return ( -
- {line || '\u00A0'} +
+ + {line || "\u00A0"} +
); })} @@ -50,5 +62,3 @@ export function DiffView({ file, patch, added, removed }: DiffViewProps) { ); } - - diff --git a/src/renderer/components/log/MetricView.tsx b/src/renderer/components/log/MetricView.tsx index 56efbe820..7084802bd 100644 --- a/src/renderer/components/log/MetricView.tsx +++ b/src/renderer/components/log/MetricView.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { Box, Text, Code } from '@radix-ui/themes'; +import { Box, Code } from "@radix-ui/themes"; interface MetricViewProps { keyName: string; @@ -9,12 +8,18 @@ interface MetricViewProps { export function MetricView({ keyName, value, unit }: MetricViewProps) { return ( - - {keyName}: - {value} - {unit ? {unit} : null} + + + {keyName}: + + + {value} + + {unit ? ( + + {unit} + + ) : null} ); } - - diff --git a/src/renderer/components/log/ToolCallView.tsx b/src/renderer/components/log/ToolCallView.tsx index 7d9386a53..6891655c1 100644 --- a/src/renderer/components/log/ToolCallView.tsx +++ b/src/renderer/components/log/ToolCallView.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { Box, Flex, Button, Code } from '@radix-ui/themes'; +import { Box, Button, Code, Flex } from "@radix-ui/themes"; +import { useState } from "react"; interface ToolCallViewProps { toolName: string; @@ -10,11 +10,11 @@ interface ToolCallViewProps { function stringify(value: unknown, maxLength = 2000): string { try { const s = JSON.stringify(value, null, 2); - if (!s) return ''; - return s.length > maxLength ? s.slice(0, maxLength) + '…' : s; + if (!s) return ""; + return s.length > maxLength ? `${s.slice(0, maxLength)}…` : s; } catch { - const s = String(value ?? ''); - return s.length > maxLength ? s.slice(0, maxLength) + '…' : s; + const s = String(value ?? ""); + return s.length > maxLength ? `${s.slice(0, maxLength)}…` : s; } } @@ -22,27 +22,35 @@ export function ToolCallView({ toolName, callId, args }: ToolCallViewProps) { const [expanded, setExpanded] = useState(true); return ( - + - {toolName} - {callId ? [{callId}] : null} + + {toolName} + + {callId ? ( + + [{callId}] + + ) : null} {expanded && args !== undefined && ( - + {stringify(args)} )} ); } - - diff --git a/src/renderer/main.tsx b/src/renderer/main.tsx index 514538331..0ebdf47e1 100644 --- a/src/renderer/main.tsx +++ b/src/renderer/main.tsx @@ -1,14 +1,17 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import '@radix-ui/themes/styles.css'; -import { ThemeWrapper } from './components/ThemeWrapper'; -import App from './App'; -import './styles/globals.css'; +import React from "react"; +import ReactDOM from "react-dom/client"; +import "@radix-ui/themes/styles.css"; +import App from "./App"; +import { ThemeWrapper } from "./components/ThemeWrapper"; +import "./styles/globals.css"; -ReactDOM.createRoot(document.getElementById('root')!).render( +const rootElement = document.getElementById("root"); +if (!rootElement) throw new Error("Root element not found"); + +ReactDOM.createRoot(rootElement).render( , -); \ No newline at end of file +); diff --git a/src/renderer/stores/authStore.ts b/src/renderer/stores/authStore.ts index e4d7c23e0..a2b45e643 100644 --- a/src/renderer/stores/authStore.ts +++ b/src/renderer/stores/authStore.ts @@ -1,7 +1,7 @@ -import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; -import { PostHogAPIClient } from '@api/posthogClient'; -import { User } from '@shared/types'; +import { PostHogAPIClient } from "@api/posthogClient"; +import type { User } from "@shared/types"; +import { create } from "zustand"; +import { persist } from "zustand/middleware"; interface AuthState { apiKey: string | null; @@ -10,7 +10,7 @@ interface AuthState { user: User | null; isAuthenticated: boolean; client: PostHogAPIClient | null; - + setCredentials: (apiKey: string, apiHost: string) => Promise; checkAuth: () => Promise; logout: () => void; @@ -20,23 +20,23 @@ export const useAuthStore = create()( persist( (set, get) => ({ apiKey: null, - apiHost: 'https://app.posthog.com', + apiHost: "https://app.posthog.com", encryptedKey: null, user: null, isAuthenticated: false, client: null, - + setCredentials: async (apiKey: string, apiHost: string) => { // Encrypt the API key using Electron's secure storage const encryptedKey = await window.electronAPI.storeApiKey(apiKey); - + // Create API client const client = new PostHogAPIClient(apiKey, apiHost); - + try { // Verify credentials by fetching user const user = await client.getCurrentUser(); - + set({ apiKey, apiHost, @@ -45,33 +45,35 @@ export const useAuthStore = create()( isAuthenticated: true, client, }); - } catch (error) { - throw new Error('Invalid API key or host'); + } catch (_error) { + throw new Error("Invalid API key or host"); } }, - + checkAuth: async () => { const state = get(); - + // Note: Environment variables are not available in renderer process // They need to be passed from main process if needed - + // Check stored encrypted key if (state.encryptedKey) { - const decryptedKey = await window.electronAPI.retrieveApiKey(state.encryptedKey); - + const decryptedKey = await window.electronAPI.retrieveApiKey( + state.encryptedKey, + ); + if (decryptedKey) { try { const client = new PostHogAPIClient(decryptedKey, state.apiHost); const user = await client.getCurrentUser(); - + set({ apiKey: decryptedKey, user, isAuthenticated: true, client, }); - + return true; } catch { // Invalid stored credentials @@ -79,10 +81,10 @@ export const useAuthStore = create()( } } } - + return false; }, - + logout: () => { set({ apiKey: null, @@ -94,11 +96,11 @@ export const useAuthStore = create()( }, }), { - name: 'mission-control-auth', + name: "mission-control-auth", partialize: (state) => ({ apiHost: state.apiHost, encryptedKey: state.encryptedKey, }), - } - ) -); \ No newline at end of file + }, + ), +); diff --git a/src/renderer/stores/statusBarStore.ts b/src/renderer/stores/statusBarStore.ts index e05f38c17..511b613b1 100644 --- a/src/renderer/stores/statusBarStore.ts +++ b/src/renderer/stores/statusBarStore.ts @@ -1,4 +1,4 @@ -import { create } from 'zustand'; +import { create } from "zustand"; interface KeyHint { keys: string[]; @@ -8,50 +8,50 @@ interface KeyHint { interface StatusBarState { statusText: string; keyHints: KeyHint[]; - mode: 'replace' | 'append'; + mode: "replace" | "append"; } interface StatusBarStore { statusText: string; keyHints: KeyHint[]; - mode: 'replace' | 'append'; + mode: "replace" | "append"; setStatus: (text: string) => void; - setKeyHints: (hints: KeyHint[], mode?: 'replace' | 'append') => void; + setKeyHints: (hints: KeyHint[], mode?: "replace" | "append") => void; setStatusBar: (config: Partial) => void; reset: () => void; } const defaultKeyHints: KeyHint[] = [ { - keys: [navigator.platform.includes('Mac') ? '⌘' : 'Ctrl', 'K'], - description: 'Command' + keys: [navigator.platform.includes("Mac") ? "⌘" : "Ctrl", "K"], + description: "Command", }, { - keys: [navigator.platform.includes('Mac') ? '⌘' : 'Ctrl', 'R'], - description: 'Refresh' - } + keys: [navigator.platform.includes("Mac") ? "⌘" : "Ctrl", "R"], + description: "Refresh", + }, ]; -export const useStatusBarStore = create((set, get) => ({ - statusText: 'Ready', +export const useStatusBarStore = create((set, _get) => ({ + statusText: "Ready", keyHints: defaultKeyHints, - mode: 'replace', + mode: "replace", setStatus: (text) => { set({ statusText: text }); }, - setKeyHints: (hints, mode = 'replace') => { - if (mode === 'append') { + setKeyHints: (hints, mode = "replace") => { + if (mode === "append") { set({ keyHints: [...defaultKeyHints, ...hints], - mode + mode, }); } else { set({ keyHints: hints, - mode + mode, }); } }, @@ -59,7 +59,7 @@ export const useStatusBarStore = create((set, get) => ({ setStatusBar: (config) => { const newState: Partial = { ...config }; - if (config.keyHints && config.mode === 'append') { + if (config.keyHints && config.mode === "append") { newState.keyHints = [...defaultKeyHints, ...config.keyHints]; } @@ -68,9 +68,9 @@ export const useStatusBarStore = create((set, get) => ({ reset: () => { set({ - statusText: 'Ready', + statusText: "Ready", keyHints: defaultKeyHints, - mode: 'replace' + mode: "replace", }); - } -})); \ No newline at end of file + }, +})); diff --git a/src/renderer/stores/tabStore.ts b/src/renderer/stores/tabStore.ts index e50d47503..dc12b24b4 100644 --- a/src/renderer/stores/tabStore.ts +++ b/src/renderer/stores/tabStore.ts @@ -1,12 +1,12 @@ -import { create } from 'zustand'; -import { TabState } from '@shared/types'; -import { v4 as uuidv4 } from 'uuid'; +import type { TabState } from "@shared/types"; +import { v4 as uuidv4 } from "uuid"; +import { create } from "zustand"; interface TabStore { tabs: TabState[]; activeTabId: string; - createTab: (tab: Omit) => void; + createTab: (tab: Omit) => void; closeTab: (tabId: string) => void; setActiveTab: (tabId: string) => void; reorderTabs: (fromIndex: number, toIndex: number) => void; @@ -15,63 +15,63 @@ interface TabStore { // Create initial tabs const taskListTab: TabState = { id: uuidv4(), - type: 'task-list', - title: 'Tasks', + type: "task-list", + title: "Tasks", }; const workflowTab: TabState = { id: uuidv4(), - type: 'workflow', - title: 'Workflow', + type: "workflow", + title: "Workflow", }; export const useTabStore = create((set, get) => ({ tabs: [taskListTab, workflowTab], activeTabId: taskListTab.id, - + createTab: (tabData) => { const newTab: TabState = { ...tabData, id: uuidv4(), }; - - set(state => ({ + + set((state) => ({ tabs: [...state.tabs, newTab], activeTabId: newTab.id, })); }, - + closeTab: (tabId) => { const state = get(); - const tabIndex = state.tabs.findIndex(tab => tab.id === tabId); - + const tabIndex = state.tabs.findIndex((tab) => tab.id === tabId); + if (tabIndex === -1 || state.tabs.length === 1) return; - - const newTabs = state.tabs.filter(tab => tab.id !== tabId); + + const newTabs = state.tabs.filter((tab) => tab.id !== tabId); let newActiveTabId = state.activeTabId; - + if (state.activeTabId === tabId) { // Select the tab to the left, or the first tab if closing the leftmost const newIndex = Math.max(0, tabIndex - 1); newActiveTabId = newTabs[newIndex].id; } - + set({ tabs: newTabs, activeTabId: newActiveTabId, }); }, - + setActiveTab: (tabId) => { set({ activeTabId: tabId }); }, reorderTabs: (fromIndex, toIndex) => { - set(state => { + set((state) => { const newTabs = [...state.tabs]; const [movedTab] = newTabs.splice(fromIndex, 1); newTabs.splice(toIndex, 0, movedTab); return { tabs: newTabs }; }); }, -})); \ No newline at end of file +})); diff --git a/src/renderer/stores/taskExecutionStore.ts b/src/renderer/stores/taskExecutionStore.ts index e2071f4ae..8f0853cac 100644 --- a/src/renderer/stores/taskExecutionStore.ts +++ b/src/renderer/stores/taskExecutionStore.ts @@ -1,13 +1,13 @@ -import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; -import { LogEntry } from '../types/log'; +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import type { LogEntry } from "../types/log"; interface TaskExecutionState { isRunning: boolean; logs: Array; repoPath: string | null; currentTaskId: string | null; - runMode: 'local' | 'cloud'; + runMode: "local" | "cloud"; unsubscribe: (() => void) | null; } @@ -17,19 +17,25 @@ interface TaskExecutionStore { // Basic state accessors getTaskState: (taskId: string) => TaskExecutionState; - updateTaskState: (taskId: string, updates: Partial) => void; + updateTaskState: ( + taskId: string, + updates: Partial, + ) => void; setRunning: (taskId: string, isRunning: boolean) => void; addLog: (taskId: string, log: string | LogEntry) => void; setLogs: (taskId: string, logs: Array) => void; setRepoPath: (taskId: string, repoPath: string | null) => void; setCurrentTaskId: (taskId: string, currentTaskId: string | null) => void; - setRunMode: (taskId: string, runMode: 'local' | 'cloud') => void; + setRunMode: (taskId: string, runMode: "local" | "cloud") => void; setUnsubscribe: (taskId: string, unsubscribe: (() => void) | null) => void; clearTaskState: (taskId: string) => void; // High-level task execution actions selectRepositoryForTask: (taskId: string) => Promise; - runTask: (taskId: string, task: { id: string; title: string; description?: string }) => Promise; + runTask: ( + taskId: string, + task: { id: string; title: string; description?: string }, + ) => Promise; cancelTask: (taskId: string) => Promise; clearTaskLogs: (taskId: string) => void; @@ -38,13 +44,12 @@ interface TaskExecutionStore { unsubscribeFromAgentEvents: (taskId: string) => void; } - const defaultTaskState: TaskExecutionState = { isRunning: false, logs: [], repoPath: null, currentTaskId: null, - runMode: 'local', + runMode: "local", unsubscribe: null, }; @@ -58,12 +63,15 @@ export const useTaskExecutionStore = create()( return state.taskStates[taskId] || { ...defaultTaskState }; }, - updateTaskState: (taskId: string, updates: Partial) => { - set(state => ({ + updateTaskState: ( + taskId: string, + updates: Partial, + ) => { + set((state) => ({ taskStates: { ...state.taskStates, [taskId]: { - ...state.taskStates[taskId] || defaultTaskState, + ...(state.taskStates[taskId] || defaultTaskState), ...updates, }, }, @@ -77,7 +85,7 @@ export const useTaskExecutionStore = create()( addLog: (taskId: string, log: string | LogEntry) => { const currentState = get().getTaskState(taskId); get().updateTaskState(taskId, { - logs: [...currentState.logs, log] + logs: [...currentState.logs, log], }); }, @@ -93,7 +101,7 @@ export const useTaskExecutionStore = create()( get().updateTaskState(taskId, { currentTaskId }); }, - setRunMode: (taskId: string, runMode: 'local' | 'cloud') => { + setRunMode: (taskId: string, runMode: "local" | "cloud") => { get().updateTaskState(taskId, { runMode }); }, @@ -107,7 +115,7 @@ export const useTaskExecutionStore = create()( if (taskState?.unsubscribe) { taskState.unsubscribe(); } - set(state => { + set((state) => { const newTaskStates = { ...state.taskStates }; delete newTaskStates[taskId]; return { taskStates: newTaskStates }; @@ -124,88 +132,118 @@ export const useTaskExecutionStore = create()( } // Create new subscription that persists even when component unmounts - const unsubscribeFn = (window as any).electronAPI?.onAgentEvent(channel, (ev: any) => { - const currentStore = get(); - - switch (ev?.type) { - case 'token': - if (typeof ev.content === 'string' && ev.content.trim().length > 0) { - currentStore.addLog(taskId, { type: 'text', ts: ev.ts || Date.now(), content: ev.content }); - } - break; - case 'status': - if (ev.phase) { - currentStore.addLog(taskId, { type: 'status', ts: ev.ts || Date.now(), phase: ev.phase }); - } else if (ev.message) { - currentStore.addLog(taskId, { type: 'text', ts: ev.ts || Date.now(), content: ev.message }); - } - break; - case 'tool_call': - currentStore.addLog(taskId, { - type: 'tool_call', - ts: ev.ts || Date.now(), - toolName: ev.toolName || ev.tool || ev.name || 'unknown-tool', - callId: ev.callId, - args: ev.args ?? ev.input - }); - break; - case 'tool_result': - currentStore.addLog(taskId, { - type: 'tool_result', - ts: ev.ts || Date.now(), - toolName: ev.toolName || ev.tool || ev.name || 'unknown-tool', - callId: ev.callId, - result: ev.result ?? ev.output - }); - break; - case 'diff': - currentStore.addLog(taskId, { - type: 'diff', - ts: ev.ts || Date.now(), - file: ev.file || ev.path || '', - patch: ev.patch ?? ev.patchText ?? ev.diff, - summary: ev.summary - }); - break; - case 'file_write': - currentStore.addLog(taskId, { - type: 'file_write', - ts: ev.ts || Date.now(), - path: ev.path || '', - bytes: ev.bytes - }); - break; - case 'metric': - currentStore.addLog(taskId, { - type: 'metric', - ts: ev.ts || Date.now(), - key: ev.key || '', - value: ev.value ?? 0, - unit: ev.unit - }); - break; - case 'artifact': - currentStore.addLog(taskId, { - type: 'artifact', - ts: ev.ts || Date.now(), - kind: ev.kind || 'artifact', - content: ev.content - }); - break; - case 'error': - currentStore.addLog(taskId, { type: 'text', ts: ev.ts || Date.now(), level: 'error', content: `error: ${ev.message || 'Unknown error'}` }); - currentStore.setRunning(taskId, false); - break; - case 'done': - currentStore.addLog(taskId, { type: 'text', ts: ev.ts || Date.now(), content: ev.success ? 'Agent run completed' : 'Agent run ended with errors' }); - currentStore.setRunning(taskId, false); - // Clean up subscription when done - currentStore.setUnsubscribe(taskId, null); - break; - default: - currentStore.addLog(taskId, `event: ${JSON.stringify(ev)}`); - } - }); + const unsubscribeFn = window.electronAPI?.onAgentEvent( + channel, + // biome-ignore lint/suspicious/noExplicitAny: I have no idea what the type is, but it's coming from the agent. + (ev: any) => { + const currentStore = get(); + + switch (ev?.type) { + case "token": + if ( + typeof ev.content === "string" && + ev.content.trim().length > 0 + ) { + currentStore.addLog(taskId, { + type: "text", + ts: ev.ts || Date.now(), + content: ev.content, + }); + } + break; + case "status": + if (ev.phase) { + currentStore.addLog(taskId, { + type: "status", + ts: ev.ts || Date.now(), + phase: ev.phase, + }); + } else if (ev.message) { + currentStore.addLog(taskId, { + type: "text", + ts: ev.ts || Date.now(), + content: ev.message, + }); + } + break; + case "tool_call": + currentStore.addLog(taskId, { + type: "tool_call", + ts: ev.ts || Date.now(), + toolName: ev.toolName || ev.tool || ev.name || "unknown-tool", + callId: ev.callId, + args: ev.args ?? ev.input, + }); + break; + case "tool_result": + currentStore.addLog(taskId, { + type: "tool_result", + ts: ev.ts || Date.now(), + toolName: ev.toolName || ev.tool || ev.name || "unknown-tool", + callId: ev.callId, + result: ev.result ?? ev.output, + }); + break; + case "diff": + currentStore.addLog(taskId, { + type: "diff", + ts: ev.ts || Date.now(), + file: ev.file || ev.path || "", + patch: ev.patch ?? ev.patchText ?? ev.diff, + summary: ev.summary, + }); + break; + case "file_write": + currentStore.addLog(taskId, { + type: "file_write", + ts: ev.ts || Date.now(), + path: ev.path || "", + bytes: ev.bytes, + }); + break; + case "metric": + currentStore.addLog(taskId, { + type: "metric", + ts: ev.ts || Date.now(), + key: ev.key || "", + value: ev.value ?? 0, + unit: ev.unit, + }); + break; + case "artifact": + currentStore.addLog(taskId, { + type: "artifact", + ts: ev.ts || Date.now(), + kind: ev.kind || "artifact", + content: ev.content, + }); + break; + case "error": + currentStore.addLog(taskId, { + type: "text", + ts: ev.ts || Date.now(), + level: "error", + content: `error: ${ev.message || "Unknown error"}`, + }); + currentStore.setRunning(taskId, false); + break; + case "done": + currentStore.addLog(taskId, { + type: "text", + ts: ev.ts || Date.now(), + content: ev.success + ? "Agent run completed" + : "Agent run ended with errors", + }); + currentStore.setRunning(taskId, false); + // Clean up subscription when done + currentStore.setUnsubscribe(taskId, null); + break; + default: + currentStore.addLog(taskId, `event: ${JSON.stringify(ev)}`); + } + }, + ); // Store the unsubscribe function store.setUnsubscribe(taskId, unsubscribeFn); @@ -224,25 +262,35 @@ export const useTaskExecutionStore = create()( selectRepositoryForTask: async (taskId: string) => { const store = get(); try { - const selected = await (window as any).electronAPI?.selectDirectory(); + const selected = await window.electronAPI?.selectDirectory(); if (selected) { - const isRepo = await (window as any).electronAPI?.validateRepo(selected); + const isRepo = await window.electronAPI?.validateRepo(selected); if (!isRepo) { - store.addLog(taskId, `Selected folder is not a git repository: ${selected}`); + store.addLog( + taskId, + `Selected folder is not a git repository: ${selected}`, + ); return; } - const canWrite = await (window as any).electronAPI?.checkWriteAccess(selected); + const canWrite = + await window.electronAPI?.checkWriteAccess(selected); if (!canWrite) { - store.addLog(taskId, `No write permission in selected folder: ${selected}`); - const { response } = await (window as any).electronAPI?.showMessageBox({ - type: 'warning', - title: 'Folder is not writable', - message: 'The selected folder is not writable by the app.', - detail: 'Grant access by selecting a different folder or adjusting permissions.', - buttons: ['Grant Access', 'Cancel'], + store.addLog( + taskId, + `No write permission in selected folder: ${selected}`, + ); + const result = await window.electronAPI?.showMessageBox({ + type: "warning", + title: "Folder is not writable", + message: "The selected folder is not writable by the app.", + detail: + "Grant access by selecting a different folder or adjusting permissions.", + buttons: ["Grant Access", "Cancel"], defaultId: 0, cancelId: 1, }); + if (!result) return; + const { response } = result; if (response === 0) { // Let user reselect and validate again return store.selectRepositoryForTask(taskId); @@ -252,11 +300,17 @@ export const useTaskExecutionStore = create()( store.setRepoPath(taskId, selected); } } catch (err) { - store.addLog(taskId, `Error selecting directory: ${err instanceof Error ? err.message : String(err)}`); + store.addLog( + taskId, + `Error selecting directory: ${err instanceof Error ? err.message : String(err)}`, + ); } }, - runTask: async (taskId: string, task: { id: string; title: string; description?: string }) => { + runTask: async ( + taskId: string, + task: { id: string; title: string; description?: string }, + ) => { const store = get(); const taskState = store.getTaskState(taskId); @@ -270,28 +324,39 @@ export const useTaskExecutionStore = create()( } if (!effectiveRepoPath) { - store.addLog(taskId, 'No repository folder selected.'); + store.addLog(taskId, "No repository folder selected."); return; } - const isRepo = await (window as any).electronAPI?.validateRepo(effectiveRepoPath); + const isRepo = + await window.electronAPI?.validateRepo(effectiveRepoPath); if (!isRepo) { - store.addLog(taskId, `Selected folder is not a git repository: ${effectiveRepoPath}`); + store.addLog( + taskId, + `Selected folder is not a git repository: ${effectiveRepoPath}`, + ); return; } - const canWrite = await (window as any).electronAPI?.checkWriteAccess(effectiveRepoPath); + const canWrite = + await window.electronAPI?.checkWriteAccess(effectiveRepoPath); if (!canWrite) { - store.addLog(taskId, `No write permission in selected folder: ${effectiveRepoPath}`); - const { response } = await (window as any).electronAPI?.showMessageBox({ - type: 'warning', - title: 'Folder is not writable', - message: 'This folder is not writable by the app.', - detail: 'Grant access by selecting a different folder or adjusting permissions.', - buttons: ['Grant Access', 'Cancel'], + store.addLog( + taskId, + `No write permission in selected folder: ${effectiveRepoPath}`, + ); + const result = await window.electronAPI?.showMessageBox({ + type: "warning", + title: "Folder is not writable", + message: "This folder is not writable by the app.", + detail: + "Grant access by selecting a different folder or adjusting permissions.", + buttons: ["Grant Access", "Cancel"], defaultId: 0, cancelId: 1, }); + if (!result) return; + const { response } = result; if (response === 0) { await store.selectRepositoryForTask(taskId); } @@ -302,32 +367,51 @@ export const useTaskExecutionStore = create()( const promptLines: string[] = []; promptLines.push(`Task: ${task.title}`); if (task.description) { - promptLines.push('Description:'); + promptLines.push("Description:"); promptLines.push(task.description); } - const prompt = promptLines.join('\n'); + const prompt = promptLines.join("\n"); const currentTaskState = store.getTaskState(taskId); store.setRunning(taskId, true); store.setLogs(taskId, [ - { type: 'text', ts: Date.now(), content: `Starting ${currentTaskState.runMode} Claude Code agent...` }, - { type: 'text', ts: Date.now(), content: `Repo: ${effectiveRepoPath}` }, + { + type: "text", + ts: Date.now(), + content: `Starting ${currentTaskState.runMode} Claude Code agent...`, + }, + { + type: "text", + ts: Date.now(), + content: `Repo: ${effectiveRepoPath}`, + }, ]); try { - const { taskId: executionTaskId, channel } = await (window as any).electronAPI?.agentStart({ + const result = await window.electronAPI?.agentStart({ prompt, repoPath: effectiveRepoPath, - model: 'claude-4-sonnet', + model: "claude-4-sonnet", }); + if (!result) { + store.addLog( + taskId, + "Failed to start agent: electronAPI not available", + ); + return; + } + const { taskId: executionTaskId, channel } = result; store.setCurrentTaskId(taskId, executionTaskId); // Subscribe to streaming events using store-managed subscription store.subscribeToAgentEvents(taskId, channel); } catch (error) { - store.addLog(taskId, `Error starting agent: ${error instanceof Error ? error.message : 'Unknown error'}`); + store.addLog( + taskId, + `Error starting agent: ${error instanceof Error ? error.message : "Unknown error"}`, + ); store.setRunning(taskId, false); } }, @@ -339,7 +423,7 @@ export const useTaskExecutionStore = create()( if (!taskState.currentTaskId) return; try { - await (window as any).electronAPI?.agentCancel(taskState.currentTaskId); + await window.electronAPI?.agentCancel(taskState.currentTaskId); } catch { // Ignore cancellation errors } @@ -353,16 +437,16 @@ export const useTaskExecutionStore = create()( }, }), { - name: 'task-execution-storage', + name: "task-execution-storage", // Don't persist unsubscribe functions as they can't be serialized partialize: (state) => ({ taskStates: Object.fromEntries( Object.entries(state.taskStates).map(([taskId, taskState]) => [ taskId, - { ...taskState, unsubscribe: null } - ]) + { ...taskState, unsubscribe: null }, + ]), ), }), - } - ) -); \ No newline at end of file + }, + ), +); diff --git a/src/renderer/stores/taskStore.ts b/src/renderer/stores/taskStore.ts index 99e4c17f5..c847afc7c 100644 --- a/src/renderer/stores/taskStore.ts +++ b/src/renderer/stores/taskStore.ts @@ -1,13 +1,13 @@ -import { create } from 'zustand'; -import { Task } from '@shared/types'; -import { useAuthStore } from './authStore'; +import type { Task } from "@shared/types"; +import { create } from "zustand"; +import { useAuthStore } from "./authStore"; interface TaskState { tasks: Task[]; selectedTaskId: string | null; isLoading: boolean; error: string | null; - + fetchTasks: () => Promise; selectTask: (taskId: string | null) => void; refreshTask: (taskId: string) => Promise; @@ -18,40 +18,40 @@ export const useTaskStore = create((set, get) => ({ selectedTaskId: null, isLoading: false, error: null, - + fetchTasks: async () => { const client = useAuthStore.getState().client; if (!client) return; - + set({ isLoading: true, error: null }); - + try { const tasks = await client.getTasks(); set({ tasks, isLoading: false }); } catch (error) { - set({ - error: error instanceof Error ? error.message : 'Failed to fetch tasks', - isLoading: false + set({ + error: error instanceof Error ? error.message : "Failed to fetch tasks", + isLoading: false, }); } }, - + selectTask: (taskId: string | null) => { set({ selectedTaskId: taskId }); }, - + refreshTask: async (taskId: string) => { const client = useAuthStore.getState().client; if (!client) return; - + try { const updatedTask = await client.getTask(taskId); - const tasks = get().tasks.map(task => - task.id === taskId ? updatedTask : task + const tasks = get().tasks.map((task) => + task.id === taskId ? updatedTask : task, ); set({ tasks }); } catch (error) { - console.error('Failed to refresh task:', error); + console.error("Failed to refresh task:", error); } }, -})); \ No newline at end of file +})); diff --git a/src/renderer/stores/themeStore.ts b/src/renderer/stores/themeStore.ts index dcd657d86..88cfbc7b9 100644 --- a/src/renderer/stores/themeStore.ts +++ b/src/renderer/stores/themeStore.ts @@ -1,5 +1,5 @@ -import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; +import { create } from "zustand"; +import { persist } from "zustand/middleware"; interface ThemeStore { isDarkMode: boolean; @@ -15,7 +15,7 @@ export const useThemeStore = create()( setDarkMode: (isDarkMode) => set({ isDarkMode }), }), { - name: 'theme-storage', // name of the storage key - } - ) -); \ No newline at end of file + name: "theme-storage", // name of the storage key + }, + ), +); diff --git a/src/renderer/stores/workflowStore.ts b/src/renderer/stores/workflowStore.ts index 0b890f77c..4274345dc 100644 --- a/src/renderer/stores/workflowStore.ts +++ b/src/renderer/stores/workflowStore.ts @@ -1,14 +1,14 @@ -import { create } from 'zustand'; -import { Task, Workflow } from '@shared/types'; -import { useAuthStore } from './authStore'; -import { useTaskStore } from './taskStore'; +import type { Task, Workflow } from "@shared/types"; +import { create } from "zustand"; +import { useAuthStore } from "./authStore"; +import { useTaskStore } from "./taskStore"; interface WorkflowState { workflows: Workflow[]; selectedWorkflowId: string | null; isLoading: boolean; error: string | null; - + fetchWorkflows: () => Promise; selectWorkflow: (workflowId: string | null) => void; getTasksByStage: (workflowId: string) => Map; @@ -19,51 +19,54 @@ export const useWorkflowStore = create((set, get) => ({ selectedWorkflowId: null, isLoading: false, error: null, - + fetchWorkflows: async () => { const client = useAuthStore.getState().client; if (!client) return; - + set({ isLoading: true, error: null }); - + try { const workflows = await client.getWorkflows(); - set({ - workflows, + set({ + workflows, isLoading: false, // Select first active workflow by default - selectedWorkflowId: workflows.find(w => w.is_active && w.is_default)?.id || workflows[0]?.id + selectedWorkflowId: + workflows.find((w) => w.is_active && w.is_default)?.id || + workflows[0]?.id, }); } catch (error) { - set({ - error: error instanceof Error ? error.message : 'Failed to fetch workflows', - isLoading: false + set({ + error: + error instanceof Error ? error.message : "Failed to fetch workflows", + isLoading: false, }); } }, - + selectWorkflow: (workflowId: string | null) => { set({ selectedWorkflowId: workflowId }); }, - + getTasksByStage: (workflowId: string) => { const tasks = useTaskStore.getState().tasks; - const workflow = get().workflows.find(w => w.id === workflowId); - + const workflow = get().workflows.find((w) => w.id === workflowId); + const tasksByStage = new Map(); - + if (!workflow) return tasksByStage; - + // Initialize all stages workflow.stages - .filter(stage => !stage.is_archived) + .filter((stage) => !stage.is_archived) .sort((a, b) => a.position - b.position) - .forEach(stage => { + .forEach((stage) => { tasksByStage.set(stage.id, []); }); - + // Group tasks by stage - tasks.forEach(task => { + tasks.forEach((task) => { if (task.workflow === workflowId && task.current_stage) { const stageList = tasksByStage.get(task.current_stage); if (stageList) { @@ -71,7 +74,7 @@ export const useWorkflowStore = create((set, get) => ({ } } }); - + return tasksByStage; }, -})); \ No newline at end of file +})); diff --git a/src/renderer/styles/globals.css b/src/renderer/styles/globals.css index f0b0d2c9c..ba7f26d68 100644 --- a/src/renderer/styles/globals.css +++ b/src/renderer/styles/globals.css @@ -3,9 +3,9 @@ @tailwind utilities; body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", + "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -43,4 +43,4 @@ body { .no-drag { -webkit-app-region: no-drag; -} \ No newline at end of file +} diff --git a/src/renderer/types/electron.d.ts b/src/renderer/types/electron.d.ts index e08af1375..5e7321239 100644 --- a/src/renderer/types/electron.d.ts +++ b/src/renderer/types/electron.d.ts @@ -5,7 +5,7 @@ export interface IElectronAPI { validateRepo: (directoryPath: string) => Promise; checkWriteAccess: (directoryPath: string) => Promise; showMessageBox: (options: { - type?: 'none' | 'info' | 'error' | 'question' | 'warning'; + type?: "none" | "info" | "error" | "question" | "warning"; title?: string; message?: string; detail?: string; @@ -13,13 +13,20 @@ export interface IElectronAPI { defaultId?: number; cancelId?: number; }) => Promise<{ response: number }>; - agentStart: (params: { prompt: string; repoPath: string; model?: string }) => Promise<{ taskId: string; channel: string }>; + agentStart: (params: { + prompt: string; + repoPath: string; + model?: string; + }) => Promise<{ taskId: string; channel: string }>; agentCancel: (taskId: string) => Promise; - onAgentEvent: (channel: string, listener: (event: any) => void) => () => void; + onAgentEvent: ( + channel: string, + listener: (event: unknown) => void, + ) => () => void; } declare global { interface Window { electronAPI: IElectronAPI; } -} \ No newline at end of file +} diff --git a/src/renderer/types/log.ts b/src/renderer/types/log.ts index d8609ba35..e650f082d 100644 --- a/src/renderer/types/log.ts +++ b/src/renderer/types/log.ts @@ -1,18 +1,50 @@ export type LogEntry = - | { type: 'text'; ts: number; content: string; level?: 'info' | 'warn' | 'error' } - | { type: 'status'; ts: number; phase: 'queued' | 'planning' | 'executing' | 'tool' | 'finalizing' | 'done' } - | { type: 'tool_call'; ts: number; toolName: string; callId?: string; args?: unknown } - | { type: 'tool_result'; ts: number; toolName: string; callId?: string; result?: unknown } - | { type: 'diff'; ts: number; file: string; patch?: string; summary?: { added: number; removed: number } } - | { type: 'file_write'; ts: number; path: string; bytes?: number } - | { type: 'metric'; ts: number; key: string; value: number; unit?: string } - | { type: 'artifact'; ts: number; kind: string; content?: unknown }; + | { + type: "text"; + ts: number; + content: string; + level?: "info" | "warn" | "error"; + } + | { + type: "status"; + ts: number; + phase: + | "queued" + | "planning" + | "executing" + | "tool" + | "finalizing" + | "done"; + } + | { + type: "tool_call"; + ts: number; + toolName: string; + callId?: string; + args?: unknown; + } + | { + type: "tool_result"; + ts: number; + toolName: string; + callId?: string; + result?: unknown; + } + | { + type: "diff"; + ts: number; + file: string; + patch?: string; + summary?: { added: number; removed: number }; + } + | { type: "file_write"; ts: number; path: string; bytes?: number } + | { type: "metric"; ts: number; key: string; value: number; unit?: string } + | { type: "artifact"; ts: number; kind: string; content?: unknown }; export function formatTime(ts: number): string { try { return new Date(ts).toLocaleTimeString(); } catch { - return ''; + return ""; } } - diff --git a/src/shared/types.ts b/src/shared/types.ts index cc58acacd..53260c233 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -6,14 +6,11 @@ export interface Task { updated_at: string; origin_product: string; status?: string; - current_stage?: string; // Stage ID - workflow?: string; // Workflow ID - repository_config?: { - organization: string; - repository: string; - }; - github_branch?: string; - github_pr_url?: string; + current_stage?: string | null; // Stage ID + workflow?: string | null; // Workflow ID + repository_config?: unknown; + github_branch?: string | null; + github_pr_url?: string | null; tags?: string[]; } @@ -21,8 +18,8 @@ export interface Workflow { id: string; name: string; description?: string; - is_default: boolean; - is_active: boolean; + is_default?: boolean; + is_active?: boolean; stages: WorkflowStage[]; } @@ -32,11 +29,11 @@ export interface WorkflowStage { name: string; key: string; position: number; - color: string; - agent_name?: string; - is_manual_only: boolean; - is_archived: boolean; - fallback_stage?: string; + color?: string; + agent_name?: string | null; + is_manual_only?: boolean; + is_archived?: boolean; + fallback_stage?: string | null; } export interface AuthConfig { @@ -56,14 +53,14 @@ export interface User { export interface LogEntry { id: string; timestamp: string; - level: 'info' | 'warning' | 'error' | 'success'; + level: "info" | "warning" | "error" | "success"; message: string; - data?: any; + data?: unknown; } export interface TabState { id: string; - type: 'task-list' | 'task-detail' | 'workflow' | 'backlog'; + type: "task-list" | "task-detail" | "workflow" | "backlog"; title: string; - data?: any; -} \ No newline at end of file + data?: Task | unknown; +} diff --git a/tailwind.config.js b/tailwind.config.js index 6a6cadcd5..afa30f227 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,40 +1,37 @@ -import { radixThemePreset } from 'radix-themes-tw'; +import { radixThemePreset } from "radix-themes-tw"; /** @type {import('tailwindcss').Config} */ module.exports = { presets: [radixThemePreset], - content: [ - "./index.html", - "./src/**/*.{js,ts,jsx,tsx}", - ], + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: { colors: { posthog: { - 50: '#fef2f2', - 100: '#fee2e2', - 200: '#fecaca', - 300: '#fca5a5', - 400: '#f87171', - 500: '#ef4444', - 600: '#dc2626', - 700: '#b91c1c', - 800: '#991b1b', - 900: '#7f1d1d', + 50: "#fef2f2", + 100: "#fee2e2", + 200: "#fecaca", + 300: "#fca5a5", + 400: "#f87171", + 500: "#ef4444", + 600: "#dc2626", + 700: "#b91c1c", + 800: "#991b1b", + 900: "#7f1d1d", }, dark: { - bg: '#0a0a0a', - surface: '#1a1a1a', - border: '#2a2a2a', - text: '#e5e5e5', - 'text-muted': '#a3a3a3', + bg: "#0a0a0a", + surface: "#1a1a1a", + border: "#2a2a2a", + text: "#e5e5e5", + "text-muted": "#a3a3a3", }, }, fontFamily: { - mono: ['JetBrains Mono', 'Consolas', 'Monaco', 'monospace'], + mono: ["JetBrains Mono", "Consolas", "Monaco", "monospace"], }, }, }, plugins: [], - darkMode: 'class', -}; \ No newline at end of file + darkMode: "class", +}; diff --git a/tsconfig.json b/tsconfig.json index 68e0b4071..1c689da73 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,21 +18,11 @@ "noUnusedLocals": true, "noUnusedParameters": true, "paths": { - "@/*": [ - "./*" - ], - "@api/*": [ - "api/*" - ], - "@main/*": [ - "main/*" - ], - "@renderer/*": [ - "renderer/*" - ], - "@shared/*": [ - "shared/*" - ] + "@/*": ["./*"], + "@api/*": ["api/*"], + "@main/*": ["main/*"], + "@renderer/*": ["renderer/*"], + "@shared/*": ["shared/*"] }, "resolveJsonModule": true, "skipLibCheck": true, @@ -40,12 +30,6 @@ "strict": true, "target": "esnext" }, - "exclude": [ - "node_modules", - "dist", - "release" - ], - "include": [ - "src/**/*" - ] -} \ No newline at end of file + "exclude": ["node_modules", "dist", "release"], + "include": ["src/**/*"] +} diff --git a/tsconfig.node.json b/tsconfig.node.json index 2d0b30631..8100b619a 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,23 +1,14 @@ { - "$schema": "https://json.schemastore.org/tsconfig", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "noEmit": false, - "module": "ES2022", - "moduleResolution": "node", - "verbatimModuleSyntax": true, - "types": [ - "node" - ] - }, - "exclude": [ - "src/renderer/**/*" - ], - "extends": "./tsconfig.json", - "include": [ - "src/main/**/*", - "src/shared/**/*", - "src/api/**/*" - ] -} \ No newline at end of file + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "noEmit": false, + "module": "ES2022", + "moduleResolution": "node", + "types": ["node"] + }, + "exclude": ["src/renderer/**/*", "src/main/preload.ts"], + "extends": "./tsconfig.json", + "include": ["src/main/**/*", "src/shared/**/*", "src/api/**/*"] +} diff --git a/tsconfig.preload.json b/tsconfig.preload.json new file mode 100644 index 000000000..5f8333772 --- /dev/null +++ b/tsconfig.preload.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "noEmit": false, + "module": "CommonJS", + "moduleResolution": "node", + "types": ["node"] + }, + "extends": "./tsconfig.json", + "include": ["src/main/preload.ts"] +} diff --git a/tsconfig.web.json b/tsconfig.web.json index 4b9431f8b..4c255c4e9 100644 --- a/tsconfig.web.json +++ b/tsconfig.web.json @@ -2,25 +2,12 @@ "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "jsx": "react-jsx", - "lib": [ - "ESNext", - "DOM", - "DOM.Iterable" - ], + "lib": ["ESNext", "DOM", "DOM.Iterable"], "outDir": "./dist/renderer", "target": "ESNext", - "types": [ - "vite/client" - ] + "types": ["vite/client"] }, - "exclude": [ - "src/main/**/*", - "src/api/repoDetector.ts" - ], + "exclude": ["src/main/**/*", "src/api/repoDetector.ts"], "extends": "./tsconfig.json", - "include": [ - "src/renderer/**/*", - "src/shared/**/*", - "src/api/**/*" - ] -} \ No newline at end of file + "include": ["src/renderer/**/*", "src/shared/**/*", "src/api/**/*"] +} diff --git a/vite.config.mts b/vite.config.mts index 85f7c1539..112f9be0c 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -1,15 +1,15 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import path from 'path'; -import { fileURLToPath } from 'url'; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); export default defineConfig({ plugins: [react()], - base: './', + base: "./", build: { - outDir: 'dist/renderer', + outDir: "dist/renderer", emptyOutDir: true, }, server: { @@ -18,11 +18,11 @@ export default defineConfig({ }, resolve: { alias: { - '@': path.resolve(__dirname, './src'), - '@main': path.resolve(__dirname, './src/main'), - '@renderer': path.resolve(__dirname, './src/renderer'), - '@shared': path.resolve(__dirname, './src/shared'), - '@api': path.resolve(__dirname, './src/api'), + "@": path.resolve(__dirname, "./src"), + "@main": path.resolve(__dirname, "./src/main"), + "@renderer": path.resolve(__dirname, "./src/renderer"), + "@shared": path.resolve(__dirname, "./src/shared"), + "@api": path.resolve(__dirname, "./src/api"), }, }, -}); \ No newline at end of file +});