diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c6f6074..4249ebbb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,7 +109,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Run coverage check - run: pnpm run --filter frontend test:coverage --coverage.reporter=json-summary + run: pnpm run --filter frontend test:coverage timeout-minutes: 5 - name: Update Coverage Status diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 548a5a4b..13a2e6e9 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,6 +2,7 @@ "recommendations": [ "svelte.svelte-vscode", "dbaeumer.vscode-eslint", - "prettier.prettier-vscode" + "esbenp.prettier-vscode", + "vitest.explorer" ] } \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index fc0a1f0e..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Launch Edge", - "request": "launch", - "type": "msedge", - "url": "http://localhost:5173", - "webRoot": "${workspaceFolder}/frontend" - }, - { - "type": "node-terminal", - "name": "Run Type check", - "request": "launch", - "command": "pnpm run check:types", - "cwd": "${workspaceFolder}/frontend" - }, - { - "type": "node-terminal", - "name": "Run Test", - "request": "launch", - "command": "pnpm run test", - "cwd": "${workspaceFolder}/frontend" - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 4cc742f2..56e0e4f1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,18 +1,18 @@ { "[javascript]": { - "editor.defaultFormatter": "prettier.prettier-vscode", + "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.tabSize": 4 }, "[typescript]": { - "editor.defaultFormatter": "prettier.prettier-vscode", + "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.tabSize": 4 }, "[svelte]": { "editor.tabSize": 4, - "editor.defaultFormatter": "prettier.prettier-vscode" + "editor.defaultFormatter": "svelte.svelte-vscode" }, "[json]": { - "editor.defaultFormatter": "prettier.prettier-vscode", + "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.tabSize": 4 }, "typescript.preferences.preferTypeOnlyAutoImports": true, @@ -24,6 +24,12 @@ "vitest.nodeEnv": { // Enabling browser test on VSC vitest, // since it will not run browser tests by default - "npm_lifecycle_event": "coverage" - } + "npm_lifecycle_event": "coverage", + "IS_IDE": "vscode" + }, + "eslint.workingDirectories": [ + { + "directory": "./frontend/", + } + ] } \ No newline at end of file diff --git a/docs/rule/README.md b/docs/rule/README.md index f31082ff..c8e882ec 100644 --- a/docs/rule/README.md +++ b/docs/rule/README.md @@ -2,4 +2,5 @@ There are some rules for readability! Check out the following documents: - [TypeScript](./typescript.md) - [Svelte](./svelte.md) -- [Testing](./tests.md) \ No newline at end of file +- [Testing](./tests.md) +- [Regional rules](./regional.md) \ No newline at end of file diff --git a/docs/rule/tests.md b/docs/rule/tests.md index e81657c8..239d8597 100644 --- a/docs/rule/tests.md +++ b/docs/rule/tests.md @@ -9,6 +9,20 @@ Test should cover: - unexpected states - TypeScript's type check, via Vitest's type assertion features. See [more](https://vitest.dev/guide/testing-types) and [more](https://github.com/mmkal/expect-type) +### Guidelines: + +When using mocks, prefer `vi.mocked()` over direct casting to `any` for better type safety. +```typescript +import { vi } from 'vitest'; +const myMockedFunction = vi.fn(); + +// Don't(it makes error on lint. Also, loses type safety and autocomplete.) +(myMockedFunction as any).mockReturnValue(42); + +// Do: +vi.mocked(myMockedFunction).mockReturnValue(42); +``` + ## 1. General Logic Tests (Node.js) Use these for your `.svelte.ts` files or pure TypeScript utility files. These tests run in Node.js for maximum speed. Since Svelte 5 runes are "universal," they can be tested without a browser if they don't touch the DOM. diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 474b9aee..f932d8b3 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -6,7 +6,8 @@ - **JSDoc Documentation**: Use JSDoc for all exported functions and complex logic. - HTML Separation: Avoid HTML strings in JS files. Who does that with Svelte? -- Use DaisyUI: We have libraries, use them! +- Use DaisyUI: We have libraries, use them! Don't use Tailwind CSS's classes if DaisyUI has the same class. + - Use color theme, not Tailwind CSS's color classes. ### Event Handling @@ -91,6 +92,7 @@ Worker logic should be tested via unit tests in `test/workers/`. Note that real - Using `lucide-svelte` instead of `@lucide/svelte`. - Fix: Replace `lucide-svelte` with `@lucide/svelte`. - Using `@testing-library/svelte` instead of `vitest-browser-svelte`. + - This makes tests run in not browser, but in Node.js. It might not work as intended, since some are not the same as in the browser. - Fix: Replace `@testing-library/svelte` with `vitest-browser-svelte`. ## Documentation of Other Libraries diff --git a/frontend/common/logger/LogBridge.ts b/frontend/common/logger/LogBridge.ts index aae34c09..ccb2adf9 100644 --- a/frontend/common/logger/LogBridge.ts +++ b/frontend/common/logger/LogBridge.ts @@ -1,3 +1,5 @@ +// It's logger, so we should use console +/* eslint-disable no-console */ import type { StandardLogEntry, LogLevel, StructuredLogEntry } from "./Logger"; import type { StructuredLogLevel } from "./LogType"; diff --git a/frontend/common/logger/LogType.ts b/frontend/common/logger/LogType.ts index a5c6df16..39f2ac64 100644 --- a/frontend/common/logger/LogType.ts +++ b/frontend/common/logger/LogType.ts @@ -8,6 +8,8 @@ type StructuredLogUI = { location: string; /** Modal identifier (e.g., "LLMSettings") */ modalName: string; + /** Optional character context */ + characterId?: string; }; /** * Modal dialog closed. @@ -112,6 +114,13 @@ type StructuredLogCharacter = { /** Error message if failed */ errorMessage?: string; }; + /** + * Character settings autosaved. + */ + "character.autosave": { + /** Character ID */ + characterId: string; + }; }; type StructuredLogLLM = { // ─── LLM Events ───────────────────────────────────────────── diff --git a/frontend/common/logger/Logger.ts b/frontend/common/logger/Logger.ts index 55749db6..36583f21 100644 --- a/frontend/common/logger/Logger.ts +++ b/frontend/common/logger/Logger.ts @@ -229,6 +229,16 @@ export class Logger { this.listeners.delete(listener); } + /** + * Reset the logger to its initial state. + * Useful for clearing all listeners and resetting the log level in tests. + */ + static reset(): void { + this.listeners.clear(); + currentLogLevel = getStoredLogLevel(); + initializeAdze(currentLogLevel); + } + /** * Remove all listeners. */ diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 6acd9a82..8f23ea79 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -5,21 +5,27 @@ import globals from "globals"; import { defineConfig } from "eslint/config"; import { dirname } from "path"; import { fileURLToPath } from "url"; +import phosphorSvelte from "eslint-plugin-phosphor-svelte"; const __dirname = dirname(fileURLToPath(import.meta.url)); const tsConfig = !process.env.SKIP_TYPE_LINT ? ts.configs.recommendedTypeChecked : ts.configs.recommended; + +const isCI = process.env.CI ? true : false; export default defineConfig([ + { + ignores: ["dist/", "node_modules/", "*.config.*"], + }, js.configs.recommended, ...tsConfig, ...svelte.configs["flat/recommended"], + phosphorSvelte.configs.recommended, { languageOptions: { globals: { ...globals.browser, - ...globals.node, }, parserOptions: { projectService: true, @@ -49,9 +55,9 @@ export default defineConfig([ "@typescript-eslint/require-await": "off", // Disable unbound-method - false positives with Svelte stores "@typescript-eslint/unbound-method": "off", + "no-debugger": isCI ? "error" : "warn", + // Use dedicated logger. Console is unrecommended since it's not pretty + "no-console": "warn", }, }, - { - ignores: ["dist/", "node_modules/", "*.config.*"], - }, ]); diff --git a/frontend/package.json b/frontend/package.json index 7d725d26..b42616dc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,7 +17,7 @@ "test:coverage": "vitest run test --coverage", "check": "pnpm run format && pnpm run lint:fix && concurrently --group \"pnpm run test\" \"pnpm run test:browser\"", "check:types": "concurrently --group \"svelte-check --tsconfig tsconfig.app.json --fail-on-warnings\" \"svelte-check --tsconfig tsconfig.worker.json --fail-on-warnings\" \"svelte-check --tsconfig tsconfig.test.json\"", - "lint": "pnpm run check:types && eslint --cache --cache-location ./node_modules/.cache/eslint/ src/ test/ worker/", + "lint": "pnpm run check:types && eslint --cache --cache-location ./node_modules/.cache/eslint/ src/ test/ worker/ common/", "lint:fix": "pnpm run lint --fix", "format": "pnpm run format:check --write --list-different", "format:check": "prettier --cache --cache-location ./node_modules/.cache/prettier/ src/ test/ worker/" @@ -29,18 +29,19 @@ "node": ">20" }, "dependencies": { - "@arisutalk/character-spec": "0.0.16", + "@arisutalk/character-spec": "0.0.17", "@humanspeak/svelte-markdown": "^0.8.13", "@langchain/anthropic": "^1.3.3", "@langchain/core": "^1.1.4", "@langchain/google-genai": "^2.1.0", "@langchain/openai": "^1.2.0", - "@lucide/svelte": "^0.559.0", "adze": "^2.2.5", "cbor-x": "^1.6.0", "comlink": "^4.4.2", "define-function": "^2.0.0", "dexie": "^4.2.1", + "lodash-es": "^4.17.22", + "phosphor-svelte": "^3.0.1", "svelte": "^5.39.3", "zod": "^4.1.12" }, @@ -48,6 +49,7 @@ "@eslint/js": "^9.39.1", "@sveltejs/vite-plugin-svelte": "^6.2.1", "@tailwindcss/postcss": "^4.1.18", + "@types/lodash-es": "^4.17.12", "@vitest/browser": "^4.0.7", "@vitest/browser-playwright": "^4.0.7", "@vitest/coverage-v8": "^4.0.7", @@ -56,20 +58,20 @@ "concurrently": "^9.2.1", "daisyui": "^5.5.13", "eslint": "^9.39.1", + "eslint-plugin-phosphor-svelte": "^0.0.2", "eslint-plugin-svelte": "^3.13.1", "fake-indexeddb": "^6.2.5", "globals": "^16.5.0", "happy-dom": "^20.0.10", "playwright": "^1.57.0", "postcss": "^8.5.6", - "prettier": "^3.7.4", - "prettier-plugin-svelte": "^3.4.0", + "prettier": "^3.8.0", + "prettier-plugin-svelte": "^3.4.1", "svelte-check": "^4.3.4", "tailwindcss": "^4.1.18", "typescript": "^5.9.3", "typescript-eslint": "^8.49.0", "vite": "^7.1.2", - "vite-tsconfig-paths": "^5.1.4", "vitest": "^4.0.7", "vitest-browser-svelte": "^2.0.1" } diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index eabd46e3..dd7d934f 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -4,11 +4,14 @@ --> @@ -43,28 +58,39 @@ ArisuTalk -{#if isLoading} -
- Loading... -
-{:else if CurrentComponent} - -{/if} - -{#if uiState.settingsModalOpen} - {#await import("@/components/SettingsModal.svelte")} + + {#if isLoading}
- Loading settings... -
- {:then { default: Component }} - - {:catch error} -
- Failed to load settings modal: {String(error)} + Loading...
- {/await} -{/if} + {:else if CurrentComponent} + + {/if} + + {#if uiState.settingsModalOpen} + {#await import("@/components/SettingsModal.svelte")} +
+ +
+ {:then { default: Component }} + + {:catch error} +
+
+

Error Loading Settings

+

{String(error)}

+ +
+
+ {/await} + {/if} - + +
diff --git a/frontend/src/components/ChatArea.svelte b/frontend/src/components/ChatArea.svelte index 8138b72d..5a8c3dc7 100644 --- a/frontend/src/components/ChatArea.svelte +++ b/frontend/src/components/ChatArea.svelte @@ -4,12 +4,16 @@ -->
-
-

{activeChat?.name || "Chat"}

+
+

{activeChat?.name || "Chat"}

+ {#if currentCharacter} + + {/if}
@@ -135,7 +164,7 @@ > {#if editingMessageId === msg.id} @@ -143,7 +172,7 @@ {/if}
- + {new Date(msg.timestamp || Date.now()).toLocaleTimeString()} -
+
{/if}
-