Skip to content

refactor: reorganize codebase into proper pnpm workspace packages#45

Merged
jbdevprimary merged 2 commits into
mainfrom
fix/git-diff-implementation
Jan 18, 2026
Merged

refactor: reorganize codebase into proper pnpm workspace packages#45
jbdevprimary merged 2 commits into
mainfrom
fix/git-diff-implementation

Conversation

@jbdevprimary
Copy link
Copy Markdown
Contributor

@jbdevprimary jbdevprimary commented Jan 18, 2026

Summary

  • Create @thumbcode/types - Domain-specific type definitions (agents, projects, workspaces, credentials, chat, user, navigation, api, events)
  • Create @thumbcode/config - Environment configuration, constants, and feature flags
  • Update @thumbcode/core - Full GitService implementation (including diff() fix) and CredentialService with secure storage
  • Update @thumbcode/state - Complete Zustand stores with devtools, persist middleware, and selectors
  • Update @thumbcode/ui - Modern NativeWind v4 patterns, ThemeProvider with P3 "Warm Technical" tokens

Key Changes

  1. GitService.diff() Implementation - Previously a stub, now uses tree-walking algorithm for proper unified diffs
  2. CredentialService - Full expo-secure-store integration with biometric auth support
  3. Linter Fixes - Reduced cognitive complexity in status() method, fixed non-null assertions in tests
  4. Modern Patterns - Removed deprecated NativeWind styled() API, uses direct className approach

Breaking Changes

None - packages are additive and existing src/ code continues to work.

Test plan

  • Run pnpm lint - passes with no warnings
  • Run pnpm test - all 114 tests passing
  • Verify package linking with pnpm install

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Secure credential management with biometric support and validation
    • Built-in Git operations: clone, commit, push, pull, diffs, branch and repo management
    • Environment-aware feature flags with runtime overrides
  • UI

    • Expanded component library: Button, Input, Alert (now supports title/info), Spinner (optional label), Card, Text, Container, Header, theme provider
  • State

    • New centralized stores for agents, chat (threads/messages), projects, credentials, and user preferences (persistent, typed)
  • Chores

    • Reorganized into modular workspace packages and consolidated type definitions

✏️ Tip: You can customize this high-level summary in your review settings.

- Create @thumbcode/types package with domain-specific type definitions
  - agents, projects, workspaces, credentials, chat, user, navigation, api, events
- Create @thumbcode/config package with environment and app configuration
  - env.ts with environment validation
  - constants.ts with API URLs, OAuth config, storage keys
  - features.ts with feature flag system
- Update @thumbcode/core with full GitService and CredentialService implementations
  - GitService.diff() now uses tree-walking algorithm for proper diffs
  - CredentialService with expo-secure-store and biometric auth
- Update @thumbcode/state with complete Zustand stores
  - agentStore, chatStore, credentialStore, projectStore, userStore
  - All stores have devtools, persist middleware, and selectors
- Update @thumbcode/ui with modern NativeWind v4 patterns
  - Remove deprecated styled() API usage
  - Add ThemeProvider with P3 "Warm Technical" tokens
  - Proper primitives, form, feedback, and layout components
- Fix linter warnings:
  - Reduce cognitive complexity in GitService.status() with lookup map
  - Fix non-null assertions in test files
- All 114 tests passing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 18, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

This PR adds five workspace packages (@thumbcode/config, @thumbcode/core, @thumbcode/state, @thumbcode/types, @thumbcode/ui), introducing centralized configuration and feature flags, a mobile-native Git service, secure credential management with biometrics, comprehensive typed domain models, and a React Native UI library plus extensive Zustand stores.

Changes

