Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,282 changes: 1,282 additions & 0 deletions apps/app/server-bundle/package-lock.json

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions apps/app/server-bundle/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "@automaker/server-bundle",
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.61",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"morgan": "^1.10.1",
"node-pty": "1.1.0-beta41",
"ws": "^8.18.0"
}
}
12 changes: 6 additions & 6 deletions apps/server/tests/unit/lib/model-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ describe('model-resolver.ts', () => {
describe('resolveModelString', () => {
it("should resolve 'haiku' alias to full model string", () => {
const result = resolveModelString('haiku');
expect(result).toBe('claude-haiku-4-5');
expect(result).toBe(CLAUDE_MODEL_MAP.haiku);
});

it("should resolve 'sonnet' alias to full model string", () => {
const result = resolveModelString('sonnet');
expect(result).toBe('claude-sonnet-4-20250514');
expect(result).toBe(CLAUDE_MODEL_MAP.sonnet);
});

it("should resolve 'opus' alias to full model string", () => {
Expand All @@ -50,7 +50,7 @@ describe('model-resolver.ts', () => {
});

it('should pass through full Claude model strings', () => {
const models = ['claude-opus-4-5-20251101', 'claude-sonnet-4-20250514', 'claude-haiku-4-5'];
const models = [CLAUDE_MODEL_MAP.opus, CLAUDE_MODEL_MAP.sonnet, CLAUDE_MODEL_MAP.haiku];
models.forEach((model) => {
const result = resolveModelString(model);
expect(result).toBe(model);
Expand Down Expand Up @@ -93,11 +93,11 @@ describe('model-resolver.ts', () => {

it('should use session model when explicit is not provided', () => {
const result = getEffectiveModel(undefined, 'sonnet', 'gpt-5.2');
expect(result).toBe('claude-sonnet-4-20250514');
expect(result).toBe(CLAUDE_MODEL_MAP.sonnet);
});

it('should use default when neither explicit nor session is provided', () => {
const customDefault = 'claude-haiku-4-5';
const customDefault = CLAUDE_MODEL_MAP.haiku;
const result = getEffectiveModel(undefined, undefined, customDefault);
expect(result).toBe(customDefault);
});
Expand All @@ -109,7 +109,7 @@ describe('model-resolver.ts', () => {

it('should handle explicit empty strings as undefined', () => {
const result = getEffectiveModel('', 'haiku');
expect(result).toBe('claude-haiku-4-5');
expect(result).toBe(CLAUDE_MODEL_MAP.haiku);
});
});

Expand Down
4 changes: 2 additions & 2 deletions apps/server/tests/unit/providers/claude-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,10 +307,10 @@ describe('claude-provider.ts', () => {
expect(sonnet35).toBeDefined();
});

it('should include Claude 3.5 Haiku', () => {
it('should include Claude Haiku 4.5', () => {
const models = provider.getAvailableModels();

const haiku = models.find((m) => m.id === 'claude-3-5-haiku-20241022');
const haiku = models.find((m) => m.id === 'claude-haiku-4-5-20251001');
expect(haiku).toBeDefined();
});
Comment on lines +310 to 315
Copy link
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

🏁 Script executed:

#!/bin/bash
# Search for CLAUDE_MODEL_MAP definition and usage
echo "=== Searching for CLAUDE_MODEL_MAP definition ==="
rg "CLAUDE_MODEL_MAP" --type ts -B 2 -A 5

echo -e "\n=== Checking for model-resolver.test.ts ==="
find . -name "model-resolver.test.ts" -type f

echo -e "\n=== Checking for claude-provider.test.ts ==="
find . -name "claude-provider.test.ts" -type f

Repository: AutoMaker-Org/automaker

Length of output: 24947


🏁 Script executed:

#!/bin/bash
echo "=== Checking claude-provider.test.ts around lines 310-315 ==="
sed -n '305,320p' apps/server/tests/unit/providers/claude-provider.test.ts

echo -e "\n=== Searching for hardcoded Claude model strings in claude-provider.test.ts ==="
rg "claude-(opus|sonnet|haiku)" apps/server/tests/unit/providers/claude-provider.test.ts -n

echo -e "\n=== Checking for CLAUDE_MODEL_MAP import in claude-provider.test.ts ==="
head -20 apps/server/tests/unit/providers/claude-provider.test.ts

Repository: AutoMaker-Org/automaker

Length of output: 1881


Use CLAUDE_MODEL_MAP for consistency across Claude provider tests.

This test hardcodes 'claude-haiku-4-5-20251001' while the related file model-resolver.test.ts in this same PR adopts CLAUDE_MODEL_MAP constants for all Claude model references, creating maintenance inconsistency. Additionally, line 298 uses an outdated Sonnet model version ('claude-sonnet-4-20250514') instead of the centralized constant.

Refactor lines 59, 71, 289, 298, 313, and 320 to import and use CLAUDE_MODEL_MAP from @automaker/types:

+import { CLAUDE_MODEL_MAP } from '@automaker/types';

Then replace hardcoded strings with:

  • CLAUDE_MODEL_MAP.opus (for 'claude-opus-4-5-20251101')
  • CLAUDE_MODEL_MAP.sonnet (for the outdated 'claude-sonnet-4-20250514' at line 298)
  • CLAUDE_MODEL_MAP.haiku (for 'claude-haiku-4-5-20251001')

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/server/tests/unit/providers/claude-provider.test.ts around lines 59, 71,
289, 298, 313 and 320, replace hardcoded Claude model ID strings with the
centralized CLAUDE_MODEL_MAP constants to keep tests consistent: add an import
of CLAUDE_MODEL_MAP from '@automaker/types' at the top of the file, then replace
occurrences as follows — use CLAUDE_MODEL_MAP.opus where the test currently uses
'claude-opus-4-5-20251101', use CLAUDE_MODEL_MAP.sonnet where it uses
'claude-sonnet-4-20250514' (line 298), and use CLAUDE_MODEL_MAP.haiku where it
uses 'claude-haiku-4-5-20251001' (lines 313/320); ensure all referenced test
assertions and finds use these constants instead of string literals.


Expand Down
5 changes: 5 additions & 0 deletions apps/server/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ export default defineConfig({
'src/**/*.d.ts',
'src/index.ts',
'src/routes/**', // Routes are better tested with integration tests
'src/types/**', // Type re-exports don't need coverage
'src/middleware/**', // Middleware needs integration tests
'src/lib/enhancement-prompts.ts', // Prompt templates don't need unit tests
'src/services/claude-usage-service.ts', // TODO: Add tests for usage tracking
'**/libs/**', // Exclude aliased shared packages from server coverage
],
thresholds: {
// Increased thresholds to ensure better code quality
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/src/components/layout/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export function Sidebar() {
const isCreatingSpec = specCreatingForProject !== null;
const creatingSpecProjectPath = specCreatingForProject;

// Auto-collapse sidebar on small screens
// Auto-collapse sidebar on small screens and update Electron window minWidth
useSidebarAutoCollapse({ sidebarOpen, toggleSidebar });

// Running agents count
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,17 @@ export function useSidebarAutoCollapse({
mediaQuery.addEventListener('change', handleResize);
return () => mediaQuery.removeEventListener('change', handleResize);
}, [sidebarOpen, toggleSidebar]);

// Update Electron window minWidth when sidebar state changes
// This ensures the window can't be resized smaller than what the kanban board needs
useEffect(() => {
const electronAPI = (
window as unknown as {
electronAPI?: { updateMinWidth?: (expanded: boolean) => Promise<void> };
}
).electronAPI;
if (electronAPI?.updateMinWidth) {
electronAPI.updateMinWidth(sidebarOpen);
}
}, [sidebarOpen]);
Comment on lines +36 to +47
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add error handling for the async Electron API call.

The updateMinWidth call returns a promise that is not awaited and lacks error handling. If the IPC call fails or rejects, it will result in an unhandled promise rejection.

Consider adding a .catch() handler to log errors gracefully:

🔎 Proposed fix with error handling
   // Update Electron window minWidth when sidebar state changes
   // This ensures the window can't be resized smaller than what the kanban board needs
   useEffect(() => {
     const electronAPI = (
       window as unknown as {
         electronAPI?: { updateMinWidth?: (expanded: boolean) => Promise<void> };
       }
     ).electronAPI;
     if (electronAPI?.updateMinWidth) {
-      electronAPI.updateMinWidth(sidebarOpen);
+      electronAPI.updateMinWidth(sidebarOpen).catch((error) => {
+        console.error('[useSidebarAutoCollapse] Failed to update window minWidth:', error);
+      });
     }
   }, [sidebarOpen]);
🤖 Prompt for AI Agents
In apps/ui/src/components/layout/sidebar/hooks/use-sidebar-auto-collapse.ts
around lines 36 to 47, the call to the async Electron IPC method updateMinWidth
is not awaited and has no error handling, which can produce unhandled promise
rejections; wrap the call so rejections are handled — either invoke it inside an
async IIFE with try/catch or append .catch() to the returned promise and log the
error (use the app logger if available, otherwise console.error) to fail
gracefully.

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export const KanbanColumn = memo(function KanbanColumn({
<div
ref={setNodeRef}
className={cn(
'relative flex flex-col h-full rounded-xl transition-all duration-200',
'relative flex flex-col h-full rounded-xl',
// Only transition ring/shadow for drag-over effect, not width
'transition-[box-shadow,ring] duration-200',
!width && 'w-72', // Only apply w-72 if no custom width
showBorder && 'border border-border/60',
isOver && 'ring-2 ring-primary/30 ring-offset-1 ring-offset-background'
Expand Down
7 changes: 4 additions & 3 deletions apps/ui/src/components/views/board-view/kanban-board.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,18 @@ export function KanbanBoard({
onArchiveAllVerified,
}: KanbanBoardProps) {
// Use responsive column widths based on window size
const { columnWidth } = useResponsiveKanban(COLUMNS.length);
// containerStyle handles centering and ensures columns fit without horizontal scroll in Electron
const { columnWidth, containerStyle } = useResponsiveKanban(COLUMNS.length);

return (
<div className="flex-1 overflow-x-auto px-4 pb-4 relative" style={backgroundImageStyle}>
<div className="flex-1 overflow-x-hidden px-5 pb-4 relative" style={backgroundImageStyle}>
<DndContext
sensors={sensors}
collisionDetection={collisionDetectionStrategy}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
>
<div className="flex gap-5 h-full py-1 justify-center">
<div className="h-full py-1" style={containerStyle}>
{COLUMNS.map((column) => {
const columnFeatures = getColumnFeatures(column.id);
return (
Expand Down
27 changes: 27 additions & 0 deletions apps/ui/src/components/views/context-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,33 @@ export function ContextView() {
id="markdown-content"
value={newMarkdownContent}
onChange={(e) => setNewMarkdownContent(e.target.value)}
onDrop={async (e) => {
e.preventDefault();
e.stopPropagation();

// Try files first, then items for better compatibility
let files = Array.from(e.dataTransfer.files);
if (files.length === 0 && e.dataTransfer.items) {
const items = Array.from(e.dataTransfer.items);
files = items
.filter((item) => item.kind === 'file')
.map((item) => item.getAsFile())
.filter((f): f is globalThis.File => f !== null);
}

const mdFile = files.find((f) => isMarkdownFile(f.name));
if (mdFile) {
const content = await mdFile.text();
setNewMarkdownContent(content);
if (!newMarkdownName.trim()) {
setNewMarkdownName(mdFile.name);
}
}
}}
Comment on lines +1003 to +1025
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add error handling for file reading.

The async mdFile.text() call on line 1019 can throw an error if the file is unreadable or corrupted. Currently, any error would silently fail and leave the user without feedback.

🔎 Proposed fix with error handling
                 const mdFile = files.find((f) => isMarkdownFile(f.name));
                 if (mdFile) {
-                  const content = await mdFile.text();
-                  setNewMarkdownContent(content);
-                  if (!newMarkdownName.trim()) {
-                    setNewMarkdownName(mdFile.name);
+                  try {
+                    const content = await mdFile.text();
+                    setNewMarkdownContent(content);
+                    if (!newMarkdownName.trim()) {
+                      setNewMarkdownName(mdFile.name);
+                    }
+                  } catch (error) {
+                    console.error('Failed to read dropped file:', error);
+                    toast.error('Failed to read file', {
+                      description: 'The dropped file could not be read.',
+                    });
                   }
                 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/ui/src/components/views/context-view.tsx around lines 1003 to 1025, the
async mdFile.text() call can throw if the file is unreadable; wrap the file read
in a try/catch, log the error (console.error) and surface user feedback (e.g.,
set an error state or call an existing toast/notification helper) so the UI
informs the user the file could not be read; only setNewMarkdownContent and
setNewMarkdownName when reading succeeds, and keep existing behavior for naming
fallback.

onDragOver={(e) => {
e.preventDefault();
e.stopPropagation();
}}
placeholder="Enter your markdown content here..."
className="w-full h-60 p-3 font-mono text-sm bg-background border border-border rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent"
spellCheck={false}
Expand Down
Loading