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}
-