Cohort / File(s) Summary
Root Workspace Setup
package.json, pnpm-workspace.yaml
Added workspace deps for new packages and minor comment edit in pnpm-workspace.
Config package
packages/config/package.json, packages/config/src/index.ts, packages/config/src/constants.ts, packages/config/src/env.ts, packages/config/src/features.ts
New package with constants, env config/validation, and feature-flag system; exports typed config surface.
Core — Credentials
packages/core/package.json, packages/core/src/credentials/* (CredentialService.ts, index.ts, types.ts, removed secureStore.ts)
Replaced simple secureStore with a comprehensive CredentialService (biometric gating, validations, metadata); explicit credential types and index exports.
Core — Git
packages/core/src/git/* (types.ts, GitService.ts, index.ts, removed client.ts, removed operations.ts), src/services/git/GitService.ts
New mobile-native GitService (isomorphic-git + Expo FS adapter): clone/fetch/pull/push/commit/diff/status/branches/logs + typed Git API; removed legacy client/operations modules.
Core index
packages/core/src/index.ts
Re-exports GitService, CredentialService, and related types.
State package setup
packages/state/package.json, packages/state/src/index.ts
Added typed exports and exports map; moved devDependency to peerDependencies.
State — Stores
packages/state/src/* (removed legacy agent.ts, chat.ts, project.ts, user.ts; added agentStore.ts, chatStore.ts, credentialStore.ts, projectStore.ts, userStore.ts)
Reworked Zustand stores with persist/immer/devtools, richer types, selectors, and persistence partializers.
Types package
packages/types/package.json, packages/types/src/* (agents, api, chat, credentials, events, navigation, projects, user, workspaces, index)
New comprehensive domain type package covering agents, projects, workspaces, chat, credentials, API shapes, navigation, and event system.
UI package
packages/ui/package.json, many packages/ui/src/* (primitives, form, feedback, layout, theme, index)
New RN component library: Text primitive, ThemeProvider/hooks, Card/Container/Header, Button/Input, Alert/Spinner, reworked exports and named exports.
Small refactors / tests / misc
src/stores/* tests & stores, packages/agent-intelligence/src/components/chat/ChatInput.tsx
Replaced deprecated substr usage with slice across ID generation; small test variable initialization tweaks.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant GitService
  participant FS as "Expo FileSystem"
  participant Remote as "Git Remote (HTTP)"

  Client->>GitService: request clone/fetch/push/commit
  activate GitService
  GitService->>FS: read/write files, create worktree
  FS-->>GitService: file IO result
  GitService->>Remote: authenticate + send/receive pack objects
  Remote-->>GitService: remote response / progress
  GitService-->>Client: GitResult{ success, data | error }
  deactivate GitService
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 A hop through packages, tokens, and stores,

I stitched Git, creds, types, and UI doors.
With feathers of code and a twitch of my nose,
Agents awaken where the new state grows.
Hooray — may builds be green wherever it goes!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: reorganizing the codebase into proper pnpm workspace packages. It is specific, directly related to the changeset, and reflects the primary objective.
Docstring Coverage ✅ Passed Docstring coverage is 91.67% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Replace all instances of .substr(2, 9) with .slice(2, 11) across:
- packages/state/src/*.ts
- src/stores/*.ts
- packages/agent-intelligence/src/components/chat/ChatInput.tsx

The substr() method is deprecated in favor of slice().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@jbdevprimary jbdevprimary merged commit d96dc33 into main Jan 18, 2026
7 checks passed
@jbdevprimary jbdevprimary deleted the fix/git-diff-implementation branch January 18, 2026 19:59
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
66.7% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 Fix all issues with AI agents
In `@packages/config/src/features.ts`:
- Around line 29-133: The exported function getFeatureConfig references the
non-exported interface FeatureFlagConfig which triggers TS4025; fix by exporting
the interface (change "interface FeatureFlagConfig" to "export interface
FeatureFlagConfig") so the type is public, or alternatively change
getFeatureConfig's signature to return an inline/public type; update the
declaration near FEATURE_FLAGS and ensure any other uses (e.g.,
isFeatureEnabled, getEnabledFeatures) still import/consume the exported type as
needed.

In `@packages/core/package.json`:
- Line 13: Update the `@types/diff` devDependency to match the installed diff
package version: change the `@types/diff` entry to ^7.0.0 so it aligns with
"diff": "^7.0.0" in package.json; locate the package.json dependency block that
contains "diff" and update the "@types/diff" version accordingly (and do the
same for the other occurrence noted at lines 21-21).

In `@packages/core/src/credentials/CredentialService.ts`:
- Around line 219-348: The validation functions (validateGitHubToken,
validateAnthropicKey, validateOpenAIKey) currently treat 403/404 responses as
hard failures and can reject valid credentials with insufficient permissions or
missing model access; change them so only 401 returns isValid: false while 403
(and for Anthropic 404) return isValid: true with messages indicating "valid but
insufficient permissions/model access" (e.g., for validateGitHubToken return {
isValid: true, message: 'Valid GitHub token but missing user scope' } on 403 and
preserve metadata extraction; for validateAnthropicKey treat 403 as { isValid:
true, message: 'Anthropic API key valid but lacks model access' } and 404 as {
isValid: true, message: 'Anthropic API key valid but model not found' }; for
validateOpenAIKey treat 403 as { isValid: true, message: 'OpenAI API key valid
but insufficient permissions' }). Only keep 401 as invalid and preserve existing
429 handling and header-based metadata.

In `@packages/state/src/agentStore.ts`:
- Around line 177-180: clearTasks currently empties state.tasks but leaves
agents[*].currentTaskId pointing to removed tasks; update clearTasks to also
clear/reset currentTaskId for all agents (e.g., iterate state.agents and set
each agent.currentTaskId = null/undefined or rebuild agents with currentTaskId
cleared) so the store remains consistent; modify the clearTasks setter (the
function passed to set in agentStore.ts) to clear state.tasks AND reset
agents[*].currentTaskId.

In `@packages/state/src/chatStore.ts`:
- Around line 210-246: When removing messages in deleteMessage or wiping them in
clearThread, also recompute and update the thread's metadata (state.unreadCount
and state.lastMessageAt) so they don't remain stale; after you mutate
state.messages[threadId], set state.unreadCount[threadId] =
remainingMessages.filter(m => !m.read).length (or the equivalent unread flag in
your Message type) and set state.lastMessageAt[threadId] =
remainingMessages.length ? the newest message's timestamp (e.g.,
message.createdAt or message.updatedAt) : null/undefined, ensuring both
deleteMessage and clearThread apply the same recalculation logic so sorting and
badges remain correct.

In `@packages/state/src/projectStore.ts`:
- Around line 127-135: The removeProject updater only clears activeProjectId,
workspace, and fileTree; extend it to also clear any branch/commit-related state
when the removed project was active so stale data doesn't persist. In the
removeProject function, after setting activeProjectId = null, also set/clear any
branch/commit fields present in this store (e.g., activeBranchId,
activeCommitId, branches, commits, currentBranch, currentCommit or similarly
named properties) to null/empty arrays as appropriate so all project-scoped
state is reset when the active project is removed.

In `@packages/types/src/api.ts`:
- Around line 118-128: The Message.role property in the Anthropic namespace is
too narrow; update the Anthropic.Message interface's role type (the role field
on Anthropic.Message) from the literal 'assistant' to the full union 'user' |
'assistant' | 'tool_inputs' | 'tool_outputs' so it matches Anthropic's API;
locate the Anthropic.Message interface and replace the role type accordingly,
ensuring any places that construct or check Message.role accept the expanded
union.

In `@packages/types/src/user.ts`:
- Around line 30-36: UserPreferences declares hapticFeedback but the runtime
state type UserSettings (in userStore.ts) lacks it; update the state to match
the canonical type by adding hapticFeedback:boolean to the UserSettings shape
and its initialization/defaults in the user store (ensure any
persistence/serialization and reducers/selectors that construct or read
UserSettings are updated to provide a default value and to persist/restore this
field), or alternatively remove hapticFeedback from UserPreferences if it should
not be persisted—refer to the UserPreferences and UserSettings symbols to locate
and make the change.
- Around line 41-49: The NotificationPreferences type in the types package
(NotificationPreferences) is out of sync with the state store's
NotificationPreferences; update the state store so both definitions match or
make the state store import and use this canonical type instead: add the missing
fields errorAlerts and dailySummary to the state store's NotificationPreferences
type (or replace its local definition with an import of NotificationPreferences
from the types package) and update any usages in userStore to satisfy the
expanded shape.

In `@packages/ui/src/layout/Card.tsx`:
- Line 27: The className in Card.tsx is using an unsupported multi-value
arbitrary border-radius `rounded-[1rem_0.75rem_1.25rem_0.5rem]`; replace it with
individual corner utilities so NativeWind/Tailwind can apply asymmetric radii —
e.g. use `rounded-tl-[1rem] rounded-tr-[0.75rem] rounded-br-[1.25rem]
rounded-bl-[0.5rem]` in the same className (locate the string containing
`rounded-[1rem_0.75rem_1.25rem_0.5rem]` and swap it for these four utilities).
🧹 Nitpick comments (24)
packages/ui/src/primitives/Text.tsx (1)

52-56: Consider trimming the composed className to avoid trailing whitespace.

When className is empty, the template literal produces a trailing space (e.g., "font-body text-base font-normal "). This is functionally harmless but could be cleaner.

Optional fix using trim or filter
  return (
-   <RNText className={`${variantClass} ${sizeClass} ${weightClass} ${className}`} {...props}>
+   <RNText className={[variantClass, sizeClass, weightClass, className].filter(Boolean).join(' ')} {...props}>
      {children}
    </RNText>
  );
packages/ui/src/feedback/Spinner.tsx (2)

19-29: Consider accessibility improvements.

The Spinner lacks accessibility hints. When a label is present, users relying on screen readers would benefit from an accessibility label on the container.

Suggested accessibility enhancement
-export function Spinner({ size = 'large', color = '#FF7059', label }: SpinnerProps) {
+export function Spinner({ size = 'large', color = '#FF7059', label }: SpinnerProps) {
   return (
-    <View className="items-center">
+    <View 
+      className="items-center" 
+      accessible={true}
+      accessibilityRole="progressbar"
+      accessibilityLabel={label ?? 'Loading'}
+    >
       <ActivityIndicator size={size} color={color} />

4-8: Consider using a theme token instead of a hardcoded color.

The default color #FF7059 is hardcoded. If the theme defines a coral-500 token (as mentioned in the JSDoc), referencing it via a theme constant would improve maintainability.

packages/ui/src/layout/Container.tsx (1)

16-27: Same trailing whitespace consideration applies here.

The className concatenation pattern matches Text.tsx. For consistency, consider applying the same refinement if you choose to address it there.

Optional fix
   return (
-    <View className={`${variantClasses} ${className}`} {...props}>
+    <View className={[variantClasses, className].filter(Boolean).join(' ')} {...props}>
       {children}
     </View>
   );
packages/ui/package.json (1)

19-19: Consider moving react-native to peerDependencies.

react-native is pinned exactly at 0.76.0 as a direct dependency, but react is correctly listed as a peer dependency. This inconsistency could cause version conflicts or duplicate installations when consumers use a different React Native version.

Proposed fix
   "dependencies": {
     "@expo/vector-icons": "^14.0.0",
-    "nativewind": "^4.1.0",
-    "react-native": "0.76.0"
+    "nativewind": "^4.1.0"
   },
   "peerDependencies": {
     "expo-router": ">=4.0.0",
-    "react": ">=18.0.0"
+    "react": ">=18.0.0",
+    "react-native": ">=0.70.0"
   }
packages/ui/src/theme/ThemeProvider.tsx (1)

127-136: Consider stricter typing for the shade parameter.

The shade parameter is typed as string, but valid shades are specific keys like '400', '500', etc. This allows invalid shade strings to be passed without compile-time errors.

Proposed improvement for type safety
+type ColorShade = '50' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
+
-export function useColor(colorName: keyof typeof tokens.colors, shade: string = '500'): string {
+export function useColor(colorName: keyof typeof tokens.colors, shade: ColorShade = '500'): string {
packages/ui/src/layout/Header.tsx (1)

25-25: Hardcoded colors bypass the new theme system.

The header uses hardcoded "white" for the icon color and text-white class, while the PR introduces a ThemeProvider with design tokens. Consider using theme tokens for consistency.

If the header should always be white regardless of theme, this is fine. Otherwise, consider deriving colors from the theme context or accepting a color prop.

Also applies to: 29-29

packages/ui/src/feedback/Alert.tsx (1)

30-43: Consider adding accessibility attributes.

The Alert component lacks accessibility props for screen readers. Adding accessibilityRole="alert" would announce the content appropriately to assistive technologies.

♿ Suggested accessibility improvement
     <View
-      className={`${config.bg} p-4 flex-row items-center rounded-[0.6rem_0.8rem_0.7rem_0.9rem]`}
+      className={`${config.bg} p-4 flex-row items-center rounded-[0.6rem_0.8rem_0.7rem_0.9rem]`}
+      accessibilityRole="alert"
+      accessibilityLabel={title ? `${type} alert: ${title}. ${message}` : `${type} alert: ${message}`}
     >
packages/types/src/api.ts (1)

143-150: Consider narrowing StreamEvent.type.

The type field is typed as string, but Anthropic's streaming events have known types (message_start, content_block_delta, message_stop, etc.). A union type would provide better type safety if you're handling these events.

🔧 Optional: Narrow StreamEvent.type
   export interface StreamEvent {
-    type: string;
+    type: 
+      | 'message_start'
+      | 'content_block_start'
+      | 'content_block_delta'
+      | 'content_block_stop'
+      | 'message_delta'
+      | 'message_stop'
+      | 'ping'
+      | 'error';
     index?: number;
     delta?: {
       type: string;
       text?: string;
     };
   }
packages/config/src/env.ts (1)

76-106: Guard against missing expoConfig in runtime environments.
If Constants.expoConfig is undefined in certain EAS/standalone contexts, appEnv silently falls back to development, which could enable dev tools unintentionally. Consider a safe fallback path (e.g., Constants.manifest) or a single expoConfig helper, and verify behavior per expo-constants 17 docs.

🔧 Possible adjustment
-function getAppEnv(): AppEnvironment {
-  const extra = Constants.expoConfig?.extra;
+function getAppEnv(): AppEnvironment {
+  const expoConfig = Constants.expoConfig ?? Constants.manifest;
+  const extra = expoConfig?.extra;
   const appEnv = extra?.appEnv;
-  const extra = Constants.expoConfig?.extra || {};
+  const expoConfig = Constants.expoConfig ?? Constants.manifest;
+  const extra = expoConfig?.extra || {};
   const appEnv = getAppEnv();
@@
-    version: Constants.expoConfig?.version || '0.0.0',
-    buildNumber: Constants.expoConfig?.ios?.buildNumber ||
-      Constants.expoConfig?.android?.versionCode?.toString() ||
+    version: expoConfig?.version || '0.0.0',
+    buildNumber: expoConfig?.ios?.buildNumber ||
+      expoConfig?.android?.versionCode?.toString() ||
       '1',
packages/types/src/credentials.ts (1)

10-21: Align provider taxonomy with supported types.
CredentialType includes gitlab/bitbucket, but CredentialProvider does not, which forces those to be treated as custom and can blur provider-based logic across packages (e.g., state/core). Consider making them first-class providers or documenting the category mapping explicitly.

♻️ Possible adjustment
-export type CredentialProvider = 'github' | 'anthropic' | 'openai' | 'custom';
+export type CredentialProvider =
+  | 'github'
+  | 'gitlab'
+  | 'bitbucket'
+  | 'anthropic'
+  | 'openai'
+  | 'custom';
packages/types/src/workspaces.ts (1)

87-115: Standardize git metadata shapes across packages.
CommitInfo/BranchInfo naming diverges from packages/core/src/git/types.ts (e.g., sha vs oid, isHead vs current, required ahead/behind). Consider aligning on a canonical shape or adding explicit adapter types to avoid repeated mapping.

packages/state/src/agentStore.ts (1)

13-54: Avoid diverging agent/task shapes from @thumbcode/types.
The store-level AgentStatus/AgentConfig/AgentTask differ from packages/types/src/agents.ts, which can lead to accidental mixing and mapping friction across layers. Consider reusing the shared types or explicitly defining a AgentStore* model and mapping at the boundary.

packages/types/src/events.ts (1)

136-139: Tighten EventEmitter.on/off typing to actually narrow by event type.

The current generic doesn’t reliably infer the event subtype from type, so handlers can accept mismatched payloads. Consider an Extract<> mapping to enforce correct handler typing.

♻️ Suggested typing refinement
 export interface EventEmitter {
   emit<T extends AppEvent>(event: T): void;
-  on<T extends AppEvent>(type: T['type'], handler: EventHandler<T>): EventSubscription;
-  off<T extends AppEvent>(type: T['type'], handler: EventHandler<T>): void;
+  on<TType extends AppEvent['type']>(
+    type: TType,
+    handler: EventHandler<Extract<AppEvent, { type: TType }>>
+  ): EventSubscription;
+  off<TType extends AppEvent['type']>(
+    type: TType,
+    handler: EventHandler<Extract<AppEvent, { type: TType }>>
+  ): void;
 }
packages/types/src/chat.ts (2)

10-19: Avoid ChatThread shape drift vs packages/state/src/chatStore.ts.

packages/types/src/chat.ts defines ChatThread with messages/context/status, while packages/state/src/chatStore.ts (lines 55-64 in the snippet) defines a different ChatThread with title, lastMessageAt, unreadCount, isPinned. The shared name risks confusing consumers. Consider aligning the shapes or renaming one (e.g., ChatThreadSummary) to make intent explicit.


155-161: Make MessageChunk a discriminated union with required fields.

Right now any combination of optional fields is allowed. Tightening the union improves safety and reduces runtime checks.

♻️ Suggested union shape
-export interface MessageChunk {
-  type: 'text' | 'tool_use' | 'tool_result';
-  delta?: string;
-  toolName?: string;
-  input?: Record<string, unknown>;
-  id?: string;
-}
+export type MessageChunk =
+  | { type: 'text'; delta: string }
+  | { type: 'tool_use'; id: string; toolName: string; input: Record<string, unknown> }
+  | { type: 'tool_result'; toolUseId: string; result: unknown; isError?: boolean };
packages/state/src/chatStore.ts (1)

256-266: Persisted unread counts can exceed trimmed message history.

partialize keeps only the last 100 messages per thread but persists unreadCount unchanged. After rehydrate, unread badges may reflect messages that aren’t stored. Consider clamping unreadCount to the persisted message count.

♻️ Possible adjustment in partialize
-        partialize: (state) => ({
-          threads: state.threads,
-          // Only persist last 100 messages per thread
-          messages: Object.fromEntries(
-            Object.entries(state.messages).map(([threadId, msgs]) => [threadId, msgs.slice(-100)])
-          ),
-        }),
+        partialize: (state) => {
+          const messages = Object.fromEntries(
+            Object.entries(state.messages).map(([threadId, msgs]) => [threadId, msgs.slice(-100)])
+          );
+          return {
+            threads: state.threads.map((thread) => {
+              const persistedCount = messages[thread.id]?.length ?? 0;
+              return { ...thread, unreadCount: Math.min(thread.unreadCount, persistedCount) };
+            }),
+            messages,
+          };
+        },
packages/ui/src/form/Input.tsx (1)

24-46: Default accessibilityLabel from label for screen readers.

If callers don’t pass accessibilityLabel, consider using label so SR users get the same context.

♿ Suggested accessibility fallback
-export function Input({ label, error, variant = 'default', className = '', ...props }: InputProps) {
+export function Input({
+  label,
+  error,
+  variant = 'default',
+  className = '',
+  accessibilityLabel,
+  ...props
+}: InputProps) {
@@
-      <RNTextInput
+      <RNTextInput
+        accessibilityLabel={accessibilityLabel ?? label}
         className={`
           ${variantClasses}
packages/core/src/credentials/CredentialService.ts (1)

125-167: Bind requireBiometric to SecureStore's OS-level authentication.

Right now biometrics are checked in-app, but SecureStore access isn't tied to OS auth. If requireBiometric is meant to enforce OS-level gating, pass SecureStore's requireAuthentication option in both setItemAsync and getItemAsync calls.

🔐 Suggested binding to SecureStore auth
-      await SecureStore.setItemAsync(key, JSON.stringify(payload), {
-        keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
-      });
+      const secureStoreOptions = {
+        keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
+        ...(requireBiometric ? { requireAuthentication: true } : {}),
+      };
+      await SecureStore.setItemAsync(key, JSON.stringify(payload), secureStoreOptions);
@@
-      const payload = await SecureStore.getItemAsync(key);
+      const payload = await SecureStore.getItemAsync(
+        key,
+        requireBiometric ? { requireAuthentication: true } : undefined
+      );

Note: This requires a standalone build; requireAuthentication is not fully supported in Expo Go when biometric authentication is available due to missing configuration.

packages/state/src/projectStore.ts (1)

113-125: Prefer a non-deprecated ID helper.
substr is deprecated, and Date/Math.random IDs can collide; consider a dedicated ID helper if you have one. At minimum, switch to slice.

♻️ Suggested tweak
-          const projectId = `project-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+          const projectId = `project-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
packages/state/src/credentialStore.ts (2)

14-18: Consider importing types from @thumbcode/types instead of redefining them.

CredentialProvider and CredentialStatus are already exported from packages/types/src/index.ts. Duplicating these definitions creates a maintenance burden and risks type drift.

♻️ Suggested refactor
-// Supported credential providers
-export type CredentialProvider = 'anthropic' | 'openai' | 'github' | 'custom';
-
-// Credential status
-export type CredentialStatus = 'valid' | 'invalid' | 'expired' | 'unknown';
+import type { CredentialProvider, CredentialStatus } from '@thumbcode/types';
+
+// Re-export for consumers
+export type { CredentialProvider, CredentialStatus };

90-103: Use substring instead of deprecated substr.

String.prototype.substr is deprecated. Use substring or slice instead.

♻️ Suggested fix
 addCredential: (credential) => {
-  const credentialId = `cred-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+  const credentialId = `cred-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
   set((state) => {
packages/types/src/user.ts (1)

15-25: Consider clarifying the relationship between User types.

This User interface represents the app-level user entity, while packages/types/src/api.ts exports a User interface representing GitHub API responses. Both being named User may cause confusion or import conflicts.

Consider renaming one (e.g., AppUser or GitHubApiUser) or adding documentation clarifying when each should be used.

packages/state/src/userStore.ts (1)

13-63: Consider importing types from @thumbcode/types instead of redefining them.

These types (ThemeMode, EditorPreferences, NotificationPreferences, AgentPreferences, GitHubProfile) are duplicated from packages/types/src/user.ts with mismatches:

  • NotificationPreferences here is missing errorAlerts and dailySummary
  • UserSettings is missing hapticFeedback that exists in UserPreferences

Import from the types package and extend if needed, or consolidate to a single source of truth.

♻️ Suggested approach
+import type {
+  ThemeMode,
+  EditorPreferences,
+  NotificationPreferences,
+  AgentPreferences,
+  GitHubProfile,
+} from '@thumbcode/types';
+
+// Re-export for consumers
+export type { ThemeMode, EditorPreferences, NotificationPreferences, AgentPreferences, GitHubProfile };
+
+// UserSettings can remain here if it differs from UserPreferences
+export interface UserSettings {
+  theme: ThemeMode;
+  editor: EditorPreferences;
+  notifications: NotificationPreferences;
+  agents: AgentPreferences;
+}
-// Theme preference
-export type ThemeMode = 'light' | 'dark' | 'system';
-
-// Editor preferences
-export interface EditorPreferences { ... }
-// ... etc

Comment on lines +29 to +133
interface FeatureFlagConfig {
enabled: boolean;
description: string;
environments: ('development' | 'staging' | 'production')[];
}

/**
* Feature flag definitions
*/
const FEATURE_FLAGS: Record<FeatureFlag, FeatureFlagConfig> = {
devTools: {
enabled: true,
description: 'Developer tools and debugging features',
environments: ['development'],
},
analytics: {
enabled: true,
description: 'Anonymous usage analytics',
environments: ['staging', 'production'],
},
crashReporting: {
enabled: true,
description: 'Automatic crash reporting',
environments: ['staging', 'production'],
},
multiAgent: {
enabled: true,
description: 'Multi-agent orchestration system',
environments: ['development', 'staging', 'production'],
},
offlineMode: {
enabled: false,
description: 'Offline mode with local caching',
environments: ['development', 'staging', 'production'],
},
i18n: {
enabled: false,
description: 'Internationalization support',
environments: ['development', 'staging', 'production'],
},
darkMode: {
enabled: true,
description: 'Dark mode theme support',
environments: ['development', 'staging', 'production'],
},
biometricAuth: {
enabled: true,
description: 'Biometric authentication for credentials',
environments: ['development', 'staging', 'production'],
},
mcpServers: {
enabled: true,
description: 'MCP server integration',
environments: ['development', 'staging', 'production'],
},
gitLabSupport: {
enabled: false,
description: 'GitLab repository support',
environments: ['development', 'staging', 'production'],
},
bitbucketSupport: {
enabled: false,
description: 'Bitbucket repository support',
environments: ['development', 'staging', 'production'],
},
};

