diff --git a/package.json b/package.json
index 79e71e3..cb8d0e8 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
- "dev": "sh -c 'npm run dev:server & npm run dev:client & wait'",
+ "dev": "bash -c 'trap \"kill 0\" EXIT; npm run dev:server & npm run dev:client & wait'",
"dev:server": "tsx watch src/server/index.ts",
"dev:client": "vite --host 0.0.0.0",
"build": "tsc && vite build",
diff --git a/src/client/index.html b/src/client/index.html
index 9d6b434..58b6f29 100644
--- a/src/client/index.html
+++ b/src/client/index.html
@@ -4,6 +4,9 @@
Pi Web POC
+
diff --git a/src/client/src/components/ChatView.ts b/src/client/src/components/ChatView.ts
new file mode 100644
index 0000000..cbc55c6
--- /dev/null
+++ b/src/client/src/components/ChatView.ts
@@ -0,0 +1,19 @@
+import { LitElement, html } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import type { ChatLine } from "./shared";
+import { chatStyles } from "./shared";
+
+@customElement("chat-view")
+export class ChatView extends LitElement {
+ @property({ attribute: false }) messages: ChatLine[] = [];
+
+ render() {
+ return html`
+
+ ${this.messages.map((message) => html`
${message.role}${message.text} `)}
+
+ `;
+ }
+
+ static styles = chatStyles;
+}
diff --git a/src/client/src/components/Composer.ts b/src/client/src/components/Composer.ts
new file mode 100644
index 0000000..af57b6c
--- /dev/null
+++ b/src/client/src/components/Composer.ts
@@ -0,0 +1,41 @@
+import { LitElement, html } from "lit";
+import { customElement, property, state } from "lit/decorators.js";
+import { composerStyles } from "./shared";
+
+@customElement("chat-composer")
+export class Composer extends LitElement {
+ @property({ type: Boolean }) disabled = false;
+ @property({ attribute: false }) onSend?: (text: string) => void;
+ @property({ attribute: false }) onCloseSession?: () => void;
+ @state() private draft = "";
+
+ render() {
+ return html`
+
+ `;
+ }
+
+ private send() {
+ const text = this.draft.trim();
+ if (!text || this.disabled) return;
+ this.draft = "";
+ this.onSend?.(text);
+ }
+
+ static styles = composerStyles;
+}
diff --git a/src/client/src/components/PiWebApp.ts b/src/client/src/components/PiWebApp.ts
new file mode 100644
index 0000000..71a8b0b
--- /dev/null
+++ b/src/client/src/components/PiWebApp.ts
@@ -0,0 +1,191 @@
+import { LitElement, html } from "lit";
+import { customElement, state } from "lit/decorators.js";
+import { api, type Project, type SessionInfo, type Workspace } from "../api";
+import { readRoute, writeRoute } from "../route";
+import { SessionSocket, type SessionUiEvent } from "../sessionSocket";
+import "./ProjectList";
+import "./WorkspaceList";
+import "./SessionList";
+import "./ChatView";
+import "./Composer";
+import { appStyles, type ChatLine } from "./shared";
+
+@customElement("pi-web-poc")
+export class PiWebApp extends LitElement {
+ @state() private projects: Project[] = [];
+ @state() private workspaces: Workspace[] = [];
+ @state() private sessions: SessionInfo[] = [];
+ @state() private messages: ChatLine[] = [];
+ @state() private selectedProject?: Project;
+ @state() private selectedWorkspace?: Workspace;
+ @state() private selectedSession?: SessionInfo;
+ @state() private error = "";
+
+ private readonly socket = new SessionSocket();
+ private readonly onPopState = () => void this.restoreRoute(false);
+
+ connectedCallback(): void {
+ super.connectedCallback();
+ window.addEventListener("popstate", this.onPopState);
+ void this.loadProjects();
+ }
+
+ disconnectedCallback(): void {
+ window.removeEventListener("popstate", this.onPopState);
+ this.socket.close();
+ super.disconnectedCallback();
+ }
+
+ private async loadProjects() {
+ this.error = "";
+ try {
+ this.projects = await api.projects();
+ await this.restoreRoute(false);
+ } catch (error) {
+ this.error = String(error);
+ }
+ }
+
+ private async addProject() {
+ const path = prompt("Project folder path");
+ if (!path) return;
+ try {
+ const project = await api.addProject(path);
+ this.projects = [...this.projects.filter((p) => p.id !== project.id), project];
+ await this.selectProject(project);
+ } catch (error) {
+ this.error = String(error);
+ }
+ }
+
+ private async selectProject(project: Project, target?: { workspaceId?: string; sessionId?: string; updateUrl?: boolean }) {
+ this.selectedProject = project;
+ this.selectedWorkspace = undefined;
+ this.selectedSession = undefined;
+ this.sessions = [];
+ this.messages = [];
+ this.socket.close();
+ try {
+ this.workspaces = await api.workspaces(project.id);
+ const workspace = target?.workspaceId ? this.workspaces.find((w) => w.id === target.workspaceId) : this.workspaces[0];
+ if (workspace) await this.selectWorkspace(workspace, { sessionId: target?.sessionId, updateUrl: target?.updateUrl });
+ else if (target?.updateUrl !== false) this.updateUrl();
+ } catch (error) {
+ this.error = String(error);
+ }
+ }
+
+ private async selectWorkspace(workspace: Workspace, target?: { sessionId?: string; updateUrl?: boolean }) {
+ this.selectedWorkspace = workspace;
+ this.selectedSession = undefined;
+ this.messages = [];
+ this.socket.close();
+ try {
+ this.sessions = await api.sessions(workspace.path);
+ const sessionId = target?.sessionId;
+ const session = sessionId ? this.sessions.find((s) => s.id === sessionId || s.id.startsWith(sessionId)) : undefined;
+ if (session) await this.selectSession(session, { updateUrl: target?.updateUrl });
+ else if (target?.updateUrl !== false) this.updateUrl();
+ } catch (error) {
+ this.error = String(error);
+ }
+ }
+
+ private async startSession() {
+ if (!this.selectedWorkspace) return;
+ try {
+ const session = await api.startSession(this.selectedWorkspace.path);
+ this.sessions = [session, ...this.sessions];
+ await this.selectSession(session);
+ } catch (error) {
+ this.error = String(error);
+ }
+ }
+
+ private async selectSession(session: SessionInfo, options?: { updateUrl?: boolean }) {
+ this.selectedSession = session;
+ this.socket.close();
+ this.messages = normalizeMessages(await api.messages(session.id));
+ this.socket.connect(session.id, (event) => this.applyEvent(event));
+ if (options?.updateUrl !== false) this.updateUrl();
+ }
+
+ private applyEvent(event: SessionUiEvent) {
+ if (event.type === "assistant.delta") {
+ const lines = [...this.messages];
+ const last = lines.at(-1);
+ if (last?.role === "assistant") last.text += event.text;
+ else lines.push({ role: "assistant", text: event.text });
+ this.messages = lines;
+ } else if (event.type === "tool.start") {
+ this.messages = [...this.messages, { role: "tool", text: `▶ ${event.toolName}` }];
+ } else if (event.type === "tool.end") {
+ this.messages = [...this.messages, { role: "tool", text: `${event.isError ? "✖" : "✓"} ${event.toolName}` }];
+ } else if (event.type === "session.error") {
+ this.messages = [...this.messages, { role: "system", text: event.message }];
+ }
+ }
+
+ private async send(text: string) {
+ if (!this.selectedSession) return;
+ this.messages = [...this.messages, { role: "user", text }];
+ try {
+ await api.prompt(this.selectedSession.id, text);
+ } catch (error) {
+ this.error = String(error);
+ }
+ }
+
+ private async closeSession() {
+ if (!this.selectedSession) return;
+ await api.close(this.selectedSession.id);
+ this.selectedSession = undefined;
+ this.socket.close();
+ this.messages = [];
+ this.updateUrl();
+ }
+
+ private async restoreRoute(updateUrl: boolean) {
+ const route = readRoute();
+ if (!route.projectId) return;
+ const project = this.projects.find((p) => p.id === route.projectId);
+ if (!project) return;
+ await this.selectProject(project, { workspaceId: route.workspaceId, sessionId: route.sessionId, updateUrl });
+ }
+
+ private updateUrl() {
+ writeRoute({ projectId: this.selectedProject?.id, workspaceId: this.selectedWorkspace?.id, sessionId: this.selectedSession?.id });
+ }
+
+ render() {
+ return html`
+
+
+
+ ${this.error ? html`${this.error}
` : null}
+ ${this.selectedSession ? html`
+
+ this.send(text)} .onCloseSession=${() => this.closeSession()}>
+ ` : html`Select or start a session.
`}
+
+
+ `;
+ }
+
+ static styles = appStyles;
+}
+
+function normalizeMessages(messages: any[]): ChatLine[] {
+ return messages.map((message) => ({
+ role: message.role === "assistant" ? "assistant" : message.role === "user" ? "user" : "system",
+ text: typeof message.content === "string" ? message.content : JSON.stringify(message.content, null, 2),
+ }));
+}
diff --git a/src/client/src/components/ProjectList.ts b/src/client/src/components/ProjectList.ts
new file mode 100644
index 0000000..5baf830
--- /dev/null
+++ b/src/client/src/components/ProjectList.ts
@@ -0,0 +1,26 @@
+import { LitElement, html } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import type { Project } from "../api";
+import { listStyles } from "./shared";
+
+@customElement("project-list")
+export class ProjectList extends LitElement {
+ @property({ attribute: false }) projects: Project[] = [];
+ @property({ attribute: false }) selected?: Project;
+ @property({ attribute: false }) onSelect?: (project: Project) => void;
+
+ render() {
+ return html`
+
+ Projects
+ ${this.projects.map((project) => html`
+
+ `)}
+
+ `;
+ }
+
+ static styles = listStyles;
+}
diff --git a/src/client/src/components/SessionList.ts b/src/client/src/components/SessionList.ts
new file mode 100644
index 0000000..54b5f6f
--- /dev/null
+++ b/src/client/src/components/SessionList.ts
@@ -0,0 +1,28 @@
+import { LitElement, html } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import type { SessionInfo } from "../api";
+import { listStyles } from "./shared";
+
+@customElement("session-list")
+export class SessionList extends LitElement {
+ @property({ attribute: false }) sessions: SessionInfo[] = [];
+ @property({ attribute: false }) selected?: SessionInfo;
+ @property({ type: Boolean }) canStart = false;
+ @property({ attribute: false }) onSelect?: (session: SessionInfo) => void;
+ @property({ attribute: false }) onStart?: () => void;
+
+ render() {
+ return html`
+
+ Sessions
+ ${this.sessions.map((session) => html`
+
+ `)}
+
+ `;
+ }
+
+ static styles = listStyles;
+}
diff --git a/src/client/src/components/WorkspaceList.ts b/src/client/src/components/WorkspaceList.ts
new file mode 100644
index 0000000..c0b36e5
--- /dev/null
+++ b/src/client/src/components/WorkspaceList.ts
@@ -0,0 +1,26 @@
+import { LitElement, html } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import type { Workspace } from "../api";
+import { listStyles } from "./shared";
+
+@customElement("workspace-list")
+export class WorkspaceList extends LitElement {
+ @property({ attribute: false }) workspaces: Workspace[] = [];
+ @property({ attribute: false }) selected?: Workspace;
+ @property({ attribute: false }) onSelect?: (workspace: Workspace) => void;
+
+ render() {
+ return html`
+
+ Workspaces
+ ${this.workspaces.map((workspace) => html`
+
+ `)}
+
+ `;
+ }
+
+ static styles = listStyles;
+}
diff --git a/src/client/src/components/shared.ts b/src/client/src/components/shared.ts
new file mode 100644
index 0000000..f882f90
--- /dev/null
+++ b/src/client/src/components/shared.ts
@@ -0,0 +1,50 @@
+import { css } from "lit";
+
+export interface ChatLine {
+ role: "user" | "assistant" | "tool" | "system";
+ text: string;
+}
+
+export const appStyles = css`
+ :host { display: block; height: 100vh; color: #e6edf3; background: #0d1117; font: 14px system-ui, sans-serif; }
+ .shell { display: grid; grid-template-columns: 340px 1fr; height: 100%; min-height: 0; }
+ aside { display: flex; flex-direction: column; min-height: 0; border-right: 1px solid #30363d; overflow: hidden; }
+ header { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; padding: 12px; border-bottom: 1px solid #30363d; }
+ project-list, workspace-list { flex: 0 0 auto; max-height: 26%; overflow: auto; border-bottom: 1px solid #21262d; }
+ session-list { flex: 1 1 auto; min-height: 0; overflow: auto; }
+ main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
+ chat-view { flex: 1 1 auto; min-height: 0; overflow: auto; }
+ chat-composer { flex: 0 0 auto; }
+ button { border: 1px solid #30363d; border-radius: 8px; background: #161b22; color: #e6edf3; padding: 7px 9px; cursor: pointer; }
+ .empty { margin: auto; color: #8b949e; }
+ .error { padding: 10px 16px; border-bottom: 1px solid #30363d; color: #ff7b72; }
+`;
+
+export const listStyles = css`
+ :host { display: block; color: #e6edf3; font: 14px system-ui, sans-serif; }
+ section { padding: 10px; }
+ h2 { display: flex; justify-content: space-between; align-items: center; margin: 0 0 8px; color: #8b949e; font-size: 12px; text-transform: uppercase; }
+ button { border: 1px solid #30363d; border-radius: 8px; background: #161b22; color: #e6edf3; padding: 7px 9px; cursor: pointer; }
+ section > button { display: block; width: 100%; text-align: left; margin: 6px 0; }
+ button.selected { border-color: #58a6ff; background: #0d2847; }
+ button:disabled { opacity: .5; cursor: not-allowed; }
+ small { display: block; color: #8b949e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+`;
+
+export const chatStyles = css`
+ :host { display: block; min-height: 0; color: #e6edf3; font: 14px system-ui, sans-serif; }
+ .chat { height: 100%; overflow: auto; padding: 16px; box-sizing: border-box; }
+ .msg { margin: 0 0 14px; padding: 12px; border: 1px solid #30363d; border-radius: 10px; background: #161b22; }
+ .msg.user { border-color: #2f81f7; }
+ .msg.tool { color: #d29922; }
+ .msg.system { color: #ff7b72; }
+ pre { margin: 6px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
+`;
+
+export const composerStyles = css`
+ :host { display: block; color: #e6edf3; font: 14px system-ui, sans-serif; }
+ footer { display: grid; grid-template-columns: 1fr auto auto; gap: 8px; padding: 12px; border-top: 1px solid #30363d; }
+ textarea { min-height: 54px; resize: vertical; border-radius: 8px; border: 1px solid #30363d; background: #0d1117; color: #e6edf3; padding: 8px; }
+ button { border: 1px solid #30363d; border-radius: 8px; background: #161b22; color: #e6edf3; padding: 7px 9px; cursor: pointer; }
+ button:disabled, textarea:disabled { opacity: .5; cursor: not-allowed; }
+`;
diff --git a/src/client/src/main.ts b/src/client/src/main.ts
index 8876890..262c3fa 100644
--- a/src/client/src/main.ts
+++ b/src/client/src/main.ts
@@ -1,224 +1 @@
-import { LitElement, css, html } from "lit";
-import { customElement, state } from "lit/decorators.js";
-import { api, sessionEvents, type Project, type SessionInfo, type Workspace } from "./api";
-
-interface ChatLine {
- role: "user" | "assistant" | "tool" | "system";
- text: string;
-}
-
-@customElement("pi-web-poc")
-class PiWebPoc extends LitElement {
- @state() private projects: Project[] = [];
- @state() private workspaces: Workspace[] = [];
- @state() private sessions: SessionInfo[] = [];
- @state() private messages: ChatLine[] = [];
- @state() private selectedProject?: Project;
- @state() private selectedWorkspace?: Workspace;
- @state() private selectedSession?: SessionInfo;
- @state() private error = "";
- @state() private draft = "";
- private socket?: WebSocket;
-
- connectedCallback(): void {
- super.connectedCallback();
- void this.loadProjects();
- }
-
- disconnectedCallback(): void {
- this.socket?.close();
- super.disconnectedCallback();
- }
-
- private async loadProjects() {
- this.error = "";
- try {
- this.projects = await api.projects();
- } catch (error) {
- this.error = String(error);
- }
- }
-
- private async addProject() {
- const path = prompt("Project folder path");
- if (!path) return;
- try {
- const project = await api.addProject(path);
- this.projects = [...this.projects.filter((p) => p.id !== project.id), project];
- await this.selectProject(project);
- } catch (error) {
- this.error = String(error);
- }
- }
-
- private async selectProject(project: Project) {
- this.selectedProject = project;
- this.selectedWorkspace = undefined;
- this.selectedSession = undefined;
- this.sessions = [];
- this.messages = [];
- try {
- this.workspaces = await api.workspaces(project.id);
- if (this.workspaces[0]) await this.selectWorkspace(this.workspaces[0]);
- } catch (error) {
- this.error = String(error);
- }
- }
-
- private async selectWorkspace(workspace: Workspace) {
- this.selectedWorkspace = workspace;
- this.selectedSession = undefined;
- this.messages = [];
- try {
- this.sessions = await api.sessions(workspace.path);
- } catch (error) {
- this.error = String(error);
- }
- }
-
- private async startSession() {
- if (!this.selectedWorkspace) return;
- try {
- const session = await api.startSession(this.selectedWorkspace.path);
- this.sessions = [session, ...this.sessions];
- await this.selectSession(session);
- } catch (error) {
- this.error = String(error);
- }
- }
-
- private async selectSession(session: SessionInfo) {
- this.selectedSession = session;
- this.socket?.close();
- this.messages = normalizeMessages(await api.messages(session.id));
- this.socket = sessionEvents(session.id);
- this.socket.onmessage = (message) => this.applyEvent(JSON.parse(message.data));
- }
-
- private applyEvent(event: any) {
- if (event.type === "assistant.delta") {
- const lines = [...this.messages];
- const last = lines.at(-1);
- if (last?.role === "assistant") last.text += event.text;
- else lines.push({ role: "assistant", text: event.text });
- this.messages = lines;
- } else if (event.type === "tool.start") {
- this.messages = [...this.messages, { role: "tool", text: `▶ ${event.toolName}` }];
- } else if (event.type === "tool.end") {
- this.messages = [...this.messages, { role: "tool", text: `${event.isError ? "✖" : "✓"} ${event.toolName}` }];
- } else if (event.type === "session.error") {
- this.messages = [...this.messages, { role: "system", text: event.message }];
- }
- }
-
- private async send() {
- const text = this.draft.trim();
- if (!text || !this.selectedSession) return;
- this.draft = "";
- this.messages = [...this.messages, { role: "user", text }];
- try {
- await api.prompt(this.selectedSession.id, text);
- } catch (error) {
- this.error = String(error);
- }
- }
-
- private async closeSession() {
- if (!this.selectedSession) return;
- await api.close(this.selectedSession.id);
- this.selectedSession = undefined;
- this.socket?.close();
- this.messages = [];
- }
-
- render() {
- return html`
-
-
-
-
- ${this.error ? html`${this.error}
` : null}
- ${this.selectedSession ? html`
-
- ${this.messages.map((message) => html`
${message.role}${message.text} `)}
-
-
- ` : html`Select or start a session.
`}
-
-
- `;
- }
-
- static styles = css`
- :host { display: block; height: 100vh; color: #e6edf3; background: #0d1117; font: 14px system-ui, sans-serif; }
- .shell { display: grid; grid-template-columns: 340px 1fr; height: 100%; }
- aside { border-right: 1px solid #30363d; overflow: auto; }
- header { display: flex; align-items: center; justify-content: space-between; padding: 12px; border-bottom: 1px solid #30363d; }
- section { padding: 10px; border-bottom: 1px solid #21262d; }
- h2 { display: flex; justify-content: space-between; align-items: center; margin: 0 0 8px; color: #8b949e; font-size: 12px; text-transform: uppercase; }
- button { border: 1px solid #30363d; border-radius: 8px; background: #161b22; color: #e6edf3; padding: 7px 9px; cursor: pointer; }
- section > button { display: block; width: 100%; text-align: left; margin: 6px 0; }
- button.selected { border-color: #58a6ff; background: #0d2847; }
- button:disabled { opacity: .5; cursor: not-allowed; }
- small { display: block; color: #8b949e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
- main { display: flex; flex-direction: column; min-width: 0; }
- .chat { flex: 1; overflow: auto; padding: 16px; }
- .msg { margin: 0 0 14px; padding: 12px; border: 1px solid #30363d; border-radius: 10px; background: #161b22; }
- .msg.user { border-color: #2f81f7; }
- .msg.tool { color: #d29922; }
- .msg.system, .error { color: #ff7b72; }
- pre { margin: 6px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
- footer { display: grid; grid-template-columns: 1fr auto auto; gap: 8px; padding: 12px; border-top: 1px solid #30363d; }
- textarea { min-height: 54px; resize: vertical; border-radius: 8px; border: 1px solid #30363d; background: #0d1117; color: #e6edf3; padding: 8px; }
- .empty { margin: auto; color: #8b949e; }
- .error { padding: 10px 16px; border-bottom: 1px solid #30363d; }
- `;
-}
-
-function normalizeMessages(messages: any[]): ChatLine[] {
- return messages.map((message) => ({
- role: message.role === "assistant" ? "assistant" : message.role === "user" ? "user" : "system",
- text: typeof message.content === "string" ? message.content : JSON.stringify(message.content, null, 2),
- }));
-}
+import "./components/PiWebApp";
diff --git a/src/client/src/route.ts b/src/client/src/route.ts
new file mode 100644
index 0000000..12cdbb9
--- /dev/null
+++ b/src/client/src/route.ts
@@ -0,0 +1,27 @@
+export interface AppRoute {
+ projectId?: string;
+ workspaceId?: string;
+ sessionId?: string;
+}
+
+export function readRoute(): AppRoute {
+ const params = new URLSearchParams(window.location.search);
+ return {
+ projectId: params.get("project") ?? undefined,
+ workspaceId: params.get("workspace") ?? undefined,
+ sessionId: params.get("session") ?? undefined,
+ };
+}
+
+export function writeRoute(route: AppRoute): void {
+ const url = new URL(window.location.href);
+ url.searchParams.delete("project");
+ url.searchParams.delete("workspace");
+ url.searchParams.delete("session");
+ if (route.projectId) url.searchParams.set("project", route.projectId);
+ if (route.workspaceId) url.searchParams.set("workspace", route.workspaceId);
+ if (route.sessionId) url.searchParams.set("session", route.sessionId);
+ const next = `${url.pathname}${url.search}${url.hash}`;
+ const current = `${window.location.pathname}${window.location.search}${window.location.hash}`;
+ if (next !== current) window.history.pushState({}, "", url);
+}
diff --git a/src/client/src/sessionSocket.ts b/src/client/src/sessionSocket.ts
new file mode 100644
index 0000000..083310f
--- /dev/null
+++ b/src/client/src/sessionSocket.ts
@@ -0,0 +1,29 @@
+import { sessionEvents } from "./api";
+
+export type SessionUiEvent =
+ | { type: "assistant.delta"; text: string }
+ | { type: "tool.start"; toolName: string }
+ | { type: "tool.end"; toolName: string; isError: boolean }
+ | { type: "session.error"; message: string };
+
+export class SessionSocket {
+ private socket?: WebSocket;
+
+ connect(sessionId: string, onEvent: (event: SessionUiEvent) => void): void {
+ this.close();
+ this.socket = sessionEvents(sessionId);
+ this.socket.onmessage = (message) => {
+ const event = JSON.parse(message.data);
+ if (isSessionUiEvent(event)) onEvent(event);
+ };
+ }
+
+ close(): void {
+ this.socket?.close();
+ this.socket = undefined;
+ }
+}
+
+function isSessionUiEvent(event: any): event is SessionUiEvent {
+ return ["assistant.delta", "tool.start", "tool.end", "session.error"].includes(event?.type);
+}
diff --git a/vite.config.ts b/vite.config.ts
index f0ab98f..5dc767f 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -8,6 +8,7 @@ export default defineConfig({
},
server: {
port: 5173,
+ strictPort: true,
proxy: {
"/api": "http://localhost:3000",
},