storkit: merge 374_story_web_ui_implements_all_bot_commands_as_slash_commands
This commit is contained in:
@@ -1,41 +1,41 @@
|
||||
{
|
||||
"name": "living-spec-standalone",
|
||||
"private": true,
|
||||
"version": "0.5.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"server": "cargo run --manifest-path server/Cargo.toml",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run",
|
||||
"test:e2e": "playwright test",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-syntax-highlighter": "^16.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.2",
|
||||
"@playwright/test": "^1.47.2",
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/node": "^25.0.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"@vitest/coverage-v8": "^2.1.9",
|
||||
"jest": "^29.0.0",
|
||||
"jsdom": "^28.1.0",
|
||||
"ts-jest": "^29.0.0",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^5.4.21",
|
||||
"vitest": "^2.1.4"
|
||||
}
|
||||
"name": "living-spec-standalone",
|
||||
"private": true,
|
||||
"version": "0.5.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"server": "cargo run --manifest-path server/Cargo.toml",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run",
|
||||
"test:e2e": "playwright test",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-syntax-highlighter": "^16.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.2",
|
||||
"@playwright/test": "^1.47.2",
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/node": "^25.0.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"@vitest/coverage-v8": "^2.1.9",
|
||||
"jest": "^29.0.0",
|
||||
"jsdom": "^28.1.0",
|
||||
"ts-jest": "^29.0.0",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^5.4.21",
|
||||
"vitest": "^2.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { defineConfig } from "@playwright/test";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineConfig } from "@playwright/test";
|
||||
|
||||
const configDir = dirname(fileURLToPath(new URL(import.meta.url)));
|
||||
const frontendRoot = resolve(configDir, ".");
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e",
|
||||
fullyParallel: true,
|
||||
timeout: 30_000,
|
||||
expect: {
|
||||
timeout: 5_000,
|
||||
},
|
||||
use: {
|
||||
baseURL: "http://127.0.0.1:41700",
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
webServer: {
|
||||
command:
|
||||
"pnpm exec vite --config vite.config.ts --host 127.0.0.1 --port 41700 --strictPort",
|
||||
url: "http://127.0.0.1:41700/@vite/client",
|
||||
reuseExistingServer: true,
|
||||
timeout: 120_000,
|
||||
cwd: frontendRoot,
|
||||
},
|
||||
testDir: "./tests/e2e",
|
||||
fullyParallel: true,
|
||||
timeout: 30_000,
|
||||
expect: {
|
||||
timeout: 5_000,
|
||||
},
|
||||
use: {
|
||||
baseURL: "http://127.0.0.1:41700",
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
webServer: {
|
||||
command:
|
||||
"pnpm exec vite --config vite.config.ts --host 127.0.0.1 --port 41700 --strictPort",
|
||||
url: "http://127.0.0.1:41700/@vite/client",
|
||||
reuseExistingServer: true,
|
||||
timeout: 120_000,
|
||||
cwd: frontendRoot,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -382,6 +382,14 @@ export const api = {
|
||||
deleteStory(storyId: string) {
|
||||
return callMcpTool("delete_story", { story_id: storyId });
|
||||
},
|
||||
/** Execute a bot slash command without LLM invocation. Returns markdown response text. */
|
||||
botCommand(command: string, args: string, baseUrl?: string) {
|
||||
return requestJson<{ response: string }>(
|
||||
"/bot/command",
|
||||
{ method: "POST", body: JSON.stringify({ command, args }) },
|
||||
baseUrl,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
async function callMcpTool(
|
||||
|
||||
@@ -40,6 +40,7 @@ vi.mock("../api/client", () => {
|
||||
setAnthropicApiKey: vi.fn(),
|
||||
readFile: vi.fn(),
|
||||
listProjectFiles: vi.fn(),
|
||||
botCommand: vi.fn(),
|
||||
};
|
||||
class ChatWebSocket {
|
||||
connect(handlers: WsHandlers) {
|
||||
@@ -64,6 +65,7 @@ const mockedApi = {
|
||||
setAnthropicApiKey: vi.mocked(api.setAnthropicApiKey),
|
||||
readFile: vi.mocked(api.readFile),
|
||||
listProjectFiles: vi.mocked(api.listProjectFiles),
|
||||
botCommand: vi.mocked(api.botCommand),
|
||||
};
|
||||
|
||||
function setupMocks() {
|
||||
@@ -76,6 +78,7 @@ function setupMocks() {
|
||||
mockedApi.listProjectFiles.mockResolvedValue([]);
|
||||
mockedApi.cancelChat.mockResolvedValue(true);
|
||||
mockedApi.setAnthropicApiKey.mockResolvedValue(true);
|
||||
mockedApi.botCommand.mockResolvedValue({ response: "Bot response" });
|
||||
}
|
||||
|
||||
describe("Default provider selection (Story 206)", () => {
|
||||
@@ -1457,3 +1460,204 @@ describe("File reference expansion (Story 269 AC4)", () => {
|
||||
expect(mockedApi.readFile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Slash command handling (Story 374)", () => {
|
||||
beforeEach(() => {
|
||||
capturedWsHandlers = null;
|
||||
lastSendChatArgs = null;
|
||||
setupMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("AC: /status calls botCommand and displays response", async () => {
|
||||
mockedApi.botCommand.mockResolvedValue({ response: "Pipeline: 3 active" });
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||
|
||||
const input = screen.getByPlaceholderText("Send a message...");
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "/status" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApi.botCommand).toHaveBeenCalledWith(
|
||||
"status",
|
||||
"",
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
expect(await screen.findByText("Pipeline: 3 active")).toBeInTheDocument();
|
||||
// Should NOT go to LLM
|
||||
expect(lastSendChatArgs).toBeNull();
|
||||
});
|
||||
|
||||
it("AC: /status <number> passes args to botCommand", async () => {
|
||||
mockedApi.botCommand.mockResolvedValue({ response: "Story 42 details" });
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||
|
||||
const input = screen.getByPlaceholderText("Send a message...");
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "/status 42" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApi.botCommand).toHaveBeenCalledWith(
|
||||
"status",
|
||||
"42",
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("AC: /start <number> calls botCommand", async () => {
|
||||
mockedApi.botCommand.mockResolvedValue({ response: "Started agent" });
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||
|
||||
const input = screen.getByPlaceholderText("Send a message...");
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "/start 42 opus" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApi.botCommand).toHaveBeenCalledWith(
|
||||
"start",
|
||||
"42 opus",
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
expect(await screen.findByText("Started agent")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("AC: /git calls botCommand", async () => {
|
||||
mockedApi.botCommand.mockResolvedValue({ response: "On branch main" });
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||
|
||||
const input = screen.getByPlaceholderText("Send a message...");
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "/git" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApi.botCommand).toHaveBeenCalledWith("git", "", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it("AC: /cost calls botCommand", async () => {
|
||||
mockedApi.botCommand.mockResolvedValue({ response: "$1.23 today" });
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||
|
||||
const input = screen.getByPlaceholderText("Send a message...");
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "/cost" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApi.botCommand).toHaveBeenCalledWith("cost", "", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it("AC: /reset clears messages and session without LLM", async () => {
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||
|
||||
// First add a message so there is history to clear
|
||||
act(() => {
|
||||
capturedWsHandlers?.onUpdate([
|
||||
{ role: "user", content: "hello" },
|
||||
{ role: "assistant", content: "world" },
|
||||
]);
|
||||
});
|
||||
expect(await screen.findByText("world")).toBeInTheDocument();
|
||||
|
||||
const input = screen.getByPlaceholderText("Send a message...");
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "/reset" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
// LLM must NOT be invoked
|
||||
expect(lastSendChatArgs).toBeNull();
|
||||
// botCommand must NOT be invoked (reset is frontend-only)
|
||||
expect(mockedApi.botCommand).not.toHaveBeenCalled();
|
||||
// Confirmation message should appear
|
||||
expect(await screen.findByText(/Session reset/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("AC: unrecognised slash command shows error message", async () => {
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||
|
||||
const input = screen.getByPlaceholderText("Send a message...");
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "/foobar" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
expect(await screen.findByText(/Unknown command/)).toBeInTheDocument();
|
||||
// Should NOT go to LLM
|
||||
expect(lastSendChatArgs).toBeNull();
|
||||
// Should NOT call botCommand
|
||||
expect(mockedApi.botCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("AC: /help shows help overlay", async () => {
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||
|
||||
const input = screen.getByPlaceholderText("Send a message...");
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "/help" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId("help-overlay")).toBeInTheDocument();
|
||||
expect(lastSendChatArgs).toBeNull();
|
||||
expect(mockedApi.botCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("AC: botCommand API error shows error message in chat", async () => {
|
||||
mockedApi.botCommand.mockRejectedValue(new Error("Server error"));
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||
|
||||
const input = screen.getByPlaceholderText("Send a message...");
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "/git" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByText(/Error running command/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -612,6 +612,80 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// /reset — clear session and message history without LLM
|
||||
if (/^\/reset\s*$/i.test(messageText)) {
|
||||
setMessages([]);
|
||||
setClaudeSessionId(null);
|
||||
setStreamingContent("");
|
||||
setStreamingThinking("");
|
||||
setActivityStatus(null);
|
||||
setMessages([
|
||||
{
|
||||
role: "assistant",
|
||||
content: "Session reset. Starting a fresh conversation.",
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Slash commands forwarded to the backend bot command endpoint
|
||||
const slashMatch = messageText.match(/^\/(\S+)(?:\s+([\s\S]*))?$/);
|
||||
if (slashMatch) {
|
||||
const cmd = slashMatch[1].toLowerCase();
|
||||
const args = (slashMatch[2] ?? "").trim();
|
||||
|
||||
// Ignore commands handled elsewhere
|
||||
if (cmd !== "btw") {
|
||||
const knownCommands = new Set([
|
||||
"status",
|
||||
"assign",
|
||||
"start",
|
||||
"show",
|
||||
"move",
|
||||
"delete",
|
||||
"cost",
|
||||
"git",
|
||||
"overview",
|
||||
"rebuild",
|
||||
]);
|
||||
|
||||
if (knownCommands.has(cmd)) {
|
||||
// Show the slash command in chat as a user message (display only)
|
||||
setMessages((prev: Message[]) => [
|
||||
...prev,
|
||||
{ role: "user", content: messageText },
|
||||
]);
|
||||
try {
|
||||
const result = await api.botCommand(cmd, args, undefined);
|
||||
setMessages((prev: Message[]) => [
|
||||
...prev,
|
||||
{ role: "assistant", content: result.response },
|
||||
]);
|
||||
} catch (e) {
|
||||
setMessages((prev: Message[]) => [
|
||||
...prev,
|
||||
{
|
||||
role: "assistant",
|
||||
content: `**Error running command:** ${e}`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Unknown slash command
|
||||
setMessages((prev: Message[]) => [
|
||||
...prev,
|
||||
{ role: "user", content: messageText },
|
||||
{
|
||||
role: "assistant",
|
||||
content: `Unknown command: \`/${cmd}\`. Type \`/help\` to see available commands.`,
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// /btw <question> — answered from context without disrupting main chat
|
||||
const btwMatch = messageText.match(/^\/btw\s+(.+)/s);
|
||||
if (btwMatch) {
|
||||
|
||||
@@ -12,6 +12,57 @@ const SLASH_COMMANDS: SlashCommand[] = [
|
||||
name: "/help",
|
||||
description: "Show this list of available slash commands.",
|
||||
},
|
||||
{
|
||||
name: "/status",
|
||||
description:
|
||||
"Show pipeline status and agent availability. `/status <number>` shows a story triage dump.",
|
||||
},
|
||||
{
|
||||
name: "/assign <number> <model>",
|
||||
description: "Pre-assign a model to a story (e.g. `/assign 42 opus`).",
|
||||
},
|
||||
{
|
||||
name: "/start <number>",
|
||||
description:
|
||||
"Start a coder on a story. Optionally specify a model: `/start <number> opus`.",
|
||||
},
|
||||
{
|
||||
name: "/show <number>",
|
||||
description: "Display the full text of a work item.",
|
||||
},
|
||||
{
|
||||
name: "/move <number> <stage>",
|
||||
description:
|
||||
"Move a work item to a pipeline stage (backlog, current, qa, merge, done).",
|
||||
},
|
||||
{
|
||||
name: "/delete <number>",
|
||||
description:
|
||||
"Remove a work item from the pipeline and stop any running agent.",
|
||||
},
|
||||
{
|
||||
name: "/cost",
|
||||
description:
|
||||
"Show token spend: 24h total, top stories, breakdown by agent type, and all-time total.",
|
||||
},
|
||||
{
|
||||
name: "/git",
|
||||
description:
|
||||
"Show git status: branch, uncommitted changes, and ahead/behind remote.",
|
||||
},
|
||||
{
|
||||
name: "/overview <number>",
|
||||
description: "Show the implementation summary for a merged story.",
|
||||
},
|
||||
{
|
||||
name: "/rebuild",
|
||||
description: "Rebuild the server binary and restart.",
|
||||
},
|
||||
{
|
||||
name: "/reset",
|
||||
description:
|
||||
"Clear the current Claude Code session and start fresh (messages and session ID are cleared locally).",
|
||||
},
|
||||
{
|
||||
name: "/btw <question>",
|
||||
description:
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -3,49 +3,49 @@ import { defineConfig } from "vite";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(() => {
|
||||
const backendPort = Number(process.env.STORKIT_PORT || "3001");
|
||||
return {
|
||||
plugins: [react()],
|
||||
define: {
|
||||
__STORKIT_PORT__: JSON.stringify(String(backendPort)),
|
||||
__BUILD_TIME__: JSON.stringify(new Date().toISOString()),
|
||||
},
|
||||
server: {
|
||||
port: backendPort + 2172,
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: `http://127.0.0.1:${String(backendPort)}`,
|
||||
timeout: 120000,
|
||||
configure: (proxy) => {
|
||||
proxy.on("error", (_err) => {
|
||||
// Swallow proxy errors (e.g. ECONNREFUSED during backend restart)
|
||||
// so the vite dev server doesn't crash.
|
||||
});
|
||||
},
|
||||
},
|
||||
"/agents": {
|
||||
target: `http://127.0.0.1:${String(backendPort)}`,
|
||||
timeout: 120000,
|
||||
configure: (proxy) => {
|
||||
proxy.on("error", (_err) => {});
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
ignored: [
|
||||
"**/.story_kit/**",
|
||||
"**/target/**",
|
||||
"**/.git/**",
|
||||
"**/server/**",
|
||||
"**/Cargo.*",
|
||||
"**/vendor/**",
|
||||
"**/node_modules/**",
|
||||
],
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: "dist",
|
||||
emptyOutDir: true,
|
||||
},
|
||||
};
|
||||
const backendPort = Number(process.env.STORKIT_PORT || "3001");
|
||||
return {
|
||||
plugins: [react()],
|
||||
define: {
|
||||
__STORKIT_PORT__: JSON.stringify(String(backendPort)),
|
||||
__BUILD_TIME__: JSON.stringify(new Date().toISOString()),
|
||||
},
|
||||
server: {
|
||||
port: backendPort + 2172,
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: `http://127.0.0.1:${String(backendPort)}`,
|
||||
timeout: 120000,
|
||||
configure: (proxy) => {
|
||||
proxy.on("error", (_err) => {
|
||||
// Swallow proxy errors (e.g. ECONNREFUSED during backend restart)
|
||||
// so the vite dev server doesn't crash.
|
||||
});
|
||||
},
|
||||
},
|
||||
"/agents": {
|
||||
target: `http://127.0.0.1:${String(backendPort)}`,
|
||||
timeout: 120000,
|
||||
configure: (proxy) => {
|
||||
proxy.on("error", (_err) => {});
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
ignored: [
|
||||
"**/.story_kit/**",
|
||||
"**/target/**",
|
||||
"**/.git/**",
|
||||
"**/server/**",
|
||||
"**/Cargo.*",
|
||||
"**/vendor/**",
|
||||
"**/node_modules/**",
|
||||
],
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: "dist",
|
||||
emptyOutDir: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2,26 +2,26 @@ import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
define: {
|
||||
__BUILD_TIME__: JSON.stringify("2026-01-01T00:00:00.000Z"),
|
||||
},
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
environmentOptions: {
|
||||
jsdom: {
|
||||
url: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
globals: true,
|
||||
testTimeout: 10_000,
|
||||
setupFiles: ["./src/setupTests.ts"],
|
||||
css: true,
|
||||
exclude: ["tests/e2e/**", "node_modules/**"],
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "json-summary"],
|
||||
reportsDirectory: "./coverage",
|
||||
},
|
||||
},
|
||||
plugins: [react()],
|
||||
define: {
|
||||
__BUILD_TIME__: JSON.stringify("2026-01-01T00:00:00.000Z"),
|
||||
},
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
environmentOptions: {
|
||||
jsdom: {
|
||||
url: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
globals: true,
|
||||
testTimeout: 10_000,
|
||||
setupFiles: ["./src/setupTests.ts"],
|
||||
css: true,
|
||||
exclude: ["tests/e2e/**", "node_modules/**"],
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "json-summary"],
|
||||
reportsDirectory: "./coverage",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user