refactor: reorganize codebase into proper pnpm workspace packages#45
Conversation
- 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>
|
Caution Review failedThe pull request is closed. 📝 WalkthroughWalkthroughThis PR adds five workspace packages ( Changes
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
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>
|
There was a problem hiding this comment.
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
classNameis 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
#FF7059is hardcoded. If the theme defines acoral-500token (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 movingreact-nativeto peerDependencies.
react-nativeis pinned exactly at0.76.0as a direct dependency, butreactis 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 theshadeparameter.The
shadeparameter is typed asstring, 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 andtext-whiteclass, while the PR introduces aThemeProviderwith 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
colorprop.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 narrowingStreamEvent.type.The
typefield is typed asstring, 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 missingexpoConfigin runtime environments.
IfConstants.expoConfigis undefined in certain EAS/standalone contexts, appEnv silently falls back todevelopment, which could enable dev tools unintentionally. Consider a safe fallback path (e.g.,Constants.manifest) or a singleexpoConfighelper, 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.
CredentialTypeincludesgitlab/bitbucket, butCredentialProviderdoes not, which forces those to be treated ascustomand 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/BranchInfonaming diverges frompackages/core/src/git/types.ts(e.g.,shavsoid,isHeadvscurrent, requiredahead/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-levelAgentStatus/AgentConfig/AgentTaskdiffer frompackages/types/src/agents.ts, which can lead to accidental mixing and mapping friction across layers. Consider reusing the shared types or explicitly defining aAgentStore*model and mapping at the boundary.packages/types/src/events.ts (1)
136-139: TightenEventEmitter.on/offtyping 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 anExtract<>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: AvoidChatThreadshape drift vspackages/state/src/chatStore.ts.
packages/types/src/chat.tsdefinesChatThreadwith messages/context/status, whilepackages/state/src/chatStore.ts(lines 55-64 in the snippet) defines a differentChatThreadwithtitle,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: MakeMessageChunka 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.
partializekeeps only the last 100 messages per thread but persistsunreadCountunchanged. After rehydrate, unread badges may reflect messages that aren’t stored. Consider clampingunreadCountto 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: DefaultaccessibilityLabelfromlabelfor screen readers.If callers don’t pass
accessibilityLabel, consider usinglabelso 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: BindrequireBiometricto SecureStore's OS-level authentication.Right now biometrics are checked in-app, but SecureStore access isn't tied to OS auth. If
requireBiometricis meant to enforce OS-level gating, pass SecureStore'srequireAuthenticationoption in bothsetItemAsyncandgetItemAsynccalls.🔐 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;
requireAuthenticationis 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.
substris deprecated, and Date/Math.random IDs can collide; consider a dedicated ID helper if you have one. At minimum, switch toslice.♻️ 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/typesinstead of redefining them.
CredentialProviderandCredentialStatusare already exported frompackages/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: Usesubstringinstead of deprecatedsubstr.
String.prototype.substris deprecated. Usesubstringorsliceinstead.♻️ 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 betweenUsertypes.This
Userinterface represents the app-level user entity, whilepackages/types/src/api.tsexports aUserinterface representing GitHub API responses. Both being namedUsermay cause confusion or import conflicts.Consider renaming one (e.g.,
AppUserorGitHubApiUser) or adding documentation clarifying when each should be used.packages/state/src/userStore.ts (1)
13-63: Consider importing types from@thumbcode/typesinstead of redefining them.These types (
ThemeMode,EditorPreferences,NotificationPreferences,AgentPreferences,GitHubProfile) are duplicated frompackages/types/src/user.tswith mismatches:
NotificationPreferenceshere is missingerrorAlertsanddailySummaryUserSettingsis missinghapticFeedbackthat exists inUserPreferencesImport 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
| 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]; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the actual features.ts file
cat -n packages/config/src/features.ts | head -150Repository: 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 -20Repository: 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.
| 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", |
There was a problem hiding this comment.
🧩 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/diffv7.0.1 (npm). [1]- jsdiff (diff) notes: ships its own TypeScript definitions starting in v8 (so
@typesunnecessary 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
- diff release-notes (7.0.0). [UNPKG / package release-notes]. [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).
| 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', | ||
| }; | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 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:
-
Confirm endpoint and URL exactly: POST https://api.anthropic.com/v1/messages (no extra path or typo). A wrong endpoint returns 404. [1][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]
-
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]
-
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]
-
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]
-
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]
-
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]
-
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:
- Verify you're sending the key as a Bearer token in the Authorization header (Authorization: Bearer SK...). [1]
- 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]
- 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]
- 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]
- 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]
- 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.
| clearTasks: () => | ||
| set((state) => { | ||
| state.tasks = []; | ||
| }), |
There was a problem hiding this comment.
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.
| 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.
| 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] = []; | ||
| }), |
There was a problem hiding this comment.
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.
| 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; | ||
| } | ||
| }), |
There was a problem hiding this comment.
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.
| 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.
| 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; | ||
| } |
There was a problem hiding this comment.
🧩 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:
- 1: https://docs.anthropic.com/en/api/streaming?utm_source=openai
- 2: https://docs.claude.com/en/docs/build-with-claude/working-with-messages?utm_source=openai
- 3: https://github.com/anthropics/anthropic-tools?utm_source=openai
- 4: https://github.com/anthropics/anthropic-tools?utm_source=openai
- 5: https://docs.anthropic.com/en/api/streaming?utm_source=openai
🏁 Script executed:
# Check the context of the Message interface and how it's used
cd packages/types/src && cat -n api.ts | head -150Repository: 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 -100Repository: 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 tsRepository: agentic-dev-library/thumbcode
Length of output: 202
🏁 Script executed:
find packages/types -type f -name "*.ts" -o -name "*.js" | head -20Repository: 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 -100Repository: 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.
| export interface UserPreferences { | ||
| theme: ThemeMode; | ||
| hapticFeedback: boolean; | ||
| notifications: NotificationPreferences; | ||
| editor: EditorPreferences; | ||
| agents: AgentPreferences; | ||
| } |
There was a problem hiding this comment.
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.
| export interface NotificationPreferences { | ||
| pushEnabled: boolean; | ||
| soundEnabled: boolean; | ||
| agentUpdates: boolean; | ||
| prApprovals: boolean; | ||
| chatMessages: boolean; | ||
| errorAlerts: boolean; | ||
| dailySummary: boolean; | ||
| } |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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.
| 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).


Summary
Key Changes
styled()API, uses direct className approachBreaking Changes
None - packages are additive and existing src/ code continues to work.
Test plan
pnpm lint- passes with no warningspnpm test- all 114 tests passingpnpm install🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
UI
State
Chores
✏️ Tip: You can customize this high-level summary in your review settings.