/**
* Check if a feature is enabled
*
* @param feature - Feature flag to check
* @returns Whether the feature is enabled for current environment
*/
export function isFeatureEnabled(feature: FeatureFlag): boolean {
const config = FEATURE_FLAGS[feature];
if (!config) {
return false;
}

// Check if feature is enabled at all
if (!config.enabled) {
return false;
}

// Check if current environment is in allowed environments
return config.environments.includes(env.appEnv);
}

/**
* Get all enabled features for current environment
*
* @returns Array of enabled feature names
*/
export function getEnabledFeatures(): FeatureFlag[] {
return (Object.keys(FEATURE_FLAGS) as FeatureFlag[]).filter(isFeatureEnabled);
}

/**
* Get feature flag configuration
*
* @param feature - Feature flag name
* @returns Feature configuration or undefined
*/
export function getFeatureConfig(feature: FeatureFlag): FeatureFlagConfig | undefined {
return FEATURE_FLAGS[feature];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the actual features.ts file
cat -n packages/config/src/features.ts | head -150

Repository: agentic-dev-library/thumbcode

Length of output: 202


🏁 Script executed:

# Check if there's a tsconfig.json to understand the declaration emit setting
find . -name "tsconfig*.json" -type f | head -20

Repository: agentic-dev-library/thumbcode

Length of output: 202


🏁 Script executed:

# Look at the tsconfig to see if declaration emit is enabled
cat tsconfig.json 2>/dev/null || cat packages/config/tsconfig.json 2>/dev/null || echo "Checking root level"

Repository: agentic-dev-library/thumbcode

Length of output: 202


Exported API references a non-exported type.
getFeatureConfig returns FeatureFlagConfig, but the interface isn't exported. With declaration emit, this causes TS4025: has or is using private name. Export the interface (or inline the return type).

✅ Suggested fix
-interface FeatureFlagConfig {
+export interface FeatureFlagConfig {
   enabled: boolean;
   description: string;
   environments: ('development' | 'staging' | 'production')[];
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface FeatureFlagConfig {
enabled: boolean;
description: string;
environments: ('development' | 'staging' | 'production')[];
}
/**
* Feature flag definitions
*/
const FEATURE_FLAGS: Record<FeatureFlag, FeatureFlagConfig> = {
devTools: {
enabled: true,
description: 'Developer tools and debugging features',
environments: ['development'],
},
analytics: {
enabled: true,
description: 'Anonymous usage analytics',
environments: ['staging', 'production'],
},
crashReporting: {
enabled: true,
description: 'Automatic crash reporting',
environments: ['staging', 'production'],
},
multiAgent: {
enabled: true,
description: 'Multi-agent orchestration system',
environments: ['development', 'staging', 'production'],
},
offlineMode: {
enabled: false,
description: 'Offline mode with local caching',
environments: ['development', 'staging', 'production'],
},
i18n: {
enabled: false,
description: 'Internationalization support',
environments: ['development', 'staging', 'production'],
},
darkMode: {
enabled: true,
description: 'Dark mode theme support',
environments: ['development', 'staging', 'production'],
},
biometricAuth: {
enabled: true,
description: 'Biometric authentication for credentials',
environments: ['development', 'staging', 'production'],
},
mcpServers: {
enabled: true,
description: 'MCP server integration',
environments: ['development', 'staging', 'production'],
},
gitLabSupport: {
enabled: false,
description: 'GitLab repository support',
environments: ['development', 'staging', 'production'],
},
bitbucketSupport: {
enabled: false,
description: 'Bitbucket repository support',
environments: ['development', 'staging', 'production'],
},
};
/**
* Check if a feature is enabled
*
* @param feature - Feature flag to check
* @returns Whether the feature is enabled for current environment
*/
export function isFeatureEnabled(feature: FeatureFlag): boolean {
const config = FEATURE_FLAGS[feature];
if (!config) {
return false;
}
// Check if feature is enabled at all
if (!config.enabled) {
return false;
}
// Check if current environment is in allowed environments
return config.environments.includes(env.appEnv);
}
/**
* Get all enabled features for current environment
*
* @returns Array of enabled feature names
*/
export function getEnabledFeatures(): FeatureFlag[] {
return (Object.keys(FEATURE_FLAGS) as FeatureFlag[]).filter(isFeatureEnabled);
}
/**
* Get feature flag configuration
*
* @param feature - Feature flag name
* @returns Feature configuration or undefined
*/
export function getFeatureConfig(feature: FeatureFlag): FeatureFlagConfig | undefined {
return FEATURE_FLAGS[feature];
export interface FeatureFlagConfig {
enabled: boolean;
description: string;
environments: ('development' | 'staging' | 'production')[];
}
/**
* Feature flag definitions
*/
const FEATURE_FLAGS: Record<FeatureFlag, FeatureFlagConfig> = {
devTools: {
enabled: true,
description: 'Developer tools and debugging features',
environments: ['development'],
},
analytics: {
enabled: true,
description: 'Anonymous usage analytics',
environments: ['staging', 'production'],
},
crashReporting: {
enabled: true,
description: 'Automatic crash reporting',
environments: ['staging', 'production'],
},
multiAgent: {
enabled: true,
description: 'Multi-agent orchestration system',
environments: ['development', 'staging', 'production'],
},
offlineMode: {
enabled: false,
description: 'Offline mode with local caching',
environments: ['development', 'staging', 'production'],
},
i18n: {
enabled: false,
description: 'Internationalization support',
environments: ['development', 'staging', 'production'],
},
darkMode: {
enabled: true,
description: 'Dark mode theme support',
environments: ['development', 'staging', 'production'],
},
biometricAuth: {
enabled: true,
description: 'Biometric authentication for credentials',
environments: ['development', 'staging', 'production'],
},
mcpServers: {
enabled: true,
description: 'MCP server integration',
environments: ['development', 'staging', 'production'],
},
gitLabSupport: {
enabled: false,
description: 'GitLab repository support',
environments: ['development', 'staging', 'production'],
},
bitbucketSupport: {
enabled: false,
description: 'Bitbucket repository support',
environments: ['development', 'staging', 'production'],
},
};
/**
* Check if a feature is enabled
*
* `@param` feature - Feature flag to check
* `@returns` Whether the feature is enabled for current environment
*/
export function isFeatureEnabled(feature: FeatureFlag): boolean {
const config = FEATURE_FLAGS[feature];
if (!config) {
return false;
}
// Check if feature is enabled at all
if (!config.enabled) {
return false;
}
// Check if current environment is in allowed environments
return config.environments.includes(env.appEnv);
}
/**
* Get all enabled features for current environment
*
* `@returns` Array of enabled feature names
*/
export function getEnabledFeatures(): FeatureFlag[] {
return (Object.keys(FEATURE_FLAGS) as FeatureFlag[]).filter(isFeatureEnabled);
}
/**
* Get feature flag configuration
*
* `@param` feature - Feature flag name
* `@returns` Feature configuration or undefined
*/
export function getFeatureConfig(feature: FeatureFlag): FeatureFlagConfig | undefined {
return FEATURE_FLAGS[feature];
}
🤖 Prompt for AI Agents
In `@packages/config/src/features.ts` around lines 29 - 133, The exported function
getFeatureConfig references the non-exported interface FeatureFlagConfig which
triggers TS4025; fix by exporting the interface (change "interface
FeatureFlagConfig" to "export interface FeatureFlagConfig") so the type is
public, or alternatively change getFeatureConfig's signature to return an
inline/public type; update the declaration near FEATURE_FLAGS and ensure any
other uses (e.g., isFeatureEnabled, getEnabledFeatures) still import/consume the
exported type as needed.

},
"dependencies": {
"isomorphic-git": "^1.27.0",
"diff": "^7.0.0",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

@types/diff npm package versions compatibility diff 7

💡 Result:

Short answer: Use @types/diff@7.x for diff (jsdiff) v7 — jsdiff only bundled official TypeScript types starting with v8, so projects using diff v7 should install the matching @types/diff (e.g. @types/diff@7.0.1). [1][2]

Sources:

  • @types/diff v7.0.1 (npm). [1]
  • jsdiff (diff) notes: ships its own TypeScript definitions starting in v8 (so @types unnecessary for v8+). [2]

🌐 Web query:

diff npm package version 7.0.0 release notes API changes

💡 Result:

Summary — diff (jsdiff) v7.0.0

  • Release note: single (breaking) bugfix: diffWords treats numbers and underscores as word characters again (undoes a behavior change introduced in v6.0.0). [1][2]

Implication for API/behavior

  • No added APIs; the change is behavioral: diffWords (and related word-diff behavior) was restored to treat digits and underscores as part of words (affects tokenization and diff output). Code calling diffWords should see word-tokenization revert to the pre-v6 behavior; no new functions or signatures were introduced. [1][2]

Sources

  1. diff release-notes (7.0.0). [UNPKG / package release-notes]. [2]
  2. diff release-notes mirrored (Fossies). [1]

Update @types/diff version to match diff package.

diff is at ^7.0.0 but @types/diff is at ^6.0.0. Use @types/diff@^7.0.0 to match the primary dependency version.

Also applies to: 21-21

🤖 Prompt for AI Agents
In `@packages/core/package.json` at line 13, Update the `@types/diff` devDependency
to match the installed diff package version: change the `@types/diff` entry to
^7.0.0 so it aligns with "diff": "^7.0.0" in package.json; locate the
package.json dependency block that contains "diff" and update the "@types/diff"
version accordingly (and do the same for the other occurrence noted at lines
21-21).

Comment on lines +219 to +348
private async validateGitHubToken(token: string): Promise<ValidationResult> {
try {
const response = await fetch(VALIDATION_ENDPOINTS.github, {
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github.v3+json',
},
});

if (!response.ok) {
if (response.status === 401) {
return { isValid: false, message: 'Invalid GitHub token' };
}
return { isValid: false, message: `GitHub API error: ${response.status}` };
}

const user = await response.json();

// Check for expiration header if present
const expiresAt = response.headers.get('github-authentication-token-expiration');

return {
isValid: true,
message: `Authenticated as ${user.login}`,
expiresAt: expiresAt ? new Date(expiresAt) : undefined,
metadata: {
username: user.login,
avatarUrl: user.avatar_url,
name: user.name,
scopes: response.headers.get('x-oauth-scopes')?.split(', ') || [],
rateLimit: parseInt(response.headers.get('x-ratelimit-remaining') || '0', 10),
},
};
} catch (error) {
return {
isValid: false,
message: error instanceof Error ? error.message : 'GitHub validation failed',
};
}
}

/**
* Validate an Anthropic API key
*/
private async validateAnthropicKey(apiKey: string): Promise<ValidationResult> {
try {
// For Anthropic, we make a minimal request to check the key
const response = await fetch(VALIDATION_ENDPOINTS.anthropic, {
method: 'POST',
headers: {
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'claude-3-haiku-20240307',
max_tokens: 1,
messages: [{ role: 'user', content: 'Hi' }],
}),
});

// We expect the request to work, indicating valid key
if (response.ok || response.status === 200) {
return {
isValid: true,
message: 'Anthropic API key is valid',
metadata: {
rateLimit: parseInt(
response.headers.get('anthropic-ratelimit-requests-remaining') || '0',
10
),
},
};
}

// 401 means invalid key
if (response.status === 401) {
return { isValid: false, message: 'Invalid Anthropic API key' };
}

// 429 means rate limited but key is valid
if (response.status === 429) {
return {
isValid: true,
message: 'Anthropic API key valid but rate limited',
};
}

return { isValid: false, message: `Anthropic API error: ${response.status}` };
} catch (error) {
return {
isValid: false,
message: error instanceof Error ? error.message : 'Anthropic validation failed',
};
}
}

/**
* Validate an OpenAI API key
*/
private async validateOpenAIKey(apiKey: string): Promise<ValidationResult> {
try {
const response = await fetch(VALIDATION_ENDPOINTS.openai, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});

if (response.ok) {
return {
isValid: true,
message: 'OpenAI API key is valid',
metadata: {
rateLimit: parseInt(response.headers.get('x-ratelimit-remaining-requests') || '0', 10),
},
};
}

if (response.status === 401) {
return { isValid: false, message: 'Invalid OpenAI API key' };
}

return { isValid: false, message: `OpenAI API error: ${response.status}` };
} catch (error) {
return {
isValid: false,
message: error instanceof Error ? error.message : 'OpenAI validation failed',
};
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

GitHub REST API /user endpoint status codes limited scope personal access token

💡 Result:

Short answer: GET /user can return 200, 304, 401 or 403. For classic personal access tokens the token must include the user scope to return private profile fields; fine‑grained personal access tokens (and GitHub App user tokens) do not require additional permissions for this endpoint. [1][2]

Sources:

  • GitHub REST API — Get the authenticated user (status codes & scope notes). [1]
  • GitHub REST API — Personal access tokens / fine‑grained token docs. [2]

🌐 Web query:

Anthropic API messages endpoint 400 404 valid key model access

💡 Result:

If you get 400 or 404 from the Anthropic /v1/messages endpoint even though your API key and model access appear valid, check these common causes and fixes:

  1. Confirm endpoint and URL exactly: POST https://api.anthropic.com/v1/messages (no extra path or typo). A wrong endpoint returns 404. [1][2]

  2. Check required headers:

    • x-api-key: YOUR_KEY (or Authorization if your proxy requires Bearer).
    • anthropic-version header (example: 2023-06-01) and Content-Type: application/json. Missing/incorrect headers can produce 400/401/403. [2][3]
  3. Validate JSON request body shape: include "model" and "messages" (array of {role, content}) and other params per the examples. Malformed JSON or wrong param types -> 400 invalid_request_error. Use the examples from the Messages docs. [3]

  4. Verify model name exactly (correct spelling/version). Requesting a non‑existent model yields 404/not_found_error. Also ensure your org/key actually has permission for that model (403 permission_error). [2][3]

  5. Watch request size and content types: requests >32 MB return request_too_large (413) or other errors; image payloads must follow doc schema. Large/malformed image parts can cause 400. [2][4]

  6. Check billing/credits and rate limits: low balance or exceeded limits can manifest as errors in some integrations (400 from proxies or 429 from API). Confirm account billing and rate limits. [5][2]

  7. If you use a proxy or third‑party endpoint (LMRouter, TypingMind, etc.), confirm that proxy expects x-api-key vs Authorization and that it maps model names correctly; proxies can return 400/404 for mismatches. [6][7]

  8. Inspect the API error body and request-id header: Anthropic returns an error type (invalid_request_error / not_found_error / authentication_error) and request-id — include those when contacting support. [2]

Quick debugging steps: reproduce with curl using the exact example from the Messages docs, copy/paste your API key into the x-api-key header, and a minimal valid body (one user message). If that succeeds, incrementally add your real payload to find the failure point. [3]

References:

  • Anthropic API Errors (error codes & meanings). [2]
  • Anthropic Messages examples / request format. [3]
  • Anthropic Getting Started / request size limits. [4]
  • Examples / community notes on billing, proxies. [5][6]

If you want, paste the exact curl/request (omit the real key) plus the full error response you receive and I’ll pinpoint the problem.


🌐 Web query:

OpenAI API /v1/models endpoint 403 valid key insufficient permissions

💡 Result:

Short checklist to fix a 403 "valid key but insufficient permissions" when calling /v1/models:

  1. Verify you're sending the key as a Bearer token in the Authorization header (Authorization: Bearer SK...). [1]
  2. Confirm you're calling the correct OpenAI endpoint (platform API base) and not another vendor's endpoint. A token for a different provider (or GitHub PAT vs OpenAI key) will produce 403s. [2]
  3. Check the key's role/scope: some endpoints (or specific models) require admin/organization-level keys or explicit model access. If the model or endpoint is gated, your API key must be granted that permission. Regenerate or request the proper-scoped key from your org admin if needed. [3][2]
  4. If you're in an organization, confirm the org/project settings haven't restricted model access (org admins can enable/disable models or require special roles). [3]
  5. Capture and inspect the full 403 response body — many providers include a short reason (e.g., "model not enabled for org") which tells you exactly what to change. [2]
  6. If the above don't resolve it, contact OpenAI support or your org administrator and include: the exact request (endpoint, method), full response body, timestamp, and the first 8 chars of the key (do NOT send the full key). Support can check account-level permissions and gating.

Sources:

  • OpenAI docs (example: some endpoints require admin keys / org permissions). [1]
  • Community troubleshooting showing 403 when token/endpoint/model access mismatch and how to verify endpoint, token scopes, and org model enablement. [2]
  • Generic API error-handling docs describing 403 meaning "valid key but insufficient permissions." [3]

If you want, paste the exact curl/response body (with the key redacted) and I can point to the likely missing permission.


Validation can falsely reject valid tokens with limited scopes or insufficient access.

store() validates by default, but some endpoints return 403/404 for valid keys lacking required permissions or model access. This can block legitimate credentials. Consider treating permission-related errors as "valid but insufficient access" and only failing hard on 401 (authentication failure).

GitHub: 403 on /user indicates valid PAT but missing user scope — treat as valid.

Anthropic: 403 (not 400) indicates valid key but lacking model access; 404 indicates non-existent model. The current code already handles 429 (rate limit) correctly.

OpenAI: 403 indicates valid key but insufficient permissions (e.g., missing list-models scope).

🛠️ Suggested adjustments
@@ private async validateGitHubToken(token: string): Promise<ValidationResult> {
-      if (!response.ok) {
-        if (response.status === 401) {
-          return { isValid: false, message: 'Invalid GitHub token' };
-        }
-        return { isValid: false, message: `GitHub API error: ${response.status}` };
-      }
+      if (!response.ok) {
+        if (response.status === 401) {
+          return { isValid: false, message: 'Invalid GitHub token' };
+        }
+        if (response.status === 403) {
+          return {
+            isValid: true,
+            message: 'GitHub token valid but lacks /user scope',
+            metadata: { status: response.status },
+          };
+        }
+        return { isValid: false, message: `GitHub API error: ${response.status}` };
+      }
@@ private async validateAnthropicKey(apiKey: string): Promise<ValidationResult> {
       if (response.status === 401) {
         return { isValid: false, message: 'Invalid Anthropic API key' };
       }
+
+      // 403/404 may indicate model access or existence issues, not an invalid key
+      if (response.status === 403 || response.status === 404) {
+        return {
+          isValid: true,
+          message: 'Anthropic key valid but model/access restricted',
+          metadata: { status: response.status },
+        };
+      }
@@ private async validateOpenAIKey(apiKey: string): Promise<ValidationResult> {
       if (response.status === 401) {
         return { isValid: false, message: 'Invalid OpenAI API key' };
       }
+
+      if (response.status === 403 || response.status === 429) {
+        return {
+          isValid: true,
+          message: 'OpenAI key valid but insufficient permission or rate limited',
+          metadata: { status: response.status },
+        };
+      }
 
       return { isValid: false, message: `OpenAI API error: ${response.status}` };
🤖 Prompt for AI Agents
In `@packages/core/src/credentials/CredentialService.ts` around lines 219 - 348,
The validation functions (validateGitHubToken, validateAnthropicKey,
validateOpenAIKey) currently treat 403/404 responses as hard failures and can
reject valid credentials with insufficient permissions or missing model access;
change them so only 401 returns isValid: false while 403 (and for Anthropic 404)
return isValid: true with messages indicating "valid but insufficient
permissions/model access" (e.g., for validateGitHubToken return { isValid: true,
message: 'Valid GitHub token but missing user scope' } on 403 and preserve
metadata extraction; for validateAnthropicKey treat 403 as { isValid: true,
message: 'Anthropic API key valid but lacks model access' } and 404 as {
isValid: true, message: 'Anthropic API key valid but model not found' }; for
validateOpenAIKey treat 403 as { isValid: true, message: 'OpenAI API key valid
but insufficient permissions' }). Only keep 401 as invalid and preserve existing
429 handling and header-based metadata.

Comment on lines +177 to +180
clearTasks: () =>
set((state) => {
state.tasks = [];
}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clear stale currentTaskId when wiping tasks.
clearTasks drops the task list but leaves agents[*].currentTaskId pointing at now-missing tasks, which can leave the store in an inconsistent state.

🧹 Suggested fix
 clearTasks: () =>
   set((state) => {
     state.tasks = [];
+    for (const agent of state.agents) {
+      agent.currentTaskId = undefined;
+    }
   }),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
clearTasks: () =>
set((state) => {
state.tasks = [];
}),
clearTasks: () =>
set((state) => {
state.tasks = [];
for (const agent of state.agents) {
agent.currentTaskId = undefined;
}
}),
🤖 Prompt for AI Agents
In `@packages/state/src/agentStore.ts` around lines 177 - 180, clearTasks
currently empties state.tasks but leaves agents[*].currentTaskId pointing to
removed tasks; update clearTasks to also clear/reset currentTaskId for all
agents (e.g., iterate state.agents and set each agent.currentTaskId =
null/undefined or rebuild agents with currentTaskId cleared) so the store
remains consistent; modify the clearTasks setter (the function passed to set in
agentStore.ts) to clear state.tasks AND reset agents[*].currentTaskId.

Comment on lines +210 to +246
deleteMessage: (messageId, threadId) =>
set((state) => {
if (state.messages[threadId]) {
state.messages[threadId] = state.messages[threadId].filter((m) => m.id !== messageId);
}
}),

respondToApproval: (messageId, threadId, approved) =>
set((state) => {
const threadMessages = state.messages[threadId];
if (threadMessages) {
const message = threadMessages.find((m) => m.id === messageId) as
| ApprovalMessage
| undefined;
if (message?.contentType === 'approval_request' && message.metadata) {
message.metadata.approved = approved;
message.metadata.respondedAt = new Date().toISOString();
}
}
}),

setTyping: (threadId, sender, isTypingNow) =>
set((state) => {
if (!state.isTyping[threadId]) {
state.isTyping[threadId] = [];
}
if (isTypingNow && !state.isTyping[threadId].includes(sender)) {
state.isTyping[threadId].push(sender);
} else if (!isTypingNow) {
state.isTyping[threadId] = state.isTyping[threadId].filter((s) => s !== sender);
}
}),

clearThread: (threadId) =>
set((state) => {
state.messages[threadId] = [];
}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Keep thread metadata consistent when messages are deleted/cleared.

deleteMessage and clearThread remove messages but leave unreadCount and lastMessageAt unchanged. This can cause stale unread badges or sorting based on a removed message. Consider resetting or recalculating these fields when the message list changes.

🐛 Suggested fix for metadata consistency
-        deleteMessage: (messageId, threadId) =>
-          set((state) => {
-            if (state.messages[threadId]) {
-              state.messages[threadId] = state.messages[threadId].filter((m) => m.id !== messageId);
-            }
-          }),
+        deleteMessage: (messageId, threadId) =>
+          set((state) => {
+            const threadMessages = state.messages[threadId];
+            if (threadMessages) {
+              const removed = threadMessages.find((m) => m.id === messageId);
+              const remaining = threadMessages.filter((m) => m.id !== messageId);
+              state.messages[threadId] = remaining;
+
+              const thread = state.threads.find((t) => t.id === threadId);
+              if (thread && removed) {
+                if (remaining.length === 0) {
+                  thread.unreadCount = 0;
+                  thread.lastMessageAt = thread.createdAt;
+                } else if (thread.lastMessageAt === removed.timestamp) {
+                  thread.lastMessageAt = remaining[remaining.length - 1].timestamp;
+                }
+              }
+            }
+          }),
 
         clearThread: (threadId) =>
           set((state) => {
             state.messages[threadId] = [];
+            const thread = state.threads.find((t) => t.id === threadId);
+            if (thread) {
+              thread.unreadCount = 0;
+              thread.lastMessageAt = thread.createdAt;
+            }
           }),
🤖 Prompt for AI Agents
In `@packages/state/src/chatStore.ts` around lines 210 - 246, When removing
messages in deleteMessage or wiping them in clearThread, also recompute and
update the thread's metadata (state.unreadCount and state.lastMessageAt) so they
don't remain stale; after you mutate state.messages[threadId], set
state.unreadCount[threadId] = remainingMessages.filter(m => !m.read).length (or
the equivalent unread flag in your Message type) and set
state.lastMessageAt[threadId] = remainingMessages.length ? the newest message's
timestamp (e.g., message.createdAt or message.updatedAt) : null/undefined,
ensuring both deleteMessage and clearThread apply the same recalculation logic
so sorting and badges remain correct.

Comment on lines +127 to +135
removeProject: (projectId) =>
set((state) => {
state.projects = state.projects.filter((p) => p.id !== projectId);
if (state.activeProjectId === projectId) {
state.activeProjectId = null;
state.workspace = null;
state.fileTree = null;
}
}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clear branch/commit state when the active project is removed.
Otherwise stale branches/commits can linger after deleting the active project.

🐛 Suggested fix
             if (state.activeProjectId === projectId) {
               state.activeProjectId = null;
               state.workspace = null;
               state.fileTree = null;
+              state.branches = [];
+              state.recentCommits = [];
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
removeProject: (projectId) =>
set((state) => {
state.projects = state.projects.filter((p) => p.id !== projectId);
if (state.activeProjectId === projectId) {
state.activeProjectId = null;
state.workspace = null;
state.fileTree = null;
}
}),
removeProject: (projectId) =>
set((state) => {
state.projects = state.projects.filter((p) => p.id !== projectId);
if (state.activeProjectId === projectId) {
state.activeProjectId = null;
state.workspace = null;
state.fileTree = null;
state.branches = [];
state.recentCommits = [];
}
}),
🤖 Prompt for AI Agents
In `@packages/state/src/projectStore.ts` around lines 127 - 135, The removeProject
updater only clears activeProjectId, workspace, and fileTree; extend it to also
clear any branch/commit-related state when the removed project was active so
stale data doesn't persist. In the removeProject function, after setting
activeProjectId = null, also set/clear any branch/commit fields present in this
store (e.g., activeBranchId, activeCommitId, branches, commits, currentBranch,
currentCommit or similarly named properties) to null/empty arrays as appropriate
so all project-scoped state is reset when the active project is removed.

Comment thread packages/types/src/api.ts
Comment on lines +118 to +128
export namespace Anthropic {
export interface Message {
id: string;
type: 'message';
role: 'assistant';
content: ContentBlock[];
model: string;
stop_reason: 'end_turn' | 'max_tokens' | 'stop_sequence' | 'tool_use';
stop_sequence: string | null;
usage: Usage;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Anthropic API Message object role field values documentation

💡 Result:

Anthropic Messages API — role field values

  • "user" — user/human message. (docs.anthropic.com)
  • "assistant" — model/assistant message (model-generated). (docs.claude.com)
  • "tool_inputs" — assistant message that requests running one or more tools (contains tool input descriptors). (github.com)
  • "tool_outputs" — a message containing results returned from running the requested tool(s) (or an error). (github.com)

Notes: older docs sometimes use "human" interchangeably with "user" when describing text-completion prompts. (docs.anthropic.com)

Citations:


🏁 Script executed:

# Check the context of the Message interface and how it's used
cd packages/types/src && cat -n api.ts | head -150

Repository: agentic-dev-library/thumbcode

Length of output: 202


🏁 Script executed:

# Search for Message interface usage in the codebase
rg "Message" packages/types/src --type ts -B 2 -A 2 | head -100

Repository: agentic-dev-library/thumbcode

Length of output: 202


🏁 Script executed:

# Check if there are other message-related types that might have different role values
rg "role.*assistant\|role.*user\|role.*system" packages/types --type ts

Repository: agentic-dev-library/thumbcode

Length of output: 202


🏁 Script executed:

find packages/types -type f -name "*.ts" -o -name "*.js" | head -20

Repository: agentic-dev-library/thumbcode

Length of output: 202


🏁 Script executed:

git ls-files packages/types/src/ | grep -E "\.(ts|js)$"

Repository: agentic-dev-library/thumbcode

Length of output: 202


🏁 Script executed:

head -200 packages/types/src/api.ts | tail -100

Repository: agentic-dev-library/thumbcode

Length of output: 202


Update Message.role to support all Anthropic API role values.

The role field is currently typed as the literal 'assistant', but according to Anthropic's official API documentation, Message objects can have role values of 'user' | 'assistant' | 'tool_inputs' | 'tool_outputs'. Update the type to reflect the complete set of valid roles.

🤖 Prompt for AI Agents
In `@packages/types/src/api.ts` around lines 118 - 128, The Message.role property
in the Anthropic namespace is too narrow; update the Anthropic.Message
interface's role type (the role field on Anthropic.Message) from the literal
'assistant' to the full union 'user' | 'assistant' | 'tool_inputs' |
'tool_outputs' so it matches Anthropic's API; locate the Anthropic.Message
interface and replace the role type accordingly, ensuring any places that
construct or check Message.role accept the expanded union.

Comment on lines +30 to +36
export interface UserPreferences {
theme: ThemeMode;
hapticFeedback: boolean;
notifications: NotificationPreferences;
editor: EditorPreferences;
agents: AgentPreferences;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Type mismatch: UserPreferences.hapticFeedback is missing from the state store.

This interface includes hapticFeedback: boolean, but UserSettings in packages/state/src/userStore.ts does not include this field. This creates an inconsistency between the canonical types and the runtime state.

🤖 Prompt for AI Agents
In `@packages/types/src/user.ts` around lines 30 - 36, UserPreferences declares
hapticFeedback but the runtime state type UserSettings (in userStore.ts) lacks
it; update the state to match the canonical type by adding
hapticFeedback:boolean to the UserSettings shape and its initialization/defaults
in the user store (ensure any persistence/serialization and reducers/selectors
that construct or read UserSettings are updated to provide a default value and
to persist/restore this field), or alternatively remove hapticFeedback from
UserPreferences if it should not be persisted—refer to the UserPreferences and
UserSettings symbols to locate and make the change.

Comment on lines +41 to +49
export interface NotificationPreferences {
pushEnabled: boolean;
soundEnabled: boolean;
agentUpdates: boolean;
prApprovals: boolean;
chatMessages: boolean;
errorAlerts: boolean;
dailySummary: boolean;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Type mismatch: NotificationPreferences differs from the state store.

This interface has 7 fields including errorAlerts and dailySummary, but packages/state/src/userStore.ts defines NotificationPreferences with only 5 fields (missing these two). This will cause type errors when the types package is consumed alongside the state package.

Either align the definitions or have the state store import from this types package.

🤖 Prompt for AI Agents
In `@packages/types/src/user.ts` around lines 41 - 49, The NotificationPreferences
type in the types package (NotificationPreferences) is out of sync with the
state store's NotificationPreferences; update the state store so both
definitions match or make the state store import and use this canonical type
instead: add the missing fields errorAlerts and dailySummary to the state
store's NotificationPreferences type (or replace its local definition with an
import of NotificationPreferences from the types package) and update any usages
in userStore to satisfy the expanded shape.

className={`
${variantClasses}
p-4
rounded-[1rem_0.75rem_1.25rem_0.5rem]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Invalid border-radius syntax for NativeWind/Tailwind.

The rounded-[1rem_0.75rem_1.25rem_0.5rem] syntax attempts to use CSS shorthand within Tailwind's arbitrary value bracket, but NativeWind doesn't support multi-value border-radius this way. This will likely be ignored or cause unexpected styling.

To achieve asymmetric corners, use individual corner utilities:

Proposed fix for asymmetric border-radius
-        rounded-[1rem_0.75rem_1.25rem_0.5rem]
+        rounded-tl-[1rem] rounded-tr-[0.75rem] rounded-br-[1.25rem] rounded-bl-[0.5rem]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
rounded-[1rem_0.75rem_1.25rem_0.5rem]
rounded-tl-[1rem] rounded-tr-[0.75rem] rounded-br-[1.25rem] rounded-bl-[0.5rem]
🤖 Prompt for AI Agents
In `@packages/ui/src/layout/Card.tsx` at line 27, The className in Card.tsx is
using an unsupported multi-value arbitrary border-radius
`rounded-[1rem_0.75rem_1.25rem_0.5rem]`; replace it with individual corner
utilities so NativeWind/Tailwind can apply asymmetric radii — e.g. use
`rounded-tl-[1rem] rounded-tr-[0.75rem] rounded-br-[1.25rem]
rounded-bl-[0.5rem]` in the same className (locate the string containing
`rounded-[1rem_0.75rem_1.25rem_0.5rem]` and swap it for these four utilities).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant