Merge main into massive-terminal-upgrade

Resolves merge conflicts:
- apps/server/src/routes/terminal/common.ts: Keep randomBytes import, use @automaker/utils for createLogger
- apps/ui/eslint.config.mjs: Use main's explicit globals list with XMLHttpRequest and MediaQueryListEvent additions
- apps/ui/src/components/views/terminal-view.tsx: Keep our terminal improvements (killAllSessions, beforeunload, better error handling)
- apps/ui/src/config/terminal-themes.ts: Keep our search highlight colors for all themes
- apps/ui/src/store/app-store.ts: Keep our terminal settings persistence improvements (merge function)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
SuperComboGamer
2025-12-21 20:27:44 -05:00
393 changed files with 32473 additions and 17974 deletions

View File

@@ -1,32 +1,23 @@
import { Page, Locator } from "@playwright/test";
import { Page, Locator } from '@playwright/test';
/**
* Get a kanban card by feature ID
*/
export async function getKanbanCard(
page: Page,
featureId: string
): Promise<Locator> {
export async function getKanbanCard(page: Page, featureId: string): Promise<Locator> {
return page.locator(`[data-testid="kanban-card-${featureId}"]`);
}
/**
* Get a kanban column by its ID
*/
export async function getKanbanColumn(
page: Page,
columnId: string
): Promise<Locator> {
export async function getKanbanColumn(page: Page, columnId: string): Promise<Locator> {
return page.locator(`[data-testid="kanban-column-${columnId}"]`);
}
/**
* Get the width of a kanban column
*/
export async function getKanbanColumnWidth(
page: Page,
columnId: string
): Promise<number> {
export async function getKanbanColumnWidth(page: Page, columnId: string): Promise<number> {
const column = page.locator(`[data-testid="kanban-column-${columnId}"]`);
const box = await column.boundingBox();
return box?.width ?? 0;
@@ -35,19 +26,16 @@ export async function getKanbanColumnWidth(
/**
* Check if a kanban column has CSS columns (masonry) layout
*/
export async function hasKanbanColumnMasonryLayout(
page: Page,
columnId: string
): Promise<boolean> {
export async function hasKanbanColumnMasonryLayout(page: Page, columnId: string): Promise<boolean> {
const column = page.locator(`[data-testid="kanban-column-${columnId}"]`);
const contentDiv = column.locator("> div").nth(1); // Second child is the content area
const contentDiv = column.locator('> div').nth(1); // Second child is the content area
const columnCount = await contentDiv.evaluate((el) => {
const style = window.getComputedStyle(el);
return style.columnCount;
});
return columnCount === "2";
return columnCount === '2';
}
/**
@@ -58,11 +46,8 @@ export async function dragKanbanCard(
featureId: string,
targetColumnId: string
): Promise<void> {
const card = page.locator(`[data-testid="kanban-card-${featureId}"]`);
const dragHandle = page.locator(`[data-testid="drag-handle-${featureId}"]`);
const targetColumn = page.locator(
`[data-testid="kanban-column-${targetColumnId}"]`
);
const targetColumn = page.locator(`[data-testid="kanban-column-${targetColumnId}"]`);
// Perform drag and drop
await dragHandle.dragTo(targetColumn);
@@ -71,15 +56,10 @@ export async function dragKanbanCard(
/**
* Click the view output button on a kanban card
*/
export async function clickViewOutput(
page: Page,
featureId: string
): Promise<void> {
export async function clickViewOutput(page: Page, featureId: string): Promise<void> {
// Try the running version first, then the in-progress version
const runningBtn = page.locator(`[data-testid="view-output-${featureId}"]`);
const inProgressBtn = page.locator(
`[data-testid="view-output-inprogress-${featureId}"]`
);
const inProgressBtn = page.locator(`[data-testid="view-output-inprogress-${featureId}"]`);
if (await runningBtn.isVisible()) {
await runningBtn.click();
@@ -104,10 +84,7 @@ export async function isDragHandleVisibleForFeature(
/**
* Get the drag handle element for a specific feature card
*/
export async function getDragHandleForFeature(
page: Page,
featureId: string
): Promise<Locator> {
export async function getDragHandleForFeature(page: Page, featureId: string): Promise<Locator> {
return page.locator(`[data-testid="drag-handle-${featureId}"]`);
}
@@ -134,9 +111,7 @@ export async function fillAddFeatureDialog(
options?: { branch?: string; category?: string }
): Promise<void> {
// Fill description (using the dropzone textarea)
const descriptionInput = page
.locator('[data-testid="add-feature-dialog"] textarea')
.first();
const descriptionInput = page.locator('[data-testid="add-feature-dialog"] textarea').first();
await descriptionInput.fill(description);
// Fill branch if provided (it's a combobox autocomplete)
@@ -145,36 +120,34 @@ export async function fillAddFeatureDialog(
const otherBranchRadio = page
.locator('[data-testid="feature-radio-group"]')
.locator('[id="feature-other"]');
await otherBranchRadio.waitFor({ state: "visible", timeout: 5000 });
await otherBranchRadio.waitFor({ state: 'visible', timeout: 5000 });
await otherBranchRadio.click();
// Wait for the branch input to appear
await page.waitForTimeout(300);
// Now click on the branch input (autocomplete)
const branchInput = page.locator('[data-testid="feature-input"]');
await branchInput.waitFor({ state: "visible", timeout: 5000 });
await branchInput.waitFor({ state: 'visible', timeout: 5000 });
await branchInput.click();
// Wait for the popover to open
await page.waitForTimeout(300);
// Type in the command input
const commandInput = page.locator("[cmdk-input]");
const commandInput = page.locator('[cmdk-input]');
await commandInput.fill(options.branch);
// Press Enter to select/create the branch
await commandInput.press("Enter");
await commandInput.press('Enter');
// Wait for popover to close
await page.waitForTimeout(200);
}
// Fill category if provided (it's also a combobox autocomplete)
if (options?.category) {
const categoryButton = page.locator(
'[data-testid="feature-category-input"]'
);
const categoryButton = page.locator('[data-testid="feature-category-input"]');
await categoryButton.click();
await page.waitForTimeout(300);
const commandInput = page.locator("[cmdk-input]");
const commandInput = page.locator('[cmdk-input]');
await commandInput.fill(options.category);
await commandInput.press("Enter");
await commandInput.press('Enter');
await page.waitForTimeout(200);
}
}
@@ -185,10 +158,9 @@ export async function fillAddFeatureDialog(
export async function confirmAddFeature(page: Page): Promise<void> {
await page.click('[data-testid="confirm-add-feature"]');
// Wait for dialog to close
await page.waitForFunction(
() => !document.querySelector('[data-testid="add-feature-dialog"]'),
{ timeout: 5000 }
);
await page.waitForFunction(() => !document.querySelector('[data-testid="add-feature-dialog"]'), {
timeout: 5000,
});
}
/**
@@ -218,12 +190,9 @@ export async function getWorktreeSelector(page: Page): Promise<Locator> {
/**
* Click on a branch button in the worktree selector
*/
export async function selectWorktreeBranch(
page: Page,
branchName: string
): Promise<void> {
const branchButton = page.getByRole("button", {
name: new RegExp(branchName, "i"),
export async function selectWorktreeBranch(page: Page, branchName: string): Promise<void> {
const branchButton = page.getByRole('button', {
name: new RegExp(branchName, 'i'),
});
await branchButton.click();
await page.waitForTimeout(500); // Wait for UI to update
@@ -232,9 +201,7 @@ export async function selectWorktreeBranch(
/**
* Get the currently selected branch in the worktree selector
*/
export async function getSelectedWorktreeBranch(
page: Page
): Promise<string | null> {
export async function getSelectedWorktreeBranch(page: Page): Promise<string | null> {
// The main branch button has aria-pressed="true" when selected
const selectedButton = page.locator(
'[data-testid="worktree-selector"] button[aria-pressed="true"]'
@@ -246,12 +213,9 @@ export async function getSelectedWorktreeBranch(
/**
* Check if a branch button is visible in the worktree selector
*/
export async function isWorktreeBranchVisible(
page: Page,
branchName: string
): Promise<boolean> {
const branchButton = page.getByRole("button", {
name: new RegExp(branchName, "i"),
export async function isWorktreeBranchVisible(page: Page, branchName: string): Promise<boolean> {
const branchButton = page.getByRole('button', {
name: new RegExp(branchName, 'i'),
});
return await branchButton.isVisible().catch(() => false);
}

View File

@@ -1,8 +1,8 @@
import { Page, Locator } from "@playwright/test";
import { clickElement, fillInput } from "../core/interactions";
import { waitForElement, waitForElementHidden } from "../core/waiting";
import { getByTestId } from "../core/elements";
import { expect } from "@playwright/test";
import { Page, Locator } from '@playwright/test';
import { clickElement, fillInput } from '../core/interactions';
import { waitForElement, waitForElementHidden } from '../core/waiting';
import { getByTestId } from '../core/elements';
import { expect } from '@playwright/test';
/**
* Get the context file list element
@@ -14,10 +14,7 @@ export async function getContextFileList(page: Page): Promise<Locator> {
/**
* Click on a context file in the list
*/
export async function clickContextFile(
page: Page,
fileName: string
): Promise<void> {
export async function clickContextFile(page: Page, fileName: string): Promise<void> {
const fileButton = page.locator(`[data-testid="context-file-${fileName}"]`);
await fileButton.click();
}
@@ -33,18 +30,15 @@ export async function getContextEditor(page: Page): Promise<Locator> {
* Get the context editor content
*/
export async function getContextEditorContent(page: Page): Promise<string> {
const editor = await getByTestId(page, "context-editor");
const editor = await getByTestId(page, 'context-editor');
return await editor.inputValue();
}
/**
* Set the context editor content
*/
export async function setContextEditorContent(
page: Page,
content: string
): Promise<void> {
const editor = await getByTestId(page, "context-editor");
export async function setContextEditorContent(page: Page, content: string): Promise<void> {
const editor = await getByTestId(page, 'context-editor');
await editor.fill(content);
}
@@ -52,8 +46,8 @@ export async function setContextEditorContent(
* Open the add context file dialog
*/
export async function openAddContextFileDialog(page: Page): Promise<void> {
await clickElement(page, "add-context-file");
await waitForElement(page, "add-context-dialog");
await clickElement(page, 'add-context-file');
await waitForElement(page, 'add-context-dialog');
}
/**
@@ -65,11 +59,11 @@ export async function createContextFile(
content: string
): Promise<void> {
await openAddContextFileDialog(page);
await clickElement(page, "add-text-type");
await fillInput(page, "new-file-name", filename);
await fillInput(page, "new-file-content", content);
await clickElement(page, "confirm-add-file");
await waitForElementHidden(page, "add-context-dialog");
await clickElement(page, 'add-text-type');
await fillInput(page, 'new-file-name', filename);
await fillInput(page, 'new-file-content', content);
await clickElement(page, 'confirm-add-file');
await waitForElementHidden(page, 'add-context-dialog');
}
/**
@@ -81,34 +75,32 @@ export async function createContextImage(
imagePath: string
): Promise<void> {
await openAddContextFileDialog(page);
await clickElement(page, "add-image-type");
await fillInput(page, "new-file-name", filename);
await clickElement(page, 'add-image-type');
await fillInput(page, 'new-file-name', filename);
await page.setInputFiles('[data-testid="image-upload-input"]', imagePath);
await clickElement(page, "confirm-add-file");
await waitForElementHidden(page, "add-context-dialog");
await clickElement(page, 'confirm-add-file');
await waitForElementHidden(page, 'add-context-dialog');
}
/**
* Delete a context file via the UI (must be selected first)
*/
export async function deleteSelectedContextFile(page: Page): Promise<void> {
await clickElement(page, "delete-context-file");
await waitForElement(page, "delete-context-dialog");
await clickElement(page, "confirm-delete-file");
await waitForElementHidden(page, "delete-context-dialog");
await clickElement(page, 'delete-context-file');
await waitForElement(page, 'delete-context-dialog');
await clickElement(page, 'confirm-delete-file');
await waitForElementHidden(page, 'delete-context-dialog');
}
/**
* Save the current context file
*/
export async function saveContextFile(page: Page): Promise<void> {
await clickElement(page, "save-context-file");
await clickElement(page, 'save-context-file');
// Wait for save to complete (button shows "Saved")
await page.waitForFunction(
() =>
document
.querySelector('[data-testid="save-context-file"]')
?.textContent?.includes("Saved"),
document.querySelector('[data-testid="save-context-file"]')?.textContent?.includes('Saved'),
{ timeout: 5000 }
);
}
@@ -117,7 +109,7 @@ export async function saveContextFile(page: Page): Promise<void> {
* Toggle markdown preview mode
*/
export async function toggleContextPreviewMode(page: Page): Promise<void> {
await clickElement(page, "toggle-preview-mode");
await clickElement(page, 'toggle-preview-mode');
}
/**
@@ -128,8 +120,8 @@ export async function waitForContextFile(
filename: string,
timeout: number = 10000
): Promise<void> {
const locator = await getByTestId(page, `context-file-${filename}`);
await locator.waitFor({ state: "visible", timeout });
const locator = page.locator(`[data-testid="context-file-${filename}"]`);
await locator.waitFor({ state: 'visible', timeout });
}
/**
@@ -142,13 +134,13 @@ export async function selectContextFile(
timeout: number = 10000
): Promise<void> {
const fileButton = await getByTestId(page, `context-file-${filename}`);
await fileButton.waitFor({ state: "visible", timeout });
await fileButton.waitFor({ state: 'visible', timeout });
// Use JavaScript click to ensure React onClick handler fires
await fileButton.evaluate((el) => (el as HTMLButtonElement).click());
// Wait for the file to be selected (toolbar with delete button becomes visible)
const deleteButton = await getByTestId(page, "delete-context-file");
const deleteButton = await getByTestId(page, 'delete-context-file');
await expect(deleteButton).toBeVisible({
timeout,
});
@@ -173,11 +165,11 @@ export async function switchToEditMode(page: Page): Promise<void> {
// First wait for content to load
await waitForFileContentToLoad(page);
const markdownPreview = await getByTestId(page, "markdown-preview");
const markdownPreview = await getByTestId(page, 'markdown-preview');
const isPreview = await markdownPreview.isVisible().catch(() => false);
if (isPreview) {
await clickElement(page, "toggle-preview-mode");
await clickElement(page, 'toggle-preview-mode');
await page.waitForSelector('[data-testid="context-editor"]', {
timeout: 5000,
});

View File

@@ -1,20 +1,19 @@
import { Page, Locator } from "@playwright/test";
import { getByTestId } from "../core/elements";
import { waitForElement } from "../core/waiting";
import { setupFirstRun } from "../project/setup";
import { Page, Locator } from '@playwright/test';
import { getByTestId } from '../core/elements';
import { waitForElement } from '../core/waiting';
/**
* Wait for setup view to be visible
*/
export async function waitForSetupView(page: Page): Promise<Locator> {
return waitForElement(page, "setup-view", { timeout: 10000 });
return waitForElement(page, 'setup-view', { timeout: 10000 });
}
/**
* Click "Get Started" button on setup welcome step
*/
export async function clickSetupGetStarted(page: Page): Promise<void> {
const button = await getByTestId(page, "setup-start-button");
const button = await getByTestId(page, 'setup-start-button');
await button.click();
}
@@ -22,7 +21,7 @@ export async function clickSetupGetStarted(page: Page): Promise<void> {
* Click continue on Claude setup step
*/
export async function clickClaudeContinue(page: Page): Promise<void> {
const button = await getByTestId(page, "claude-next-button");
const button = await getByTestId(page, 'claude-next-button');
await button.click();
}
@@ -30,46 +29,40 @@ export async function clickClaudeContinue(page: Page): Promise<void> {
* Click finish on setup complete step
*/
export async function clickSetupFinish(page: Page): Promise<void> {
const button = await getByTestId(page, "setup-finish-button");
const button = await getByTestId(page, 'setup-finish-button');
await button.click();
}
/**
* Enter Anthropic API key in setup
*/
export async function enterAnthropicApiKey(
page: Page,
apiKey: string
): Promise<void> {
export async function enterAnthropicApiKey(page: Page, apiKey: string): Promise<void> {
// Click "Use Anthropic API Key Instead" button
const useApiKeyButton = await getByTestId(page, "use-api-key-button");
const useApiKeyButton = await getByTestId(page, 'use-api-key-button');
await useApiKeyButton.click();
// Enter the API key
const input = await getByTestId(page, "anthropic-api-key-input");
const input = await getByTestId(page, 'anthropic-api-key-input');
await input.fill(apiKey);
// Click save button
const saveButton = await getByTestId(page, "save-anthropic-key-button");
const saveButton = await getByTestId(page, 'save-anthropic-key-button');
await saveButton.click();
}
/**
* Enter OpenAI API key in setup
*/
export async function enterOpenAIApiKey(
page: Page,
apiKey: string
): Promise<void> {
export async function enterOpenAIApiKey(page: Page, apiKey: string): Promise<void> {
// Click "Enter OpenAI API Key" button
const useApiKeyButton = await getByTestId(page, "use-openai-key-button");
const useApiKeyButton = await getByTestId(page, 'use-openai-key-button');
await useApiKeyButton.click();
// Enter the API key
const input = await getByTestId(page, "openai-api-key-input");
const input = await getByTestId(page, 'openai-api-key-input');
await input.fill(apiKey);
// Click save button
const saveButton = await getByTestId(page, "save-openai-key-button");
const saveButton = await getByTestId(page, 'save-openai-key-button');
await saveButton.click();
}

View File

@@ -1,6 +1,6 @@
import { Page, Locator } from "@playwright/test";
import { clickElement } from "../core/interactions";
import { navigateToSpec } from "../navigation/views";
import { Page, Locator } from '@playwright/test';
import { clickElement } from '../core/interactions';
import { navigateToSpec } from '../navigation/views';
/**
* Get the spec editor element
@@ -20,10 +20,7 @@ export async function getSpecEditorContent(page: Page): Promise<string> {
/**
* Set the spec editor content
*/
export async function setSpecEditorContent(
page: Page,
content: string
): Promise<void> {
export async function setSpecEditorContent(page: Page, content: string): Promise<void> {
const editor = await getSpecEditor(page);
await editor.fill(content);
}
@@ -32,14 +29,14 @@ export async function setSpecEditorContent(
* Click the save spec button
*/
export async function clickSaveSpec(page: Page): Promise<void> {
await clickElement(page, "save-spec");
await clickElement(page, 'save-spec');
}
/**
* Click the reload spec button
*/
export async function clickReloadSpec(page: Page): Promise<void> {
await clickElement(page, "reload-spec");
await clickElement(page, 'reload-spec');
}
/**
@@ -47,7 +44,7 @@ export async function clickReloadSpec(page: Page): Promise<void> {
*/
export async function getDisplayedSpecPath(page: Page): Promise<string | null> {
const specView = page.locator('[data-testid="spec-view"]');
const pathElement = specView.locator("p.text-muted-foreground").first();
const pathElement = specView.locator('p.text-muted-foreground').first();
return await pathElement.textContent();
}
@@ -60,13 +57,17 @@ export async function navigateToSpecEditor(page: Page): Promise<void> {
/**
* Get the CodeMirror editor content
* Waits for CodeMirror to be ready and returns the content
*/
export async function getEditorContent(page: Page): Promise<string> {
// CodeMirror uses a contenteditable div with class .cm-content
const content = await page
.locator('[data-testid="spec-editor"] .cm-content')
.textContent();
return content || "";
// Wait for it to be visible and then read its textContent
const contentElement = page.locator('[data-testid="spec-editor"] .cm-content');
await contentElement.waitFor({ state: 'visible', timeout: 10000 });
// Read the content - CodeMirror should have updated its DOM by now
const content = await contentElement.textContent();
return content || '';
}
/**
@@ -81,14 +82,14 @@ export async function setEditorContent(page: Page, content: string): Promise<voi
await page.waitForTimeout(200);
// Select all content (Cmd+A on Mac, Ctrl+A on others)
const isMac = process.platform === "darwin";
await page.keyboard.press(isMac ? "Meta+a" : "Control+a");
const isMac = process.platform === 'darwin';
await page.keyboard.press(isMac ? 'Meta+a' : 'Control+a');
// Wait for selection
await page.waitForTimeout(100);
// Delete the selected content first
await page.keyboard.press("Backspace");
await page.keyboard.press('Backspace');
// Wait for deletion
await page.waitForTimeout(100);
@@ -111,7 +112,7 @@ export async function clickSaveButton(page: Page): Promise<void> {
await page.waitForFunction(
() => {
const btn = document.querySelector('[data-testid="save-spec"]');
return btn?.textContent?.includes("Saved");
return btn?.textContent?.includes('Saved');
},
{ timeout: 5000 }
);