diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md
new file mode 100644
index 00000000..72b75fad
--- /dev/null
+++ b/.github/instructions/testing.instructions.md
@@ -0,0 +1,4 @@
+---
+applyTo: '**/*.feature,features/**'
+---
+Read docs/Testing.md for commands you can use. Don't guess shell commands!
diff --git a/build-resources/menubar@2x.png b/build-resources/tidgiMiniWindow@2x.png
similarity index 100%
rename from build-resources/menubar@2x.png
rename to build-resources/tidgiMiniWindow@2x.png
diff --git a/build-resources/menubarTemplate@2x.png b/build-resources/tidgiMiniWindowTemplate@2x.png
similarity index 100%
rename from build-resources/menubarTemplate@2x.png
rename to build-resources/tidgiMiniWindowTemplate@2x.png
diff --git a/docs/Testing.md b/docs/Testing.md
index d47229c4..30858408 100644
--- a/docs/Testing.md
+++ b/docs/Testing.md
@@ -11,14 +11,14 @@ pnpm test
# Run unit tests only
pnpm test:unit
-# Run E2E tests (requires prepare packaged app, but only when you modify code in ./src)
+# Run E2E tests (requires prepare packaged app, but only when you modify code in ./src) Don't need to run this if you only modify .feature file or step definition ts files.
pnpm run test:prepare-e2e
# (When only modify tests in ./features folder, and you have packaged app before, only need to run this.)
pnpm test:e2e
# Or run a specific e2e test by using same `@xxx` as in the `.feature` file.
pnpm test:e2e --tags="@smoke"
-# Or run a single e2e
-pnpm test:e2e --name "Wiki-search tool usage"
+# Or run a single e2e test by `--name`
+pnpm test:e2e --name "Wiki-search tool usage" # Not `-- --name` , and not `name`, is is just `--name` and have "" around the value, not omitting `--name`
# Run with coverage
pnpm test:unit -- --coverage
@@ -232,6 +232,14 @@ When('(Dont do this) I click on a specific button and wait for 2 seconds.', asyn
## Testing Library Best Practices
+**Important Testing Rules:**
+
+- **Do NOT simplify tests** - write comprehensive, professional unit tests
+- **Can add test-ids** when accessibility queries aren't practical
+- **Do NOT be lazy** - fix ALL tests until `pnpm test:unit` passes completely
+- **Do NOT summarize** until ALL unit tests pass
+- **Focus on professional, fix all seemly complex unit tests** before moving to E2E
+
### Query Priority (use in this order)
1. Accessible queries - `getByRole`, `getByLabelText`, `getByPlaceholderText`
@@ -323,6 +331,7 @@ This warning occurs when React components perform asynchronous state updates dur
- Components with `useEffect` that fetch data on mount
- Async API calls that update component state
- Timers or intervals that trigger state changes
+- **RxJS Observable subscriptions** that trigger state updates
**Solution**: Wait for async operations to complete using helper functions:
@@ -342,12 +351,62 @@ it('should test feature', async () => {
await renderAsyncComponent();
// Now safe to interact without warnings
});
+
+// For tests that trigger state updates, wait for UI to stabilize
+it('should update when data changes', async () => {
+ render();
+
+ // Trigger update
+ someObservable.next(newData);
+
+ // Wait for UI to reflect the change
+ await waitFor(() => {
+ expect(screen.getByText('Updated Content')).toBeInTheDocument();
+ });
+});
```
Avoid explicitly using `act()` - React Testing Library handles most cases automatically when using proper async patterns.
+**Critical**: To avoid act warnings with RxJS Observables:
+
+1. **Never call `.next()` on BehaviorSubject during test execution** - Set all data before rendering
+2. **Don't trigger Observable updates via mocked APIs** - Test the component's configuration, not the full update cycle
+3. **For loading state tests** - Unmount immediately after assertion to prevent subsequent updates
+4. **Follow the Main component test pattern** - Create Observables at file scope, never update them in tests
+
+**Example of correct Observable testing:**
+
+```typescript
+// ❌ Wrong: Updating Observable during test
+it('should update when data changes', async () => {
+ render();
+ preferencesSubject.next({ setting: false }); // This causes act warnings!
+ await waitFor(...);
+});
+
+// ✅ Correct: Observable created at file scope, never updated
+const preferencesSubject = new BehaviorSubject({ setting: true });
+
+describe('Component', () => {
+ it('should render correctly', async () => {
+ const { unmount } = render();
+ expect(screen.getByText('Content')).toBeInTheDocument();
+ // Optional: unmount() if testing transient state
+ });
+});
+```
+
### E2E test open production app
See User profile section above, we need to set `NODE_ENV` as `test` to open with correct profile.
This is done by using `EnvironmentPlugin` in [webpack.plugins.js](../webpack.plugins.js). Note that EsbuildPlugin's `define` doesn't work, it won't set env properly.
+
+### E2E test hang, and refused to exit until ctrl+C
+
+Check `features/stepDefinitions/application.ts` to see if `After` step includes all clean up steps, like closing all windows instances before closing the app, and stop all utility servers.
+
+### Global shortcut not working
+
+See `src/helpers/testKeyboardShortcuts.ts`
diff --git a/docs/Translate.md b/docs/Translate.md
index c31f277c..5b4efce3 100644
--- a/docs/Translate.md
+++ b/docs/Translate.md
@@ -13,7 +13,7 @@ Add your language, make it looks like:
"ja": "日本語",
"ru": "русский",
"vi": "Tiếng Việt",
- "zh-Hans": "汉字"
+ "zh-Hans": "汉语"
}
```
diff --git a/features/defaultWiki.feature b/features/defaultWiki.feature
index d6fea780..33d5faca 100644
--- a/features/defaultWiki.feature
+++ b/features/defaultWiki.feature
@@ -3,14 +3,21 @@ Feature: TidGi Default Wiki
I want app auto create a default wiki workspace for me
So that I can start using wiki immediately
- @wiki
- Scenario: Application has default wiki workspace
+ Background:
# Note: tests expect the test wiki parent folder to exist. Run the preparation step before E2E:
# cross-env NODE_ENV=test pnpm dlx tsx scripts/developmentMkdir.ts
- Given I cleanup test wiki
+ Given I cleanup test wiki so it could create a new one on start
When I launch the TidGi application
And I wait for the page to load completely
+
+ @wiki
+ Scenario: Application has default wiki workspace
Then I should see "page body and wiki workspace" elements with selectors:
| body |
| div[data-testid^='workspace-']:has-text('wiki') |
And the window title should contain "太记"
+
+ @wiki @browser-view
+ Scenario: Default wiki workspace displays TiddlyWiki content in browser view
+ And the browser view should be loaded and visible
+ And I should see "我的 TiddlyWiki" in the browser view content
diff --git a/features/preference.feature b/features/preference.feature
index de572d5e..d722a382 100644
--- a/features/preference.feature
+++ b/features/preference.feature
@@ -4,7 +4,6 @@ Feature: TidGi Preference
So that I can customize its behavior and improve my experience
Background:
- Given I clear test ai settings
Given I launch the TidGi application
And I wait for the page to load completely
And I should see a "page body" element with selector "body"
diff --git a/features/stepDefinitions/agent.ts b/features/stepDefinitions/agent.ts
index 2d082612..bc26d282 100644
--- a/features/stepDefinitions/agent.ts
+++ b/features/stepDefinitions/agent.ts
@@ -291,9 +291,11 @@ Given('I add test ai settings', function() {
fs.writeJsonSync(settingsPath, { ...existing, aiSettings: newAi } as ISettingFile, { spaces: 2 });
});
-Given('I clear test ai settings', function() {
+function clearAISettings() {
if (!fs.existsSync(settingsPath)) return;
const parsed = fs.readJsonSync(settingsPath) as ISettingFile;
const cleaned = omit(parsed, ['aiSettings']);
fs.writeJsonSync(settingsPath, cleaned, { spaces: 2 });
-});
+}
+
+export { clearAISettings };
diff --git a/features/stepDefinitions/application.ts b/features/stepDefinitions/application.ts
index 9b6d6eef..dbba9f51 100644
--- a/features/stepDefinitions/application.ts
+++ b/features/stepDefinitions/application.ts
@@ -3,10 +3,30 @@ import fs from 'fs-extra';
import path from 'path';
import { _electron as electron } from 'playwright';
import type { ElectronApplication, Page } from 'playwright';
-import { isMainWindowPage, PageType } from '../../src/constants/pageTypes';
+import { windowDimension, WindowNames } from '../../src/services/windows/WindowProperties';
import { MockOpenAIServer } from '../supports/mockOpenAI';
import { logsDirectory, makeSlugPath, screenshotsDirectory } from '../supports/paths';
import { getPackedAppPath } from '../supports/paths';
+import { clearAISettings } from './agent';
+import { clearTidgiMiniWindowSettings } from './tidgiMiniWindow';
+
+// Helper function to check if window type is valid and return the corresponding WindowNames
+export function checkWindowName(windowType: string): WindowNames {
+ // Exact match - windowType must be a valid WindowNames enum key
+ if (windowType in WindowNames) {
+ return (WindowNames as Record)[windowType];
+ }
+ throw new Error(`Window type "${windowType}" is not a valid WindowNames. Check the WindowNames enum in WindowProperties.ts. Available: ${Object.keys(WindowNames).join(', ')}`);
+}
+
+// Helper function to get window dimensions and ensure they are valid
+export function checkWindowDimension(windowName: WindowNames): { width: number; height: number } {
+ const targetDimensions = windowDimension[windowName];
+ if (!targetDimensions.width || !targetDimensions.height) {
+ throw new Error(`Window "${windowName}" does not have valid dimensions defined in windowDimension`);
+ }
+ return targetDimensions as { width: number; height: number };
+}
export class ApplicationWorld {
app: ElectronApplication | undefined;
@@ -14,43 +34,141 @@ export class ApplicationWorld {
currentWindow: Page | undefined; // New state-managed current window
mockOpenAIServer: MockOpenAIServer | undefined;
+ // Helper method to check if window is visible
+ async isWindowVisible(page: Page): Promise {
+ if (!this.app) return false;
+ try {
+ const browserWindow = await this.app.browserWindow(page);
+ return await browserWindow.evaluate((win: Electron.BrowserWindow) => win.isVisible());
+ } catch {
+ return false;
+ }
+ }
+
+ // Helper method to wait for window with retry logic
+ async waitForWindowCondition(
+ windowType: string,
+ condition: (window: Page | undefined, isVisible: boolean) => boolean,
+ maxAttempts: number = 3,
+ retryInterval: number = 250,
+ ): Promise {
+ if (!this.app) return false;
+
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
+ const targetWindow = await this.findWindowByType(windowType);
+ const visible = targetWindow ? await this.isWindowVisible(targetWindow) : false;
+
+ if (condition(targetWindow, visible)) {
+ return true;
+ }
+
+ // Wait before retrying
+ await new Promise(resolve => setTimeout(resolve, retryInterval));
+ }
+ return false;
+ }
+
+ // Helper method to find window by type - strict WindowNames matching
+ async findWindowByType(windowType: string): Promise {
+ if (!this.app) return undefined;
+
+ // Validate window type first
+ const windowName = checkWindowName(windowType);
+
+ const pages = this.app.windows();
+
+ if (windowName === WindowNames.main) {
+ // Main window is the first/primary window, typically showing guide, agent, help, or wiki pages
+ // It's the window that opens on app launch
+ const allWindows = pages.filter(page => !page.isClosed());
+ if (allWindows.length > 0) {
+ // Return the first window (main window is always the first one created)
+ return allWindows[0];
+ }
+ return undefined;
+ } else if (windowName === WindowNames.tidgiMiniWindow) {
+ // Special handling for tidgi mini window
+ // First try to find by Electron window dimensions (more reliable than title)
+ const windowDimensions = checkWindowDimension(windowName);
+ try {
+ const electronWindowInfo = await this.app.evaluate(
+ async ({ BrowserWindow }, size: { width: number; height: number }) => {
+ const allWindows = BrowserWindow.getAllWindows();
+ const tidgiMiniWindow = allWindows.find(win => {
+ const bounds = win.getBounds();
+ return bounds.width === size.width && bounds.height === size.height;
+ });
+ return tidgiMiniWindow ? { id: tidgiMiniWindow.id } : null;
+ },
+ windowDimensions,
+ );
+
+ if (electronWindowInfo) {
+ // Found by dimensions, now match with Playwright page
+ const allWindows = pages.filter(page => !page.isClosed());
+ for (const page of allWindows) {
+ try {
+ // Try to match by checking if this page belongs to the found electron window
+ // For now, use title as fallback verification
+ const title = await page.title();
+ if (title.includes('太记小窗') || title.includes('TidGi Mini Window') || title.includes('TidGiMiniWindow')) {
+ return page;
+ }
+ } catch {
+ continue;
+ }
+ }
+ }
+ } catch {
+ // If Electron API fails, fallback to title matching
+ }
+
+ // Fallback: Match by window title
+ const allWindows = pages.filter(page => !page.isClosed());
+ for (const page of allWindows) {
+ try {
+ const title = await page.title();
+ if (title.includes('太记小窗') || title.includes('TidGi Mini Window') || title.includes('TidGiMiniWindow')) {
+ return page;
+ }
+ } catch {
+ // Page might be closed or not ready, continue to next
+ continue;
+ }
+ }
+ return undefined;
+ } else {
+ // For regular windows (preferences, about, addWorkspace, etc.)
+ return pages.find(page => {
+ if (page.isClosed()) return false;
+ const url = page.url() || '';
+ // Match exact route paths: /#/windowType or ending with /windowType
+ return url.includes(`#/${windowType}`) || url.endsWith(`/${windowType}`);
+ });
+ }
+ }
+
async getWindow(windowType: string = 'main'): Promise {
if (!this.app) return undefined;
+ // Special case for 'current' window
+ if (windowType === 'current') {
+ return this.currentWindow;
+ }
+
+ // Use the findWindowByType method with retry logic
for (let attempt = 0; attempt < 3; attempt++) {
- const pages = this.app.windows();
-
- const extractFragment = (url: string) => {
- if (!url) return '';
- const afterHash = url.includes('#') ? url.split('#').slice(1).join('#') : '';
- // remove leading slashes or colons like '/preferences' or ':Index'
- return afterHash.replace(/^[:/]+/, '').split(/[/?#]/)[0] || '';
- };
-
- if (windowType === 'main') {
- const mainWindow = pages.find(page => {
- const pageType = extractFragment(page.url());
- // file:///C:/Users/linonetwo/Documents/repo-c/TidGi-Desktop/out/TidGi-win32-x64/resources/app.asar/.webpack/renderer/main_window/index.html#/guide
- // file:///...#/guide or tidgi://.../#:Index based on different workspace
- return isMainWindowPage(pageType as PageType | undefined);
- });
- if (mainWindow) return mainWindow;
- } else if (windowType === 'current') {
- if (this.currentWindow) return this.currentWindow;
- } else {
- // match windows more flexibly by checking the full URL and fragment for the windowType
- const specificWindow = pages.find(page => {
- const rawUrl = page.url() || '';
- const frag = extractFragment(rawUrl);
- // Case-insensitive full-url match first (handles variants like '#:Index' or custom schemes)
- if (rawUrl.toLowerCase().includes(windowType.toLowerCase())) return true;
- // Fallback to fragment inclusion
- return frag.toLowerCase().includes(windowType.toLowerCase());
- });
- if (specificWindow) return specificWindow;
+ try {
+ const window = await this.findWindowByType(windowType);
+ if (window) return window;
+ } catch (error) {
+ // If it's an invalid window type error, throw immediately
+ if (error instanceof Error && error.message.includes('is not a valid WindowNames')) {
+ throw error;
+ }
}
- // If window not found, wait 1 second and retry (except for the last attempt)
+ // If window not found, wait and retry (except for the last attempt)
if (attempt < 2) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
@@ -64,7 +182,7 @@ setWorldConstructor(ApplicationWorld);
// setDefaultTimeout(50000);
-Before(function(this: ApplicationWorld) {
+Before(function(this: ApplicationWorld, { pickle }) {
// Create necessary directories under userData-test/logs to match appPaths in dev/test
if (!fs.existsSync(logsDirectory)) {
fs.mkdirSync(logsDirectory, { recursive: true });
@@ -74,11 +192,29 @@ Before(function(this: ApplicationWorld) {
if (!fs.existsSync(screenshotsDirectory)) {
fs.mkdirSync(screenshotsDirectory, { recursive: true });
}
+
+ if (pickle.tags.some((tag) => tag.name === '@setup')) {
+ clearAISettings();
+ }
+ if (pickle.tags.some((tag) => tag.name === '@tidgiminiwindow')) {
+ clearTidgiMiniWindowSettings();
+ }
});
-After(async function(this: ApplicationWorld) {
+After(async function(this: ApplicationWorld, { pickle }) {
if (this.app) {
try {
+ // Close all windows including tidgi mini window before closing the app, otherwise it might hang, and refused to exit until ctrl+C
+ const allWindows = this.app.windows();
+ for (const window of allWindows) {
+ try {
+ if (!window.isClosed()) {
+ await window.close();
+ }
+ } catch (error) {
+ console.error('Error closing window:', error);
+ }
+ }
await this.app.close();
} catch (error) {
console.error('Error during cleanup:', error);
@@ -87,6 +223,12 @@ After(async function(this: ApplicationWorld) {
this.mainWindow = undefined;
this.currentWindow = undefined;
}
+ if (pickle.tags.some((tag) => tag.name === '@tidgiminiwindow')) {
+ clearTidgiMiniWindowSettings();
+ }
+ if (pickle.tags.some((tag) => tag.name === '@setup')) {
+ clearAISettings();
+ }
});
AfterStep(async function(this: ApplicationWorld, { pickle, pickleStep, result }) {
@@ -125,7 +267,7 @@ AfterStep(async function(this: ApplicationWorld, { pickle, pickleStep, result })
/**
* Typical steps like:
* - I add test ai settings
- * - I cleanup test wiki
+ * - I cleanup test wiki so it could create a new one on start
* - I clear test ai settings
*/
if (!pageToUse || pageToUse.isClosed()) {
@@ -208,7 +350,7 @@ When('I launch the TidGi application', async function(this: ApplicationWorld) {
this.currentWindow = this.mainWindow;
} catch (error) {
throw new Error(
- `Failed to launch TidGi application: ${error as Error}. You should run \`pnpm run package\` before running the tests to ensure the app is built, and build with binaries like "dugite" and "tiddlywiki", see scripts/afterPack.js for more details.`,
+ `Failed to launch TidGi application: ${error as Error}. You should run \`pnpm run test:prepare-e2e\` before running the tests to ensure the app is built, and build with binaries like "dugite" and "tiddlywiki", see scripts/afterPack.js for more details.`,
);
}
});
diff --git a/features/stepDefinitions/browserView.ts b/features/stepDefinitions/browserView.ts
new file mode 100644
index 00000000..8393c7f1
--- /dev/null
+++ b/features/stepDefinitions/browserView.ts
@@ -0,0 +1,103 @@
+import { Then } from '@cucumber/cucumber';
+import { getDOMContent, getTextContent, isLoaded } from '../supports/webContentsViewHelper';
+import type { ApplicationWorld } from './application';
+
+// Constants for retry logic
+const MAX_ATTEMPTS = 3;
+const RETRY_INTERVAL_MS = 500;
+
+Then('I should see {string} in the browser view content', async function(this: ApplicationWorld, expectedText: string) {
+ if (!this.app) {
+ throw new Error('Application not launched');
+ }
+
+ if (!this.currentWindow) {
+ throw new Error('No current window available');
+ }
+
+ // Retry logic to check for expected text in content
+ const maxAttempts = MAX_ATTEMPTS;
+ const retryInterval = RETRY_INTERVAL_MS;
+
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
+ const content = await getTextContent(this.app);
+ if (content && content.includes(expectedText)) {
+ return; // Success, exit early
+ }
+
+ // Wait before retrying (except for the last attempt)
+ if (attempt < maxAttempts - 1) {
+ await new Promise(resolve => setTimeout(resolve, retryInterval));
+ }
+ }
+
+ // Final attempt to get content for error message
+ const finalContent = await getTextContent(this.app);
+ throw new Error(
+ `Expected text "${expectedText}" not found in browser view content after ${MAX_ATTEMPTS} attempts. Actual content: ${
+ finalContent ? finalContent.substring(0, 200) + '...' : 'null'
+ }`,
+ );
+});
+
+Then('I should see {string} in the browser view DOM', async function(this: ApplicationWorld, expectedText: string) {
+ if (!this.app) {
+ throw new Error('Application not launched');
+ }
+
+ if (!this.currentWindow) {
+ throw new Error('No current window available');
+ }
+
+ // Retry logic to check for expected text in DOM
+ const maxAttempts = MAX_ATTEMPTS;
+ const retryInterval = RETRY_INTERVAL_MS;
+
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
+ const domContent = await getDOMContent(this.app);
+ if (domContent && domContent.includes(expectedText)) {
+ return; // Success, exit early
+ }
+
+ // Wait before retrying (except for the last attempt)
+ if (attempt < maxAttempts - 1) {
+ await new Promise(resolve => setTimeout(resolve, retryInterval));
+ }
+ }
+
+ // Final attempt to get DOM content for error message
+ const finalDomContent = await getDOMContent(this.app);
+ throw new Error(
+ `Expected text "${expectedText}" not found in browser view DOM after ${MAX_ATTEMPTS} attempts. Actual DOM: ${
+ finalDomContent ? finalDomContent.substring(0, 200) + '...' : 'null'
+ }`,
+ );
+});
+
+Then('the browser view should be loaded and visible', async function(this: ApplicationWorld) {
+ if (!this.app) {
+ throw new Error('Application not launched');
+ }
+
+ if (!this.currentWindow) {
+ throw new Error('No current window available');
+ }
+
+ // Retry logic to check if browser view is loaded
+ const maxAttempts = MAX_ATTEMPTS;
+ const retryInterval = RETRY_INTERVAL_MS;
+
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
+ const isLoadedResult = await isLoaded(this.app);
+ if (isLoadedResult) {
+ return; // Success, exit early
+ }
+
+ // Wait before retrying (except for the last attempt)
+ if (attempt < maxAttempts - 1) {
+ await new Promise(resolve => setTimeout(resolve, retryInterval));
+ }
+ }
+
+ throw new Error(`Browser view is not loaded or visible after ${MAX_ATTEMPTS} attempts`);
+});
diff --git a/features/stepDefinitions/tidgiMiniWindow.ts b/features/stepDefinitions/tidgiMiniWindow.ts
new file mode 100644
index 00000000..6899bc32
--- /dev/null
+++ b/features/stepDefinitions/tidgiMiniWindow.ts
@@ -0,0 +1,74 @@
+import { Given } from '@cucumber/cucumber';
+import fs from 'fs-extra';
+import { omit } from 'lodash';
+import path from 'path';
+import type { ISettingFile } from '../../src/services/database/interface';
+import { settingsPath } from '../supports/paths';
+
+Given('I configure tidgi mini window with shortcut', async function() {
+ let existing = {} as ISettingFile;
+ if (await fs.pathExists(settingsPath)) {
+ existing = await fs.readJson(settingsPath) as ISettingFile;
+ } else {
+ // ensure settings directory exists so writeJsonSync won't fail
+ await fs.ensureDir(path.dirname(settingsPath));
+ }
+
+ // Convert CommandOrControl to platform-specific format
+ const isWindows = process.platform === 'win32';
+ const isLinux = process.platform === 'linux';
+ let shortcut = 'CommandOrControl+Shift+M';
+ if (isWindows || isLinux) {
+ shortcut = 'Ctrl+Shift+M';
+ } else {
+ shortcut = 'Cmd+Shift+M';
+ }
+
+ const updatedPreferences = {
+ ...existing.preferences,
+ tidgiMiniWindow: true,
+ keyboardShortcuts: {
+ ...(existing.preferences?.keyboardShortcuts || {}),
+ 'Window.toggleTidgiMiniWindow': shortcut,
+ },
+ };
+ const finalSettings = { ...existing, preferences: updatedPreferences } as ISettingFile;
+ await fs.writeJson(settingsPath, finalSettings, { spaces: 2 });
+});
+
+// Cleanup function to be called after tidgi mini window tests (after app closes)
+function clearTidgiMiniWindowSettings() {
+ if (!fs.existsSync(settingsPath)) return;
+ const parsed = fs.readJsonSync(settingsPath) as ISettingFile;
+ // Remove tidgi mini window-related preferences to avoid affecting other tests
+ const cleanedPreferences = omit(parsed.preferences || {}, [
+ 'tidgiMiniWindow',
+ 'tidgiMiniWindowSyncWorkspaceWithMainWindow',
+ 'tidgiMiniWindowFixedWorkspaceId',
+ 'tidgiMiniWindowAlwaysOnTop',
+ 'tidgiMiniWindowShowSidebar',
+ 'tidgiMiniWindowShowTitleBar',
+ ]);
+ // Also clean up the tidgi mini window shortcut from keyboardShortcuts
+ if (cleanedPreferences.keyboardShortcuts) {
+ cleanedPreferences.keyboardShortcuts = omit(cleanedPreferences.keyboardShortcuts, ['Window.toggleTidgiMiniWindow']);
+ }
+
+ // Reset active workspace to first wiki workspace to avoid agent workspace being active
+ const workspaces = parsed.workspaces || {};
+ const workspaceEntries = Object.entries(workspaces);
+ // Set all workspaces to inactive first
+ for (const [, workspace] of workspaceEntries) {
+ workspace.active = false;
+ }
+ // Find first non-page-type workspace (wiki) and activate it
+ const firstWikiWorkspace = workspaceEntries.find(([, workspace]) => !workspace.pageType);
+ if (firstWikiWorkspace) {
+ firstWikiWorkspace[1].active = true;
+ }
+
+ const cleaned = { ...parsed, preferences: cleanedPreferences, workspaces };
+ fs.writeJsonSync(settingsPath, cleaned, { spaces: 2 });
+}
+
+export { clearTidgiMiniWindowSettings };
diff --git a/features/stepDefinitions/ui.ts b/features/stepDefinitions/ui.ts
index af548c36..94e74b55 100644
--- a/features/stepDefinitions/ui.ts
+++ b/features/stepDefinitions/ui.ts
@@ -6,12 +6,13 @@ When('I wait for {float} seconds', async function(this: ApplicationWorld, second
});
When('I wait for the page to load completely', async function(this: ApplicationWorld) {
- const currentWindow = this.currentWindow || this.mainWindow;
+ const currentWindow = this.currentWindow;
await currentWindow?.waitForLoadState('networkidle', { timeout: 30000 });
});
Then('I should see a(n) {string} element with selector {string}', async function(this: ApplicationWorld, elementComment: string, selector: string) {
- const currentWindow = this.currentWindow || this.mainWindow;
+ const currentWindow = this.currentWindow;
+
try {
await currentWindow?.waitForSelector(selector, { timeout: 10000 });
const isVisible = await currentWindow?.isVisible(selector);
@@ -24,7 +25,7 @@ Then('I should see a(n) {string} element with selector {string}', async function
});
Then('I should see {string} elements with selectors:', async function(this: ApplicationWorld, elementDescriptions: string, dataTable: DataTable) {
- const currentWindow = this.currentWindow || this.mainWindow;
+ const currentWindow = this.currentWindow;
if (!currentWindow) {
throw new Error('No current window is available');
}
@@ -57,7 +58,7 @@ Then('I should see {string} elements with selectors:', async function(this: Appl
});
Then('I should not see a(n) {string} element with selector {string}', async function(this: ApplicationWorld, elementComment: string, selector: string) {
- const currentWindow = this.currentWindow || this.mainWindow;
+ const currentWindow = this.currentWindow;
if (!currentWindow) {
throw new Error('No current window is available');
}
@@ -67,7 +68,18 @@ Then('I should not see a(n) {string} element with selector {string}', async func
if (count > 0) {
const isVisible = await element.isVisible();
if (isVisible) {
- throw new Error(`Element "${elementComment}" with selector "${selector}" should not be visible but was found`);
+ // Get parent element HTML for debugging
+ let parentHtml = '';
+ try {
+ const parent = element.locator('xpath=..');
+ parentHtml = await parent.evaluate((node) => node.outerHTML);
+ } catch {
+ parentHtml = 'Failed to get parent HTML';
+ }
+ throw new Error(
+ `Element "${elementComment}" with selector "${selector}" should not be visible but was found\n` +
+ `Parent element HTML:\n${parentHtml}`,
+ );
}
}
// Element not found or not visible - this is expected
@@ -80,6 +92,48 @@ Then('I should not see a(n) {string} element with selector {string}', async func
}
});
+Then('I should not see {string} elements with selectors:', async function(this: ApplicationWorld, elementDescriptions: string, dataTable: DataTable) {
+ const currentWindow = this.currentWindow;
+ if (!currentWindow) {
+ throw new Error('No current window is available');
+ }
+
+ const descriptions = elementDescriptions.split(' and ').map(d => d.trim());
+ const rows = dataTable.raw();
+ const errors: string[] = [];
+
+ if (descriptions.length !== rows.length) {
+ throw new Error(`Mismatch: ${descriptions.length} element descriptions but ${rows.length} selectors provided`);
+ }
+
+ // Check all elements
+ for (let index = 0; index < rows.length; index++) {
+ const [selector] = rows[index];
+ const elementComment = descriptions[index];
+ try {
+ const element = currentWindow.locator(selector).first();
+ const count = await element.count();
+ if (count > 0) {
+ const isVisible = await element.isVisible();
+ if (isVisible) {
+ errors.push(`Element "${elementComment}" with selector "${selector}" should not be visible but was found`);
+ }
+ }
+ // Element not found or not visible - this is expected
+ } catch (error) {
+ // If the error is our custom error, rethrow it
+ if (error instanceof Error && error.message.includes('should not be visible')) {
+ errors.push(error.message);
+ }
+ // Otherwise, element not found is expected - continue
+ }
+ }
+
+ if (errors.length > 0) {
+ throw new Error(`Failed to verify elements are not visible:\n${errors.join('\n')}`);
+ }
+});
+
When('I click on a(n) {string} element with selector {string}', async function(this: ApplicationWorld, elementComment: string, selector: string) {
const targetWindow = await this.getWindow('current');
@@ -156,7 +210,7 @@ When('I right-click on a(n) {string} element with selector {string}', async func
});
When('I click all {string} elements matching selector {string}', async function(this: ApplicationWorld, elementComment: string, selector: string) {
- const win = this.currentWindow || this.mainWindow;
+ const win = this.currentWindow;
if (!win) throw new Error('No active window available to click elements');
const locator = win.locator(selector);
@@ -177,7 +231,7 @@ When('I click all {string} elements matching selector {string}', async function(
});
When('I type {string} in {string} element with selector {string}', async function(this: ApplicationWorld, text: string, elementComment: string, selector: string) {
- const currentWindow = this.currentWindow || this.mainWindow;
+ const currentWindow = this.currentWindow;
if (!currentWindow) {
throw new Error('No current window is available');
}
@@ -192,7 +246,7 @@ When('I type {string} in {string} element with selector {string}', async functio
});
When('I clear text in {string} element with selector {string}', async function(this: ApplicationWorld, elementComment: string, selector: string) {
- const currentWindow = this.currentWindow || this.mainWindow;
+ const currentWindow = this.currentWindow;
if (!currentWindow) {
throw new Error('No current window is available');
}
@@ -207,7 +261,7 @@ When('I clear text in {string} element with selector {string}', async function(t
});
When('the window title should contain {string}', async function(this: ApplicationWorld, expectedTitle: string) {
- const currentWindow = this.currentWindow || this.mainWindow;
+ const currentWindow = this.currentWindow;
if (!currentWindow) {
throw new Error('No current window is available');
}
@@ -258,3 +312,143 @@ When('I close {string} window', async function(this: ApplicationWorld, windowTyp
throw new Error(`Could not find ${windowType} window to close`);
}
});
+
+When('I press the key combination {string}', async function(this: ApplicationWorld, keyCombo: string) {
+ const currentWindow = this.currentWindow;
+ if (!currentWindow) {
+ throw new Error('No current window is available');
+ }
+
+ // Convert CommandOrControl to platform-specific key
+ let platformKeyCombo = keyCombo;
+ if (keyCombo.includes('CommandOrControl')) {
+ // Prefer explicit platform detection: use 'Meta' only on macOS (darwin),
+ // otherwise default to 'Control'. This avoids assuming non-Windows/Linux
+ // is always macOS.
+ if (process.platform === 'darwin') {
+ platformKeyCombo = keyCombo.replace('CommandOrControl', 'Meta');
+ } else {
+ platformKeyCombo = keyCombo.replace('CommandOrControl', 'Control');
+ }
+ }
+ // Use dispatchEvent to trigger document-level keydown events
+
+ // This ensures the event is properly captured by React components listening to document events
+ // The testKeyboardShortcutFallback in test environment expects key to match the format used in shortcuts
+ await currentWindow.evaluate((keyCombo) => {
+ const parts = keyCombo.split('+');
+ let mainKey = parts[parts.length - 1];
+ const modifiers = parts.slice(0, -1);
+
+ // For single letter keys, match the case sensitivity used by the shortcut system
+ // Shift+Key -> uppercase, otherwise lowercase
+ if (mainKey.length === 1) {
+ mainKey = modifiers.includes('Shift') ? mainKey.toUpperCase() : mainKey.toLowerCase();
+ }
+
+ const event = new KeyboardEvent('keydown', {
+ key: mainKey,
+ code: mainKey.length === 1 ? `Key${mainKey.toUpperCase()}` : mainKey,
+ ctrlKey: modifiers.includes('Control'),
+ metaKey: modifiers.includes('Meta'),
+ shiftKey: modifiers.includes('Shift'),
+ altKey: modifiers.includes('Alt'),
+ bubbles: true,
+ cancelable: true,
+ });
+
+ document.dispatchEvent(event);
+ }, platformKeyCombo);
+});
+
+When('I select {string} from MUI Select with test id {string}', async function(this: ApplicationWorld, optionValue: string, testId: string) {
+ const currentWindow = this.currentWindow;
+ if (!currentWindow) {
+ throw new Error('No current window is available');
+ }
+
+ try {
+ // Find the hidden input element with the test-id
+ const inputSelector = `input[data-testid="${testId}"]`;
+ await currentWindow.waitForSelector(inputSelector, { timeout: 10000 });
+
+ // Try to click using Playwright's click on the div with role="combobox"
+ // According to your HTML structure, the combobox is a sibling of the input
+ const clicked = await currentWindow.evaluate((testId) => {
+ const input = document.querySelector(`input[data-testid="${testId}"]`);
+ if (!input) return { success: false, error: 'Input not found' };
+ const parent = input.parentElement;
+ if (!parent) return { success: false, error: 'Parent not found' };
+
+ // Find all elements in parent
+ const combobox = parent.querySelector('[role="combobox"]');
+ if (!combobox) {
+ return {
+ success: false,
+ error: 'Combobox not found',
+ parentHTML: parent.outerHTML.substring(0, 500),
+ };
+ }
+
+ // Trigger both mousedown and click events
+ combobox.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
+ combobox.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
+ (combobox as HTMLElement).click();
+
+ return { success: true };
+ }, testId);
+
+ if (!clicked.success) {
+ throw new Error(`Failed to click: ${JSON.stringify(clicked)}`);
+ }
+
+ // Wait a bit for the menu to appear
+ await currentWindow.waitForTimeout(500);
+
+ // Wait for the menu to appear
+ await currentWindow.waitForSelector('[role="listbox"]', { timeout: 5000 });
+
+ // Try to click on the option with the specified value (data-value attribute)
+ // If not found, try to find by text content
+ const optionClicked = await currentWindow.evaluate((optionValue) => {
+ // First try: Find by data-value attribute
+ const optionByValue = document.querySelector(`[role="option"][data-value="${optionValue}"]`);
+ if (optionByValue) {
+ (optionByValue as HTMLElement).click();
+ return { success: true, method: 'data-value' };
+ }
+
+ // Second try: Find by text content (case-insensitive)
+ const allOptions = Array.from(document.querySelectorAll('[role="option"]'));
+ const optionByText = allOptions.find(option => {
+ const text = option.textContent?.trim().toLowerCase();
+ return text === optionValue.toLowerCase();
+ });
+
+ if (optionByText) {
+ (optionByText as HTMLElement).click();
+ return { success: true, method: 'text-content' };
+ }
+
+ // Return available options for debugging
+ return {
+ success: false,
+ availableOptions: allOptions.map(opt => ({
+ text: opt.textContent?.trim(),
+ value: opt.getAttribute('data-value'),
+ })),
+ };
+ }, optionValue);
+
+ if (!optionClicked.success) {
+ throw new Error(
+ `Could not find option "${optionValue}". Available options: ${JSON.stringify(optionClicked.availableOptions)}`,
+ );
+ }
+
+ // Wait for the menu to close
+ await currentWindow.waitForSelector('[role="listbox"]', { state: 'hidden', timeout: 5000 });
+ } catch (error) {
+ throw new Error(`Failed to select option "${optionValue}" from MUI Select with test id "${testId}": ${String(error)}`);
+ }
+});
diff --git a/features/stepDefinitions/wiki.ts b/features/stepDefinitions/wiki.ts
index b074b259..cfae5943 100644
--- a/features/stepDefinitions/wiki.ts
+++ b/features/stepDefinitions/wiki.ts
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
import type { IWorkspace } from '../../src/services/workspaces/interface';
import { settingsPath, wikiTestWikiPath } from '../supports/paths';
-When('I cleanup test wiki', async function() {
+When('I cleanup test wiki so it could create a new one on start', async function() {
if (fs.existsSync(wikiTestWikiPath)) fs.removeSync(wikiTestWikiPath);
type SettingsFile = { workspaces?: Record } & Record;
diff --git a/features/stepDefinitions/window.ts b/features/stepDefinitions/window.ts
new file mode 100644
index 00000000..c2689bf3
--- /dev/null
+++ b/features/stepDefinitions/window.ts
@@ -0,0 +1,207 @@
+import { When } from '@cucumber/cucumber';
+import type { ElectronApplication } from 'playwright';
+import type { ApplicationWorld } from './application';
+import { checkWindowDimension, checkWindowName } from './application';
+
+// Constants for retry logic
+const MAX_ATTEMPTS = 10;
+const RETRY_INTERVAL_MS = 1000;
+
+// Helper function to get browser view info from Electron window
+async function getBrowserViewInfo(
+ app: ElectronApplication,
+ dimensions: { width: number; height: number },
+): Promise<{ view?: { x: number; y: number; width: number; height: number }; windowContent?: { width: number; height: number }; hasView: boolean }> {
+ return app.evaluate(async ({ BrowserWindow }, dimensions: { width: number; height: number }) => {
+ const windows = BrowserWindow.getAllWindows();
+
+ // Find the target window by dimensions
+ const targetWindow = windows.find(win => {
+ const bounds = win.getBounds();
+ return bounds.width === dimensions.width && bounds.height === dimensions.height;
+ });
+
+ if (!targetWindow) {
+ return { hasView: false };
+ }
+
+ // Get all child views (WebContentsView instances) attached to this specific window
+ if (targetWindow.contentView && 'children' in targetWindow.contentView) {
+ const views = targetWindow.contentView.children || [];
+
+ for (const view of views) {
+ // Type guard to check if view is a WebContentsView
+ if (view && view.constructor.name === 'WebContentsView') {
+ const webContentsView = view as unknown as { getBounds: () => { x: number; y: number; width: number; height: number } };
+ const viewBounds = webContentsView.getBounds();
+ const windowContentBounds = targetWindow.getContentBounds();
+
+ return {
+ view: viewBounds,
+ windowContent: windowContentBounds,
+ hasView: true,
+ };
+ }
+ }
+ }
+
+ return { hasView: false };
+ }, dimensions);
+}
+
+When('I confirm the {string} window exists', async function(this: ApplicationWorld, windowType: string) {
+ if (!this.app) {
+ throw new Error('Application is not launched');
+ }
+
+ const success = await this.waitForWindowCondition(
+ windowType,
+ (window) => window !== undefined && !window.isClosed(),
+ MAX_ATTEMPTS,
+ RETRY_INTERVAL_MS,
+ );
+
+ if (!success) {
+ throw new Error(`${windowType} window was not found or is closed`);
+ }
+});
+
+When('I confirm the {string} window visible', async function(this: ApplicationWorld, windowType: string) {
+ if (!this.app) {
+ throw new Error('Application is not launched');
+ }
+
+ const success = await this.waitForWindowCondition(
+ windowType,
+ (window, isVisible) => window !== undefined && !window.isClosed() && isVisible,
+ MAX_ATTEMPTS,
+ RETRY_INTERVAL_MS,
+ );
+
+ if (!success) {
+ throw new Error(`${windowType} window was not visible after ${MAX_ATTEMPTS} attempts`);
+ }
+});
+
+When('I confirm the {string} window not visible', async function(this: ApplicationWorld, windowType: string) {
+ if (!this.app) {
+ throw new Error('Application is not launched');
+ }
+
+ const success = await this.waitForWindowCondition(
+ windowType,
+ (window, isVisible) => window !== undefined && !window.isClosed() && !isVisible,
+ MAX_ATTEMPTS,
+ RETRY_INTERVAL_MS,
+ );
+
+ if (!success) {
+ throw new Error(`${windowType} window was visible or not found after ${MAX_ATTEMPTS} attempts`);
+ }
+});
+
+When('I confirm the {string} window does not exist', async function(this: ApplicationWorld, windowType: string) {
+ if (!this.app) {
+ throw new Error('Application is not launched');
+ }
+
+ const success = await this.waitForWindowCondition(
+ windowType,
+ (window) => window === undefined,
+ MAX_ATTEMPTS,
+ RETRY_INTERVAL_MS,
+ );
+
+ if (!success) {
+ throw new Error(`${windowType} window still exists after ${MAX_ATTEMPTS} attempts`);
+ }
+});
+
+When('I confirm the {string} window browser view is positioned within visible window bounds', async function(this: ApplicationWorld, windowType: string) {
+ if (!this.app) {
+ throw new Error('Application is not available');
+ }
+
+ const targetWindow = await this.findWindowByType(windowType);
+ if (!targetWindow || targetWindow.isClosed()) {
+ throw new Error(`Window "${windowType}" is not available or has been closed`);
+ }
+
+ // Get the window dimensions to identify it - must match a defined WindowNames
+ const windowName = checkWindowName(windowType);
+ const windowDimensions = checkWindowDimension(windowName);
+
+ // Get browser view bounds for the specific window type
+ const viewInfo = await getBrowserViewInfo(this.app, windowDimensions);
+
+ if (!viewInfo.hasView || !viewInfo.view || !viewInfo.windowContent) {
+ throw new Error(`No browser view found in "${windowType}" window`);
+ }
+
+ // Check if browser view is within window content bounds
+ // View coordinates are relative to the window, so we check if they're within the content area
+ const viewRight = viewInfo.view.x + viewInfo.view.width;
+ const viewBottom = viewInfo.view.y + viewInfo.view.height;
+ const contentWidth = viewInfo.windowContent.width;
+ const contentHeight = viewInfo.windowContent.height;
+
+ const isWithinBounds = viewInfo.view.x >= 0 &&
+ viewInfo.view.y >= 0 &&
+ viewRight <= contentWidth &&
+ viewBottom <= contentHeight &&
+ viewInfo.view.width > 0 &&
+ viewInfo.view.height > 0;
+
+ if (!isWithinBounds) {
+ throw new Error(
+ `Browser view is not positioned within visible window bounds.\n` +
+ `View: {x: ${viewInfo.view.x}, y: ${viewInfo.view.y}, width: ${viewInfo.view.width}, height: ${viewInfo.view.height}}, ` +
+ `Window content: {width: ${contentWidth}, height: ${contentHeight}}`,
+ );
+ }
+});
+
+When('I confirm the {string} window browser view is not positioned within visible window bounds', async function(this: ApplicationWorld, windowType: string) {
+ if (!this.app) {
+ throw new Error('Application is not available');
+ }
+
+ const targetWindow = await this.findWindowByType(windowType);
+ if (!targetWindow || targetWindow.isClosed()) {
+ throw new Error(`Window "${windowType}" is not available or has been closed`);
+ }
+
+ // Get the window dimensions to identify it - must match a defined WindowNames
+ const windowName = checkWindowName(windowType);
+ const windowDimensions = checkWindowDimension(windowName);
+
+ // Get browser view bounds for the specific window type
+ const viewInfo = await getBrowserViewInfo(this.app, windowDimensions);
+
+ if (!viewInfo.hasView || !viewInfo.view || !viewInfo.windowContent) {
+ // No view found is acceptable for this check - means it's definitely not visible
+ return;
+ }
+
+ // Check if browser view is OUTSIDE window content bounds
+ // View coordinates are relative to the window, so we check if they're outside the content area
+ const viewRight = viewInfo.view.x + viewInfo.view.width;
+ const viewBottom = viewInfo.view.y + viewInfo.view.height;
+ const contentWidth = viewInfo.windowContent.width;
+ const contentHeight = viewInfo.windowContent.height;
+
+ const isWithinBounds = viewInfo.view.x >= 0 &&
+ viewInfo.view.y >= 0 &&
+ viewRight <= contentWidth &&
+ viewBottom <= contentHeight &&
+ viewInfo.view.width > 0 &&
+ viewInfo.view.height > 0;
+
+ if (isWithinBounds) {
+ throw new Error(
+ `Browser view IS positioned within visible window bounds, but expected it to be outside.\n` +
+ `View: {x: ${viewInfo.view.x}, y: ${viewInfo.view.y}, width: ${viewInfo.view.width}, height: ${viewInfo.view.height}}, ` +
+ `Window content: {width: ${contentWidth}, height: ${contentHeight}}`,
+ );
+ }
+});
diff --git a/features/supports/paths.ts b/features/supports/paths.ts
index e41f8b9e..5adaa786 100644
--- a/features/supports/paths.ts
+++ b/features/supports/paths.ts
@@ -40,7 +40,7 @@ export function getPackedAppPath(): string {
}
throw new Error(
- `TidGi executable not found. Checked paths:\n${possiblePaths.join('\n')}\n\nYou should run \`pnpm run package:dev\` before running the tests to ensure the app is built.`,
+ `TidGi executable not found. Checked paths:\n${possiblePaths.join('\n')}\n\nYou should run \`pnpm run test:prepare-e2e\` before running the tests to ensure the app is built.`,
);
}
diff --git a/features/supports/webContentsViewHelper.ts b/features/supports/webContentsViewHelper.ts
new file mode 100644
index 00000000..632bceb1
--- /dev/null
+++ b/features/supports/webContentsViewHelper.ts
@@ -0,0 +1,151 @@
+import type { ElectronApplication } from 'playwright';
+
+/**
+ * Get text content from WebContentsView
+ * @param app Electron application instance
+ * @returns Promise Returns text content or null
+ */
+export async function getTextContent(app: ElectronApplication): Promise {
+ return await app.evaluate(async ({ BrowserWindow }) => {
+ // Get all browser windows
+ const windows = BrowserWindow.getAllWindows();
+
+ for (const window of windows) {
+ // Get all child views (WebContentsView instances) attached to this window
+ if (window.contentView && 'children' in window.contentView) {
+ const views = window.contentView.children || [];
+
+ for (const view of views) {
+ // Type guard to check if view is a WebContentsView
+ if (view && view.constructor.name === 'WebContentsView') {
+ try {
+ // Cast to WebContentsView type and execute JavaScript
+ const webContentsView = view as unknown as { webContents: { executeJavaScript: (script: string) => Promise } };
+ const content = await webContentsView.webContents.executeJavaScript(`
+ document.body.textContent || document.body.innerText || ''
+ `);
+ if (content && content.trim()) {
+ return content;
+ }
+ } catch {
+ // Continue to next view if this one fails
+ continue;
+ }
+ }
+ }
+ }
+ }
+ return null;
+ });
+}
+
+/**
+ * Get DOM content from WebContentsView
+ * @param app Electron application instance
+ * @returns Promise Returns DOM content or null
+ */
+export async function getDOMContent(app: ElectronApplication): Promise {
+ return await app.evaluate(async ({ BrowserWindow }) => {
+ // Get all browser windows
+ const windows = BrowserWindow.getAllWindows();
+
+ for (const window of windows) {
+ // Get all child views (WebContentsView instances) attached to this window
+ if (window.contentView && 'children' in window.contentView) {
+ const views = window.contentView.children || [];
+
+ for (const view of views) {
+ // Type guard to check if view is a WebContentsView
+ if (view && view.constructor.name === 'WebContentsView') {
+ try {
+ // Cast to WebContentsView type and execute JavaScript
+ const webContentsView = view as unknown as { webContents: { executeJavaScript: (script: string) => Promise } };
+ const content = await webContentsView.webContents.executeJavaScript(`
+ document.documentElement.outerHTML || ''
+ `);
+ if (content && content.trim()) {
+ return content;
+ }
+ } catch {
+ // Continue to next view if this one fails
+ continue;
+ }
+ }
+ }
+ }
+ }
+ return null;
+ });
+}
+
+/**
+ * Check if WebContentsView exists and is loaded
+ * @param app Electron application instance
+ * @returns Promise Returns whether it exists and is loaded
+ */
+export async function isLoaded(app: ElectronApplication): Promise {
+ return await app.evaluate(async ({ BrowserWindow }) => {
+ // Get all browser windows
+ const windows = BrowserWindow.getAllWindows();
+
+ for (const window of windows) {
+ // Get all child views (WebContentsView instances) attached to this window
+ if (window.contentView && 'children' in window.contentView) {
+ const views = window.contentView.children || [];
+
+ for (const view of views) {
+ // Type guard to check if view is a WebContentsView
+ if (view && view.constructor.name === 'WebContentsView') {
+ // If we found a WebContentsView, consider it loaded
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ });
+}
+
+/**
+ * Find specified text in WebContentsView
+ * @param app Electron application instance
+ * @param expectedText Text to search for
+ * @param contentType Content type: 'text' or 'dom'
+ * @returns Promise Returns whether text was found
+ */
+export async function containsText(
+ app: ElectronApplication,
+ expectedText: string,
+ contentType: 'text' | 'dom' = 'text',
+): Promise {
+ const content = contentType === 'text'
+ ? await getTextContent(app)
+ : await getDOMContent(app);
+
+ return content !== null && content.includes(expectedText);
+}
+
+/**
+ * Get WebContentsView content summary (for error messages)
+ * @param app Electron application instance
+ * @param contentType Content type: 'text' or 'dom'
+ * @param maxLength Maximum length, default 200
+ * @returns Promise Returns content summary
+ */
+export async function getContentSummary(
+ app: ElectronApplication,
+ contentType: 'text' | 'dom' = 'text',
+ maxLength: number = 200,
+): Promise {
+ const content = contentType === 'text'
+ ? await getTextContent(app)
+ : await getDOMContent(app);
+
+ if (!content) {
+ return 'null';
+ }
+
+ return content.length > maxLength
+ ? content.substring(0, maxLength) + '...'
+ : content;
+}
diff --git a/features/tidgiMiniWindow.feature b/features/tidgiMiniWindow.feature
new file mode 100644
index 00000000..272fc762
--- /dev/null
+++ b/features/tidgiMiniWindow.feature
@@ -0,0 +1,38 @@
+@tidgiminiwindow
+Feature: TidGi Mini Window
+ As a user
+ I want to enable and use the TidGi mini window
+ So that I can quickly access TidGi from the system tray
+
+ Scenario: Enable tidgi mini window and test keyboard shortcut
+ Given I cleanup test wiki so it could create a new one on start
+ And I launch the TidGi application
+ And I wait for the page to load completely
+ And I click on an "open preferences button" element with selector "#open-preferences-button"
+ And I switch to "preferences" window
+ When I click on a "tidgi mini window section" element with selector "[data-testid='preference-section-tidgiMiniWindow']"
+ And I confirm the "tidgiMiniWindow" window does not exist
+ When I click on an "attach to tidgi mini window switch" element with selector "[data-testid='attach-to-tidgi-mini-window-switch']"
+ And I confirm the "tidgiMiniWindow" window exists
+ And I confirm the "tidgiMiniWindow" window not visible
+ Then I should see "always on top toggle and workspace sync toggle" elements with selectors:
+ | [data-testid='tidgi-mini-window-always-on-top-switch'] |
+ | [data-testid='tidgi-mini-window-sync-workspace-switch'] |
+ Then I click on a "shortcut register button" element with selector "[data-testid='shortcut-register-button']"
+ And I press the key combination "CommandOrControl+Shift+M"
+ And I click on a "shortcut confirm button" element with selector "[data-testid='shortcut-confirm-button']"
+ And I close "preferences" window
+ Then I switch to "main" window
+ When I press the key combination "CommandOrControl+Shift+M"
+ And I confirm the "tidgiMiniWindow" window exists
+ And I confirm the "tidgiMiniWindow" window visible
+ And I confirm the "tidgiMiniWindow" window browser view is positioned within visible window bounds
+ And I switch to "tidgiMiniWindow" window
+ Then the browser view should be loaded and visible
+ And I should see "我的 TiddlyWiki" in the browser view content
+ Then I switch to "main" window
+ When I press the key combination "CommandOrControl+Shift+M"
+ And I wait for 2 seconds
+ And I confirm the "tidgiMiniWindow" window exists
+ And I confirm the "tidgiMiniWindow" window not visible
+
diff --git a/features/tidgiMiniWindowWorkspace.feature b/features/tidgiMiniWindowWorkspace.feature
new file mode 100644
index 00000000..10bb2fbd
--- /dev/null
+++ b/features/tidgiMiniWindowWorkspace.feature
@@ -0,0 +1,130 @@
+@tidgiminiwindow
+Feature: TidGi Mini Window Workspace Switching
+ As a user with tidgi mini window already enabled
+ I want to test tidgi mini window behavior with different workspace configurations
+ So that I can verify workspace switching and fixed workspace features
+
+ Background:
+ Given I configure tidgi mini window with shortcut
+ Then I launch the TidGi application
+ And I wait for the page to load completely
+ Then I switch to "main" window
+
+ Scenario: TidGi mini window syncs with main window switching to agent workspace
+ # Switch main window to agent workspace
+ When I click on an "agent workspace button" element with selector "[data-testid='workspace-agent']"
+ # Verify tidgi mini window exists in background (created but not visible)
+ And I wait for 0.2 seconds
+ Then I confirm the "tidgiMiniWindow" window exists
+ And I confirm the "tidgiMiniWindow" window not visible
+ When I press the key combination "CommandOrControl+Shift+M"
+ And I confirm the "tidgiMiniWindow" window visible
+ And I confirm the "tidgiMiniWindow" window browser view is not positioned within visible window bounds
+ Then I switch to "tidgiMiniWindow" window
+ # In sync mode, browser view shows the current active workspace (agent), sidebar is hidden
+ And I should see a "new tab button" element with selector "[data-tab-id='new-tab-button']"
+
+ Scenario: TidGi mini window with fixed agent workspace shows no view and fixed wiki workspace shows browser view
+ # Configure fixed agent workspace through UI
+ And I click on an "open preferences button" element with selector "#open-preferences-button"
+ And I switch to "preferences" window
+ When I click on "tidgi mini window section and disable sync workspace switch" elements with selectors:
+ | [data-testid='preference-section-tidgiMiniWindow'] |
+ | [data-testid='tidgi-mini-window-sync-workspace-switch'] |
+ # Enable sidebar to see workspace buttons
+ And I click on a "Enable sidebar toggle switch" element with selector "[data-testid='sidebar-on-tidgi-mini-window-switch']"
+ And I wait for 0.2 seconds
+ # Select agent workspace (which is a page type workspace)
+ And I select "agent" from MUI Select with test id "tidgi-mini-window-fixed-workspace-select"
+ # Open tidgi mini window - should show agent workspace and no browser view
+ When I press the key combination "CommandOrControl+Shift+M"
+ And I confirm the "tidgiMiniWindow" window visible
+ And I confirm the "tidgiMiniWindow" window browser view is not positioned within visible window bounds
+ Then I switch to "tidgiMiniWindow" window
+ # Verify sidebar is visible
+ And I should see a "main sidebar" element with selector "[data-testid='main-sidebar']"
+ # Verify agent workspace is active
+ And I should see a "agent workspace active button" element with selector "[data-testid='workspace-agent'][data-active='true']"
+ # Close tidgi mini window and switch to wiki workspace
+ And I wait for 0.2 seconds
+ Then I switch to "preferences" window
+ When I press the key combination "CommandOrControl+Shift+M"
+ And I confirm the "tidgiMiniWindow" window not visible
+ # Get the first wiki workspace ID and select it
+ And I select "wiki" from MUI Select with test id "tidgi-mini-window-fixed-workspace-select"
+ And I wait for 0.2 seconds
+ # Open tidgi mini window again - should show wiki workspace with browser view
+ When I press the key combination "CommandOrControl+Shift+M"
+ And I confirm the "tidgiMiniWindow" window visible
+ And I confirm the "tidgiMiniWindow" window browser view is positioned within visible window bounds
+ Then I switch to "tidgiMiniWindow" window
+ # Verify sidebar is visible
+ And I should see a "main sidebar" element with selector "[data-testid='main-sidebar']"
+ # Verify browser view content is visible and wiki workspace is active
+ And I should see "我的 TiddlyWiki" in the browser view content
+ And I should see a "wiki workspace active button" element with selector "[data-active='true']"
+
+ Scenario: Enabling sync workspace automatically hides sidebar
+ # Configure tidgi mini window with fixed workspace first
+ When I click on "agent workspace button and open preferences button" elements with selectors:
+ | [data-testid='workspace-agent'] |
+ | #open-preferences-button |
+ And I switch to "preferences" window
+ When I click on "tidgi mini window section and disable sync workspace switch" elements with selectors:
+ | [data-testid='preference-section-tidgiMiniWindow'] |
+ | [data-testid='tidgi-mini-window-sync-workspace-switch'] |
+ And I should see a "sidebar toggle switch" element with selector "[data-testid='sidebar-on-tidgi-mini-window-switch']"
+ # Enable sidebar to see it in mini window
+ And I click on a "Enable sidebar toggle switch" element with selector "[data-testid='sidebar-on-tidgi-mini-window-switch']"
+ # Open tidgi mini window and verify sidebar is visible
+ When I press the key combination "CommandOrControl+Shift+M"
+ And I confirm the "tidgiMiniWindow" window visible
+ And I switch to "tidgiMiniWindow" window
+ And I should see a "main sidebar" element with selector "[data-testid='main-sidebar']"
+ # Close mini window and go back to preferences
+ Then I switch to "preferences" window
+ When I press the key combination "CommandOrControl+Shift+M"
+ And I confirm the "tidgiMiniWindow" window not visible
+ # Now enable sync workspace - should automatically hide sidebar
+ When I click on a "Enable tidgi mini window sync workspace switch" element with selector "[data-testid='tidgi-mini-window-sync-workspace-switch']"
+ # Verify sidebar option is now hidden
+ And I should not see "sidebar toggle switch and fixed workspace select" elements with selectors:
+ | [data-testid='sidebar-on-tidgi-mini-window-switch'] |
+ | [data-testid='tidgi-mini-window-fixed-workspace-select'] |
+ # Open tidgi mini window in sync mode - should sync to agent workspace
+ When I press the key combination "CommandOrControl+Shift+M"
+ And I confirm the "tidgiMiniWindow" window visible
+ And I switch to "tidgiMiniWindow" window
+ # In sync mode, sidebar should not be visible (automatically hidden)
+ And I should not see a "main sidebar" element with selector "[data-testid='main-sidebar']"
+
+ Scenario: Clicking workspace button in mini window updates fixed workspace ID
+ # First click on guide workspace in main window to set a different active workspace
+ Then I switch to "main" window
+ When I click on a "guide workspace button" element with selector "[data-testid='workspace-guide']"
+ And I wait for 0.2 seconds
+ # Configure tidgi mini window with fixed workspace
+ And I click on an "open preferences button" element with selector "#open-preferences-button"
+ And I switch to "preferences" window
+ When I click on "tidgi mini window section and disable sync workspace switch" elements with selectors:
+ | [data-testid='preference-section-tidgiMiniWindow'] |
+ | [data-testid='tidgi-mini-window-sync-workspace-switch'] |
+ # Enable sidebar to see workspace buttons
+ And I click on a "Enable sidebar toggle switch" element with selector "[data-testid='sidebar-on-tidgi-mini-window-switch']"
+ And I wait for 0.2 seconds
+ # Select agent workspace as fixed workspace
+ And I select "agent" from MUI Select with test id "tidgi-mini-window-fixed-workspace-select"
+ And I wait for 0.2 seconds
+ # Open tidgi mini window
+ When I press the key combination "CommandOrControl+Shift+M"
+ And I confirm the "tidgiMiniWindow" window visible
+ Then I switch to "tidgiMiniWindow" window
+ # Verify agent workspace is active initially
+ And I should see a "agent workspace button with active state" element with selector "[data-testid='workspace-agent'][data-active='true']"
+ # Click on guide workspace button to update fixed workspace ID
+ When I click on a "guide workspace button" element with selector "[data-testid='workspace-guide']"
+ And I wait for 0.2 seconds
+ # Verify guide workspace is now active and agent workspace is no longer active
+ And I should see "guide workspace button with active state and agent workspace button without active state" elements with selectors:
+ | [data-testid='workspace-guide'][data-active='true'] |
+ | [data-testid='workspace-agent'][data-active='false'] |
\ No newline at end of file
diff --git a/forge.config.ts b/forge.config.ts
index 31c90a0a..a5968b87 100644
--- a/forge.config.ts
+++ b/forge.config.ts
@@ -32,7 +32,7 @@ const config: ForgeConfig = {
// Unpack worker files, native modules path, and ALL .node binaries (including better-sqlite3)
unpack: '{**/.webpack/main/*.worker.*,**/.webpack/main/native_modules/path.txt,**/{.**,**}/**/*.node}',
},
- extraResource: ['localization', 'template/wiki', 'build-resources/menubar@2x.png', 'build-resources/menubarTemplate@2x.png'],
+ extraResource: ['localization', 'template/wiki', 'build-resources/tidgiMiniWindow@2x.png', 'build-resources/tidgiMiniWindowTemplate@2x.png'],
// @ts-expect-error - mac config is valid
mac: {
category: 'productivity',
diff --git a/localization/locales/en/translation.json b/localization/locales/en/translation.json
index 2f593c6a..ca3ef7d1 100644
--- a/localization/locales/en/translation.json
+++ b/localization/locales/en/translation.json
@@ -92,7 +92,7 @@
"OpenCommandPalette": "Open CommandPalette",
"OpenLinkInBrowser": "Open Link in Browser",
"OpenTidGi": "Open TidGi",
- "OpenTidGiMenuBar": "Open TidGi MenuBar",
+ "OpenTidGiMiniWindow": "Open TidGi Mini Window",
"OpenWorkspaceInNewWindow": "Open Workspace in New Window",
"Paste": "Paste",
"Preferences": "Preferences...",
@@ -309,7 +309,7 @@
"SelectNextWorkspace": "Select Next Workspace",
"SelectPreviousWorkspace": "Select Previous Workspace",
"TidGi": "TidGi",
- "TidGiMenuBar": "TidGi MenuBar",
+ "TidGiMiniWindow": "TidGi Mini Window",
"View": "View",
"Wiki": "Wiki",
"Window": "Window",
@@ -324,10 +324,10 @@
"AlwaysOnTopDetail": "Keep TidGi’s main window always on top of other windows, and will not be covered by other windows",
"AntiAntiLeech": "Some website has Anti-Leech, will prevent some images from being displayed on your wiki, we simulate a request header that looks like visiting that website to bypass this protection.",
"AskDownloadLocation": "Ask where to save each file before downloading",
- "AttachToMenuBar": "Attach to menu bar",
- "AttachToMenuBarShowSidebar": "Attach To Menu Bar Show Sidebar",
- "AttachToMenuBarShowSidebarTip": "Generally, TidGi small window is only used to quickly view the current workspace, so the default synchronization with the main window workspace, do not need a sidebar, the default hidden sidebar.",
- "AttachToMenuBarTip": "Make a small TidGi popup window that pop when you click appbar mini icon. Tip: Right-click on mini app icon to access context menu.",
+ "TidgiMiniWindow": "Attach to TidGi mini window",
+ "TidgiMiniWindowShowSidebar": "Attach To TidGi Mini Window Show Sidebar",
+ "TidgiMiniWindowShowSidebarTip": "Generally, TidGi mini window is only used to quickly view the current workspace, so the default synchronization with the main window workspace, do not need a sidebar, the default hidden sidebar.",
+ "TidgiMiniWindowTip": "Make a small TidGi popup window that pop when you click system tray mini icon. Tip: Right-click on mini app icon to access context menu.",
"AttachToTaskbar": "Attach to taskbar",
"AttachToTaskbarShowSidebar": "Attach To Taskbar Show Sidebar",
"ChooseLanguage": "Choose Language 选择语言",
@@ -362,8 +362,16 @@
"ItIsWorking": "It is working!",
"Languages": "Lang/语言",
"LightTheme": "Light Theme",
- "MenubarAlwaysOnTop": "Menubar Always on top",
- "MenubarAlwaysOnTopDetail": "Keep TidGi’s Menubar always on top of other windows, and will not be covered by other windows",
+ "TidgiMiniWindowAlwaysOnTop": "TidGi Mini Window Always on top",
+ "TidgiMiniWindowAlwaysOnTopDetail": "Keep TidGi's Mini Window always on top of other windows, and will not be covered by other windows",
+ "TidgiMiniWindowFixedWorkspace": "Select workspace for fixed TidGi Mini Window",
+ "TidgiMiniWindowShortcutKey": "Set shortcut key to toggle TidGi Mini Window",
+ "TidgiMiniWindowShortcutKeyHelperText": "Set a shortcut key to quickly open or close TidGi Mini Window",
+ "TidgiMiniWindowShortcutKeyPlaceholder": "e.g.: Ctrl+Shift+D",
+ "TidgiMiniWindowSyncWorkspaceWithMainWindow": "TidGi Mini Window syncs with main window workspace",
+ "TidgiMiniWindowSyncWorkspaceWithMainWindowDetail": "When checked, TidGi Mini Window will display the same workspace content as main window",
+ "TidgiMiniWindowShowTitleBar": "Show title bar on TidGi Mini Window",
+ "TidgiMiniWindowShowTitleBarDetail": "Show draggable title bar on TidGi Mini Window",
"Miscellaneous": "Miscellaneous",
"MoreWorkspaceSyncSettings": "More Workspace Sync Settings",
"MoreWorkspaceSyncSettingsDescription": "Please right-click the workspace icon, open its workspace setting by click on \"Edit Workspace\" context menu item, and configure its independent synchronization settings in it.",
@@ -430,7 +438,7 @@
"TestNotificationDescription": "<0>If notifications dont show up, make sure you enable notifications in<1>macOS Preferences → Notifications → TidGi1>.0>",
"Theme": "Theme",
"TiddlyWiki": "TiddlyWiki",
- "ToggleMenuBar": "Toggle Menu Bar",
+ "ToggleTidgiMiniWindow": "Toggle TidGi Mini Window",
"Token": "Git credentials",
"TokenDescription": "The credentials used to authenticate to the Git server so you can securely synchronize content. Can be obtained by logging in to storage services (e.g., Github), or manually obtain \"personal access token\" and filled in here.",
"Translatium": "Translatium",
diff --git a/localization/locales/fr/translation.json b/localization/locales/fr/translation.json
index 3c39e936..eb751d72 100644
--- a/localization/locales/fr/translation.json
+++ b/localization/locales/fr/translation.json
@@ -92,7 +92,7 @@
"OpenCommandPalette": "Ouvrir la palette de commandes",
"OpenLinkInBrowser": "Ouvrir le lien dans le navigateur",
"OpenTidGi": "Ouvrir TidGi",
- "OpenTidGiMenuBar": "Ouvrir la barre de menu TidGi",
+ "OpenTidGiMiniWindow": "Ouvrir la mini-fenêtre TidGi",
"OpenWorkspaceInNewWindow": "Ouvrir l'espace de travail dans une nouvelle fenêtre",
"Paste": "Coller",
"Preferences": "Préférences...",
@@ -309,7 +309,7 @@
"SelectNextWorkspace": "Sélectionner l'espace de travail suivant",
"SelectPreviousWorkspace": "Sélectionner l'espace de travail précédent",
"TidGi": "TidGi",
- "TidGiMenuBar": "Barre de menu TidGi",
+ "TidGiMiniWindow": "Mini-fenêtre TidGi",
"View": "Vue",
"Wiki": "Wiki",
"Window": "Fenêtre",
@@ -324,10 +324,10 @@
"AlwaysOnTopDetail": "Garder la fenêtre principale de TidGi toujours au-dessus des autres fenêtres, et ne sera pas couverte par d'autres fenêtres",
"AntiAntiLeech": "Certains sites web ont une protection anti-leech, empêchant certaines images d'être affichées sur votre wiki, nous simulons un en-tête de requête qui ressemble à la visite de ce site web pour contourner cette protection.",
"AskDownloadLocation": "Demander où enregistrer chaque fichier avant de télécharger",
- "AttachToMenuBar": "Attacher à la barre de menu",
- "AttachToMenuBarShowSidebar": "Attacher à la barre de menu Afficher la barre latérale",
- "AttachToMenuBarShowSidebarTip": "En général, la petite fenêtre TidGi est uniquement utilisée pour visualiser rapidement l'espace de travail actuel, donc la synchronisation avec l'espace de travail de la fenêtre principale n'est pas nécessaire, la barre latérale est masquée par défaut.",
- "AttachToMenuBarTip": "Créer une petite fenêtre contextuelle TidGi qui apparaît lorsque vous cliquez sur l'icône mini de la barre d'application. Astuce : Cliquez avec le bouton droit sur l'icône mini de l'application pour accéder au menu contextuel.",
+ "TidgiMiniWindow": "Attacher à la mini-fenêtre TidGi",
+ "TidgiMiniWindowShowSidebar": "Attacher à la mini-fenêtre TidGi Afficher la barre latérale",
+ "TidgiMiniWindowShowSidebarTip": "En général, la petite fenêtre TidGi est uniquement utilisée pour visualiser rapidement l'espace de travail actuel, donc la synchronisation avec l'espace de travail de la fenêtre principale n'est pas nécessaire, la barre latérale est masquée par défaut.",
+ "TidgiMiniWindowTip": "Créer une petite fenêtre contextuelle TidGi qui apparaît lorsque vous cliquez sur l'icône mini de la barre d'application. Astuce : Cliquez avec le bouton droit sur l'icône mini de l'application pour accéder au menu contextuel.",
"AttachToTaskbar": "Attacher à la barre des tâches",
"AttachToTaskbarShowSidebar": "Attacher à la barre des tâches Afficher la barre latérale",
"ChooseLanguage": "Choisir la langue 选择语言",
@@ -349,8 +349,8 @@
"General": "UI & Interact",
"HibernateAllUnusedWorkspaces": "Mettre en veille les espaces de travail inutilisés au lancement de l'application",
"HibernateAllUnusedWorkspacesDescription": "Mettre en veille tous les espaces de travail au lancement, sauf le dernier espace de travail actif.",
- "HideMenuBar": "Masquer la barre de menu",
- "HideMenuBarDetail": "Masquer la barre de menu sauf si Alt+M est pressé.",
+ "HideTidgiMiniWindow": "Masquer la mini-fenêtre TidGi",
+ "HideTidgiMiniWindowDetail": "Masquer la mini-fenêtre TidGi sauf si Alt+M est pressé.",
"HideSideBar": "Masquer la barre latérale",
"HideSideBarIconDetail": "Masquer l'icône et n'afficher que le nom de l'espace de travail pour rendre la liste des espaces de travail plus compacte",
"HideTitleBar": "Masquer la barre de titre",
@@ -360,8 +360,8 @@
"ItIsWorking": "Ça fonctionne !",
"Languages": "Lang/语言",
"LightTheme": "Thème clair",
- "MenubarAlwaysOnTop": "Barre de menu toujours au-dessus",
- "MenubarAlwaysOnTopDetail": "Garder la barre de menu de TidGi toujours au-dessus des autres fenêtres, et ne sera pas couverte par d'autres fenêtres",
+ "TidgiMiniWindowAlwaysOnTop": "TidGi Mini Window toujours au-dessus",
+ "TidgiMiniWindowAlwaysOnTopDetail": "Garder la Mini Window de TidGi toujours au-dessus des autres fenêtres, et ne sera pas couverte par d'autres fenêtres",
"Miscellaneous": "Divers",
"MoreWorkspaceSyncSettings": "Plus de paramètres de synchronisation de l'espace de travail",
"MoreWorkspaceSyncSettingsDescription": "Veuillez cliquer avec le bouton droit sur l'icône de l'espace de travail, ouvrir ses paramètres d'espace de travail en cliquant sur l'élément de menu contextuel \"Modifier l'espace de travail\", et configurer ses paramètres de synchronisation indépendants.",
@@ -414,7 +414,7 @@
"TestNotificationDescription": "<0>Si les notifications ne s'affichent pas, assurez-vous d'activer les notifications dans<1>Préférences macOS → Notifications → TidGi1>.0>",
"Theme": "Thème",
"TiddlyWiki": "TiddlyWiki",
- "ToggleMenuBar": "Basculer la barre de menu",
+ "ToggleTidgiMiniWindow": "Basculer la mini-fenêtre TidGi",
"Token": "Informations d'identification Git",
"TokenDescription": "Les informations d'identification utilisées pour s'authentifier auprès du serveur Git afin de pouvoir synchroniser le contenu en toute sécurité. Peut être obtenu en se connectant à des services de stockage (par exemple, Github), ou en obtenant manuellement un \"jeton d'accès personnel\" et en le remplissant ici.",
"Translatium": "Translatium",
diff --git a/localization/locales/ja/translation.json b/localization/locales/ja/translation.json
index 509b9665..429e5a55 100644
--- a/localization/locales/ja/translation.json
+++ b/localization/locales/ja/translation.json
@@ -92,7 +92,7 @@
"OpenCommandPalette": "コマンドパレットを開く",
"OpenLinkInBrowser": "ブラウザでリンクを開く",
"OpenTidGi": "TidGiを開く",
- "OpenTidGiMenuBar": "TidGiメニューバーを開く",
+ "OpenTidGiMiniWindow": "TidGiミニウィンドウを開く",
"OpenWorkspaceInNewWindow": "ワークスペースを新しいウィンドウで開く",
"Paste": "貼り付け",
"Preferences": "設定...",
@@ -359,8 +359,8 @@
"ItIsWorking": "使いやすい!",
"Languages": "言語/ランゲージ",
"LightTheme": "明るい色のテーマ",
- "MenubarAlwaysOnTop": "メニューバーの小ウィンドウを他のウィンドウの上に保持する",
- "MenubarAlwaysOnTopDetail": "太記のメニューバーウィンドウを常に他のウィンドウの上に表示させ、他のウィンドウで覆われないようにします。",
+ "TidgiMiniWindowAlwaysOnTop": "太記小ウィンドウを他のウィンドウの上に保持する",
+ "TidgiMiniWindowAlwaysOnTopDetail": "太記の小ウィンドウを常に他のウィンドウの上に表示させ、他のウィンドウで覆われないようにします。",
"Miscellaneous": "その他の設定",
"MoreWorkspaceSyncSettings": "さらに多くのワークスペース同期設定",
"MoreWorkspaceSyncSettingsDescription": "ワークスペースアイコンを右クリックし、右クリックメニューから「ワークスペースの編集」を選択して、ワークスペース設定を開いてください。そこで各ワークスペースの同期設定を行います。",
diff --git a/localization/locales/ru/translation.json b/localization/locales/ru/translation.json
index cf2b2534..b51e3d6d 100644
--- a/localization/locales/ru/translation.json
+++ b/localization/locales/ru/translation.json
@@ -92,7 +92,7 @@
"OpenCommandPalette": "Открыть палитру команд",
"OpenLinkInBrowser": "Открыть ссылку в браузере",
"OpenTidGi": "Открыть TidGi",
- "OpenTidGiMenuBar": "Открыть меню TidGi",
+ "OpenTidGiMiniWindow": "Открыть мини-окно TidGi",
"OpenWorkspaceInNewWindow": "Открыть рабочее пространство в новом окне",
"Paste": "Вставить",
"Preferences": "Настройки...",
@@ -309,7 +309,7 @@
"SelectNextWorkspace": "Выбрать следующее рабочее пространство",
"SelectPreviousWorkspace": "Выбрать предыдущее рабочее пространство",
"TidGi": "TidGi",
- "TidGiMenuBar": "Слишком помню маленькое окно.",
+ "TidGiMiniWindow": "Мини-окно TidGi",
"View": "Просмотр",
"Wiki": "Wiki",
"Window": "Окно",
@@ -359,8 +359,8 @@
"ItIsWorking": "Работает!",
"Languages": "Языки",
"LightTheme": "Светлая тема",
- "MenubarAlwaysOnTop": "Меню всегда сверху",
- "MenubarAlwaysOnTopDetail": "Детали меню всегда сверху",
+ "TidgiMiniWindowAlwaysOnTop": "TidGi мини-окно всегда сверху",
+ "TidgiMiniWindowAlwaysOnTopDetail": "Держать мини-окно TidGi всегда поверх других окон",
"Miscellaneous": "Разное",
"MoreWorkspaceSyncSettings": "Дополнительные настройки синхронизации рабочего пространства",
"MoreWorkspaceSyncSettingsDescription": "Описание дополнительных настроек синхронизации рабочего пространства",
diff --git a/localization/locales/zh-Hans/translation.json b/localization/locales/zh-Hans/translation.json
index 8af669cc..830f8dad 100644
--- a/localization/locales/zh-Hans/translation.json
+++ b/localization/locales/zh-Hans/translation.json
@@ -92,7 +92,7 @@
"OpenCommandPalette": "打开搜索与命令面板",
"OpenLinkInBrowser": "在浏览器中打开链接",
"OpenTidGi": "打开太记",
- "OpenTidGiMenuBar": "打开太记小窗口",
+ "OpenTidGiMiniWindow": "打开太记小窗口",
"OpenWorkspaceInNewWindow": "在新窗口中打开工作区",
"Paste": "粘贴",
"Preferences": "设置...",
@@ -177,6 +177,7 @@
"SyncOnIntervalDescription": "开启后会根据全局设置里的时间间隔自动同步,并且依然会在启动时自动同步,点击按钮也可以手动同步。同步云端前会自动先把数据备份到本地Git。如果关闭,则只有在应用程序打开时会有一次自动同步,还有当用户通过点击知识库中的同步按钮手动触发同步。",
"SyncOnStartup": "启动时自动同步",
"SyncOnStartupDescription": "在应用冷启动时自动同步一次。",
+ "TiddlyWiki": "太微",
"TokenAuth": "凭证鉴权",
"TokenAuthAutoFillUserNameDescription": "此功能需要在全局设置或工作区设置里填写用户名,不然不会生效。若你未填,将自动在工作区设置里填一个默认值,你可自行修改。",
"TokenAuthCurrentHeader": "凭证鉴权当前请求头",
@@ -188,7 +189,6 @@
"UploadOrSelectPathDescription": "点击上传按钮将文件提交给太记保管,也可以点击选择路径按钮从你保管的位置选取文件。",
"WikiRootTiddler": "知识库根条目",
"WikiRootTiddlerDescription": "知识库的根条目(root-tiddler)决定了系统的核心行为,修改前请阅读官方文档来了解",
- "TiddlyWiki": "太微",
"WikiRootTiddlerItems": {
}
},
@@ -234,6 +234,14 @@
"Tags": {
}
},
+ "KeyboardShortcut": {
+ "Clear": "清除",
+ "HelpText": "按下任意键组合(如 Ctrl+Shift+A)。单独的修饰键将被忽略。",
+ "None": "无",
+ "PressKeys": "请按键...",
+ "PressKeysPrompt": "请为{{feature}}按下快捷键组合",
+ "RegisterShortcut": "注册快捷键"
+ },
"LOG": {
"CommitBackupMessage": "使用太记桌面版备份",
"CommitMessage": "使用太记桌面版同步"
@@ -309,7 +317,7 @@
"SelectNextWorkspace": "选择下一个工作区",
"SelectPreviousWorkspace": "选择前一个工作区",
"TidGi": "太记",
- "TidGiMenuBar": "太记小窗",
+ "TidGiMiniWindow": "太记小窗",
"View": "查看",
"Wiki": "知识库",
"Window": "窗口",
@@ -324,10 +332,10 @@
"AlwaysOnTopDetail": "让太记的主窗口永远保持在其它窗口上方,不会被其他窗口覆盖",
"AntiAntiLeech": "有的网站做了防盗链,会阻止某些图片在你的知识库上显示,我们通过模拟访问该网站的请求头来绕过这种限制。",
"AskDownloadLocation": "下载前询问每个文件的保存位置",
- "AttachToMenuBar": "附加到菜单栏",
- "AttachToMenuBarShowSidebar": "附加到菜单栏的窗口包含侧边栏",
- "AttachToMenuBarShowSidebarTip": "一般太记小窗仅用于快速查看当前工作区,所以默认与主窗口工作区同步,不需要侧边栏,默认隐藏侧边栏。",
- "AttachToMenuBarTip": "创建一个点击菜单栏/任务栏图标会弹出的小太记窗口。提示:右键单击小图标以访问上下文菜单。",
+ "TidgiMiniWindow": "附加到太记小窗",
+ "TidgiMiniWindowShowSidebar": "太记小窗包含侧边栏",
+ "TidgiMiniWindowShowSidebarTip": "一般太记小窗仅用于快速查看当前工作区,所以默认与主窗口工作区同步,不需要侧边栏,默认隐藏侧边栏。",
+ "TidgiMiniWindowTip": "创建一个点击系统托盘图标会弹出的小太记窗口。提示:右键单击小图标以访问上下文菜单。",
"AttachToTaskbar": "附加到任务栏",
"AttachToTaskbarShowSidebar": "附加到任务栏的窗口包含侧边栏",
"ChooseLanguage": "选择语言 Choose Language",
@@ -363,8 +371,16 @@
"ItIsWorking": "好使的!",
"Languages": "语言/Lang",
"LightTheme": "亮色主题",
- "MenubarAlwaysOnTop": "保持菜单栏小窗口在其他窗口上方",
- "MenubarAlwaysOnTopDetail": "让太记的菜单栏小窗口永远保持在其它窗口上方,不会被其他窗口覆盖",
+ "TidgiMiniWindowAlwaysOnTop": "保持太记小窗口在其他窗口上方",
+ "TidgiMiniWindowAlwaysOnTopDetail": "让太记的小窗口永远保持在其它窗口上方,不会被其他窗口覆盖",
+ "TidgiMiniWindowFixedWorkspace": "为固定的太记小窗口选择工作区",
+ "TidgiMiniWindowShortcutKey": "设置快捷键来切换太记小窗口",
+ "TidgiMiniWindowShortcutKeyHelperText": "设置一个快捷键来快速打开或关闭太记小窗口",
+ "TidgiMiniWindowShortcutKeyPlaceholder": "例如:Ctrl+Shift+D",
+ "TidgiMiniWindowSyncWorkspaceWithMainWindow": "小窗和主窗口展示同样的工作区",
+ "TidgiMiniWindowSyncWorkspaceWithMainWindowDetail": "勾选后,小窗将与主窗口同步显示相同的工作区内容",
+ "TidgiMiniWindowShowTitleBar": "小窗显示标题栏",
+ "TidgiMiniWindowShowTitleBarDetail": "在太记小窗口上显示可拖动的标题栏",
"Miscellaneous": "其他设置",
"MoreWorkspaceSyncSettings": "更多工作区同步设置",
"MoreWorkspaceSyncSettingsDescription": "请右键工作区图标,点右键菜单里的「编辑工作区」来打开工作区设置,在里面配各个工作区的同步设置。",
@@ -406,6 +422,7 @@
"SearchEmbeddingStatusIdle": "未生成嵌入",
"SearchEmbeddingUpdate": "更新嵌入",
"SearchNoWorkspaces": "未找到工作区",
+ "SelectWorkspace": "选择工作区",
"ShareBrowsingData": "在工作区之间共享浏览器数据(cookies、缓存等),关闭后可以每个工作区登不同的第三方服务账号。",
"ShowSideBar": "显示侧边栏",
"ShowSideBarDetail": "侧边栏让你可以在工作区之间快速切换",
@@ -431,7 +448,7 @@
"TestNotificationDescription": "<0>如果通知未显示,请确保在<1>macOS首选项 → 通知 → TidGi中启用通知1>0>",
"Theme": "主题色",
"TiddlyWiki": "太微",
- "ToggleMenuBar": "切换显隐菜单栏",
+ "ToggleTidgiMiniWindow": "切换太记小窗",
"Token": "Git身份凭证",
"TokenDescription": "用于向Git服务器验证身份并同步内容的凭证,可通过登录在线存储服务(如Github)来取得,也可以手动获取「Personal Access Token」后填到这里。",
"Translatium": "翻译素APP",
@@ -465,6 +482,7 @@
"Add": "添加",
"Agent": "智能体",
"AreYouSure": "你确定要移除这个工作区吗?移除工作区会删除本应用中的工作区,但不会删除硬盘上的文件夹。如果你选择一并删除知识库文件夹,则所有内容都会被删除。",
+ "DedicatedWorkspace": "专用工作区",
"DefaultTiddlers": "默认条目",
"EditCurrentWorkspace": "配置当前工作区",
"EditWorkspace": "配置工作区",
diff --git a/localization/locales/zh-Hant/translation.json b/localization/locales/zh-Hant/translation.json
index 992466e0..ab92283f 100644
--- a/localization/locales/zh-Hant/translation.json
+++ b/localization/locales/zh-Hant/translation.json
@@ -92,7 +92,7 @@
"OpenCommandPalette": "打開搜索與命令面板",
"OpenLinkInBrowser": "在瀏覽器中打開連結",
"OpenTidGi": "打開太記",
- "OpenTidGiMenuBar": "打開太記小窗口",
+ "OpenTidGiMiniWindow": "打開太記小窗口",
"OpenWorkspaceInNewWindow": "在新窗口中打開工作區",
"Paste": "黏貼",
"Preferences": "設置...",
@@ -309,7 +309,7 @@
"SelectNextWorkspace": "選擇下一個工作區",
"SelectPreviousWorkspace": "選擇前一個工作區",
"TidGi": "太記",
- "TidGiMenuBar": "太記小窗",
+ "TidGiMiniWindow": "太記小窗",
"View": "查看",
"Wiki": "知識庫",
"Window": "窗口",
@@ -324,10 +324,10 @@
"AlwaysOnTopDetail": "讓太記的主窗口永遠保持在其它窗口上方,不會被其他窗口覆蓋",
"AntiAntiLeech": "有的網站做了防盜鏈,會阻止某些圖片在你的知識庫上顯示,我們透過模擬訪問該網站的請求頭來繞過這種限制。",
"AskDownloadLocation": "下載前詢問每個文件的保存位置",
- "AttachToMenuBar": "附加到選單欄",
- "AttachToMenuBarShowSidebar": "附加到選單欄的窗口包含側邊欄",
- "AttachToMenuBarShowSidebarTip": "一般太記小窗僅用於快速查看當前工作區,所以默認與主窗口工作區同步,不需要側邊欄,默認隱藏側邊欄。",
- "AttachToMenuBarTip": "創建一個點擊選單欄/任務欄圖示會彈出的小太記窗口。提示:右鍵單擊小圖示以訪問上下文菜單。",
+ "TidgiMiniWindow": "附加到太記小窗",
+ "TidgiMiniWindowShowSidebar": "太記小窗包含側邊欄",
+ "TidgiMiniWindowShowSidebarTip": "一般太記小窗僅用於快速查看當前工作區,所以默認與主窗口工作區同步,不需要側邊欄,默認隱藏側邊欄。",
+ "TidgiMiniWindowTip": "創建一個點擊系統托盤圖示會彈出的小太記窗口。提示:右鍵單擊小圖示以訪問上下文菜單。",
"AttachToTaskbar": "附加到任務欄",
"AttachToTaskbarShowSidebar": "附加到任務欄的窗口包含側邊欄",
"ChooseLanguage": "選擇語言 Choose Language",
@@ -363,8 +363,8 @@
"ItIsWorking": "好使的!",
"Languages": "語言/Lang",
"LightTheme": "亮色主題",
- "MenubarAlwaysOnTop": "保持選單欄小窗口在其他窗口上方",
- "MenubarAlwaysOnTopDetail": "讓太記的選單欄小窗口永遠保持在其它窗口上方,不會被其他窗口覆蓋",
+ "TidgiMiniWindowAlwaysOnTop": "保持太記小窗口在其他窗口上方",
+ "TidgiMiniWindowAlwaysOnTopDetail": "讓太記的小窗口永遠保持在其它窗口上方,不會被其他窗口覆蓋",
"Miscellaneous": "其他設置",
"MoreWorkspaceSyncSettings": "更多工作區同步設定",
"MoreWorkspaceSyncSettingsDescription": "請右鍵工作區圖示,點右鍵菜單裡的「編輯工作區」來打開工作區設置,在裡面配各個工作區的同步設定。",
@@ -431,7 +431,7 @@
"TestNotificationDescription": "<0>如果通知未顯示,請確保在<1>macOS首選項 → 通知 → TidGi中啟用通知1>0>",
"Theme": "主題色",
"TiddlyWiki": "太微",
- "ToggleMenuBar": "切換顯隱選單欄",
+ "ToggleTidgiMiniWindow": "切換太記小窗",
"Token": "Git身份憑證",
"TokenDescription": "用於向Git伺服器驗證身份並同步內容的憑證,可透過登錄在線儲存服務(如Github)來取得,也可以手動獲取「Personal Access Token」後填到這裡。",
"Translatium": "翻譯素APP",
diff --git a/localization/supportedLanguages.json b/localization/supportedLanguages.json
index 149d082f..04dd2f61 100644
--- a/localization/supportedLanguages.json
+++ b/localization/supportedLanguages.json
@@ -1,7 +1,7 @@
{
"en": "English",
- "zh-Hans": "汉字",
- "zh-Hant": "漢字",
+ "zh-Hans": "汉语",
+ "zh-Hant": "漢語",
"ja": "日本語",
"fr": "Français",
"ru": "Русский"
diff --git a/package.json b/package.json
index e7011b30..e6756835 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"description": "Customizable personal knowledge-base with Github as unlimited storage and blogging platform.",
"version": "0.12.4",
"license": "MPL 2.0",
- "packageManager": "pnpm@10.13.1",
+ "packageManager": "pnpm@10.18.2",
"scripts": {
"start:init": "pnpm run clean && pnpm run init:git-submodule && pnpm run build:plugin && cross-env NODE_ENV=development pnpm dlx tsx scripts/developmentMkdir.ts && pnpm run start:dev",
"start:dev": "cross-env NODE_ENV=development electron-forge start",
@@ -18,7 +18,7 @@
"test": "pnpm run test:unit && pnpm run test:prepare-e2e && pnpm run test:e2e",
"test:unit": "cross-env ELECTRON_RUN_AS_NODE=1 ./node_modules/.bin/electron ./node_modules/vitest/vitest.mjs run",
"test:unit:coverage": "pnpm run test:unit --coverage",
- "test:prepare-e2e": "pnpm run clean && pnpm run build:plugin && cross-env NODE_ENV=test DEBUG=electron-forge:* electron-forge package",
+ "test:prepare-e2e": "cross-env READ_DOC_BEFORE_USING='docs/Testing.md' && pnpm run clean && pnpm run build:plugin && cross-env NODE_ENV=test DEBUG=electron-forge:* electron-forge package",
"test:e2e": "rimraf -- ./userData-test ./wiki-test && cross-env NODE_ENV=test pnpm dlx tsx scripts/developmentMkdir.ts && cross-env NODE_ENV=test cucumber-js --config features/cucumber.config.js",
"make": "pnpm run build:plugin && cross-env NODE_ENV=production electron-forge make",
"make:analyze": "cross-env ANALYZE=true pnpm run make",
@@ -65,7 +65,7 @@
"default-gateway": "6.0.3",
"dugite": "2.7.1",
"electron-dl": "^4.0.0",
- "electron-ipc-cat": "2.0.1",
+ "electron-ipc-cat": "2.1.1",
"electron-settings": "5.0.0",
"electron-unhandled": "4.0.1",
"electron-window-state": "5.0.3",
@@ -102,6 +102,7 @@
"rotating-file-stream": "^3.2.5",
"rxjs": "7.8.2",
"semver": "7.7.2",
+ "serialize-error": "^12.0.0",
"simplebar": "6.3.1",
"simplebar-react": "3.3.0",
"source-map-support": "0.5.21",
@@ -122,19 +123,19 @@
"zx": "8.5.5"
},
"optionalDependencies": {
- "@electron-forge/maker-deb": "7.8.1",
- "@electron-forge/maker-flatpak": "7.8.1",
- "@electron-forge/maker-rpm": "7.8.1",
- "@electron-forge/maker-snap": "7.8.1",
- "@electron-forge/maker-squirrel": "7.8.1",
- "@electron-forge/maker-zip": "7.8.1",
- "@reforged/maker-appimage": "^5.0.0"
+ "@electron-forge/maker-deb": "7.10.2",
+ "@electron-forge/maker-flatpak": "7.10.2",
+ "@electron-forge/maker-rpm": "7.10.2",
+ "@electron-forge/maker-snap": "7.10.2",
+ "@electron-forge/maker-squirrel": "7.10.2",
+ "@electron-forge/maker-zip": "7.10.2",
+ "@reforged/maker-appimage": "5.1.0"
},
"devDependencies": {
"@cucumber/cucumber": "^11.2.0",
- "@electron-forge/cli": "7.8.1",
- "@electron-forge/plugin-auto-unpack-natives": "7.8.1",
- "@electron-forge/plugin-vite": "^7.9.0",
+ "@electron-forge/cli": "7.10.2",
+ "@electron-forge/plugin-auto-unpack-natives": "7.10.2",
+ "@electron-forge/plugin-vite": "7.10.2",
"@electron/rebuild": "^4.0.1",
"@fetsorn/vite-node-worker": "^1.0.1",
"@testing-library/jest-dom": "^6.6.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8260c0b9..81dd762a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -113,8 +113,8 @@ importers:
specifier: ^4.0.0
version: 4.0.0
electron-ipc-cat:
- specifier: 2.0.1
- version: 2.0.1(electron@36.4.0)(rxjs@7.8.2)
+ specifier: 2.1.1
+ version: 2.1.1(electron@36.4.0)(rxjs@7.8.2)
electron-settings:
specifier: 5.0.0
version: 5.0.0(electron@36.4.0)
@@ -223,6 +223,9 @@ importers:
semver:
specifier: 7.7.2
version: 7.7.2
+ serialize-error:
+ specifier: ^12.0.0
+ version: 12.0.0
simplebar:
specifier: 6.3.1
version: 6.3.1
@@ -282,14 +285,14 @@ importers:
specifier: ^11.2.0
version: 11.2.0
'@electron-forge/cli':
- specifier: 7.8.1
- version: 7.8.1(bluebird@3.7.2)(encoding@0.1.13)
+ specifier: 7.10.2
+ version: 7.10.2(@swc/core@1.12.0)(bluebird@3.7.2)(encoding@0.1.13)(esbuild@0.25.2)
'@electron-forge/plugin-auto-unpack-natives':
- specifier: 7.8.1
- version: 7.8.1(bluebird@3.7.2)
+ specifier: 7.10.2
+ version: 7.10.2(bluebird@3.7.2)
'@electron-forge/plugin-vite':
- specifier: ^7.9.0
- version: 7.9.0(bluebird@3.7.2)
+ specifier: 7.10.2
+ version: 7.10.2(bluebird@3.7.2)
'@electron/rebuild':
specifier: ^4.0.1
version: 4.0.1
@@ -424,26 +427,26 @@ importers:
version: 3.2.3(@types/node@22.13.0)(@vitest/ui@3.2.3)(jiti@2.4.2)(jsdom@26.1.0)(terser@5.18.2)(yaml@2.8.0)
optionalDependencies:
'@electron-forge/maker-deb':
- specifier: 7.8.1
- version: 7.8.1(bluebird@3.7.2)
+ specifier: 7.10.2
+ version: 7.10.2(bluebird@3.7.2)
'@electron-forge/maker-flatpak':
- specifier: 7.8.1
- version: 7.8.1(bluebird@3.7.2)
+ specifier: 7.10.2
+ version: 7.10.2(bluebird@3.7.2)
'@electron-forge/maker-rpm':
- specifier: 7.8.1
- version: 7.8.1(bluebird@3.7.2)
+ specifier: 7.10.2
+ version: 7.10.2(bluebird@3.7.2)
'@electron-forge/maker-snap':
- specifier: 7.8.1
- version: 7.8.1(bluebird@3.7.2)
+ specifier: 7.10.2
+ version: 7.10.2(bluebird@3.7.2)
'@electron-forge/maker-squirrel':
- specifier: 7.8.1
- version: 7.8.1(bluebird@3.7.2)
+ specifier: 7.10.2
+ version: 7.10.2(bluebird@3.7.2)
'@electron-forge/maker-zip':
- specifier: 7.8.1
- version: 7.8.1(bluebird@3.7.2)
+ specifier: 7.10.2
+ version: 7.10.2(bluebird@3.7.2)
'@reforged/maker-appimage':
- specifier: ^5.0.0
- version: 5.0.0(bluebird@3.7.2)
+ specifier: 5.1.0
+ version: 5.1.0(bluebird@3.7.2)
packages:
@@ -920,61 +923,51 @@ packages:
resolution: {integrity: sha512-ZeIh6qMPWLBBifDtU0XadpK36b4WoaTqCOt0rWKfoTjq1RAt78EgqETWp43Dbr6et/HvTgYdoWF0ZNEu2FJFFA==}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@dprint/linux-arm64-glibc@0.50.0':
resolution: {integrity: sha512-EL0+uMSdj/n+cZOP9ZO8ndvjmtOSWXNsMHKdAAaTG0+EjH9M9YKXD6kopP6PKOR5pJuiyHCRpVKJ4xoD4adfpQ==}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@dprint/linux-arm64-musl@0.49.1':
resolution: {integrity: sha512-/nuRyx+TykN6MqhlSCRs/t3o1XXlikiwTc9emWdzMeLGllYvJrcht9gRJ1/q1SqwCFhzgnD9H7roxxfji1tc+Q==}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@dprint/linux-arm64-musl@0.50.0':
resolution: {integrity: sha512-bzyYxKtFw/hYAA+7lWQGQGo2YFPnH7Ql9uWxxWqiGaWVPU66K9WQt0RUEqu1hQBrCk9mMz3jx5l4oKWQ/Dc0fw==}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@dprint/linux-riscv64-glibc@0.49.1':
resolution: {integrity: sha512-RHBqrnvGO+xW4Oh0QuToBqWtkXMcfjqa1TqbBFF03yopFzZA2oRKX83PhjTWgd/IglaOns0BgmaLJy/JBSxOfQ==}
cpu: [riscv64]
os: [linux]
- libc: [glibc]
'@dprint/linux-riscv64-glibc@0.50.0':
resolution: {integrity: sha512-ElFqmKs96NyVXWqd2SJGJGtyVmUWNiLUyaImEzL7XZRmpoJG+Ky7SryhccMQU0ENtQmY0CVgZipLZ1SqhIoluA==}
cpu: [riscv64]
os: [linux]
- libc: [glibc]
'@dprint/linux-x64-glibc@0.49.1':
resolution: {integrity: sha512-MjFE894mIQXOKBencuakKyzAI4KcDe/p0Y9lRp9YSw/FneR4QWH9VBH90h8fRxcIlWMArjFFJJAtsBnn5qgxeg==}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@dprint/linux-x64-glibc@0.50.0':
resolution: {integrity: sha512-Kim8TtCdpCQUNqF2D96vunuonYy6tPfp/AQblSVA4ADChVyFLGfPaQIECpGAAKxXnIG2SX5JRQP7nB/4JgPNbA==}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@dprint/linux-x64-musl@0.49.1':
resolution: {integrity: sha512-CvGBWOksHgrL1uzYqtPFvZz0+E82BzgoCIEHJeuYaveEn37qWZS5jqoCm/vz6BfoivE1dVuyyOT78Begj9KxkQ==}
cpu: [x64]
os: [linux]
- libc: [musl]
'@dprint/linux-x64-musl@0.50.0':
resolution: {integrity: sha512-ChZf0BnS3S6BIfqAPgQKqEh/7vgD1xc0MpcFcTrvkVQHuSdCQu1XiqUN12agzxB+Y5Ml9exgzP8lYgNza7iXvw==}
cpu: [x64]
os: [linux]
- libc: [musl]
'@dprint/markdown@0.15.3':
resolution: {integrity: sha512-QCpvOQZtvq8HNbUobh9lAW5V4PrEncpfKLltxgM/DjLymDHUQ5EOnHUHaBlKu0ze+xtApBFnJpZS2xhjoNpj9g==}
@@ -1016,115 +1009,103 @@ packages:
peerDependencies:
react: '19'
- '@electron-forge/cli@7.8.1':
- resolution: {integrity: sha512-QI3EShutfq9Y+2TWWrPjm4JZM3eSAKzoQvRZdVhAfVpUbyJ8K23VqJShg3kGKlPf9BXHAGvE+8LyH5s2yDr1qA==}
+ '@electron-forge/cli@7.10.2':
+ resolution: {integrity: sha512-X1RtS5IqNgzGDS2rr1q0Y74wU/m3DbU4vSgllNun1ZQv1BfMpDcKLhnKi3aeetoA0huLTpMVU9eWJ7bziI9fxA==}
engines: {node: '>= 16.4.0'}
hasBin: true
- '@electron-forge/core-utils@7.8.1':
- resolution: {integrity: sha512-mRoPLDNZgmjyOURE/K0D3Op53XGFmFRgfIvFC7c9S/BqsRpovVblrqI4XxPRdNmH9dvhd8On9gGz+XIYAKD3aQ==}
+ '@electron-forge/core-utils@7.10.2':
+ resolution: {integrity: sha512-JXrk2hWR4q8KgZFABpojjuqql3tYeVIH6qmtbkNEkZEQq7YIxajJBCct7J7bWfNQTmHotsQ3k5KLknhyhTaBMw==}
engines: {node: '>= 16.4.0'}
- '@electron-forge/core@7.8.1':
- resolution: {integrity: sha512-jkh0QPW5p0zmruu1E8+2XNufc4UMxy13WLJcm7hn9jbaXKLkMbKuEvhrN1tH/9uGp1mhr/t8sC4N67gP+gS87w==}
+ '@electron-forge/core@7.10.2':
+ resolution: {integrity: sha512-HAIuOtpOfGjA0cd55tbEV2gAv+A7tSZg9bonmVDYFEe6dBgbLk8a3+/1fJUdWW8fyFkg1wa8zK7pjP751bAXsA==}
engines: {node: '>= 16.4.0'}
- '@electron-forge/maker-base@7.6.1':
- resolution: {integrity: sha512-kA6k0z4fFbqfjV++bbYVC46TckiqyqIo/gTW/QexsT6xlutXUbnNevhoRPVfGigftSAjE6T26DwTogC9hNDkwg==}
+ '@electron-forge/maker-base@7.10.2':
+ resolution: {integrity: sha512-1QN4qnPVTjo+qWYG+s0kYv7XcuIowsPVvbl718FgJUcvkxyRjUA6kWHjFxRvdV6g7Sa2PzZBF+/Mrjpws1lehQ==}
engines: {node: '>= 16.4.0'}
'@electron-forge/maker-base@7.8.1':
resolution: {integrity: sha512-GUZqschGuEBzSzE0bMeDip65IDds48DZXzldlRwQ+85SYVA6RMU2AwDDqx3YiYsvP2OuxKruuqIJZtOF5ps4FQ==}
engines: {node: '>= 16.4.0'}
- '@electron-forge/maker-deb@7.8.1':
- resolution: {integrity: sha512-tjjeesQtCP5Xht1X7gl4+K9bwoETPmQfBkOVAY/FZIxPj40uQh/hOUtLX2tYENNGNVZ1ryDYRs8TuPi+I41Vfw==}
+ '@electron-forge/maker-deb@7.10.2':
+ resolution: {integrity: sha512-4MPr9NW5UbEUbf9geZn5R/0O/QVIiy2EgUXOYOeKkA7oR8U6I1I3+BytYFHYcxbY6+PGhi1H1VTLJLITbHGVWw==}
engines: {node: '>= 16.4.0'}
- '@electron-forge/maker-flatpak@7.8.1':
- resolution: {integrity: sha512-lp1R+G+3dfFJUMHUMIeDzhNhD1NsRcVlGjVUTP8NAaPEkcK8aSclXBEEHcKCIOsknhHIN+Odhpf3zAqTvAWdwg==}
+ '@electron-forge/maker-flatpak@7.10.2':
+ resolution: {integrity: sha512-LldkYGkIhri99+HqetGjNzi2cdXy32o5uLlr7fDLoiegm8WAkvvWxFTLdSDS1RP94f6PVOKR9KHqPauu5GaIYw==}
engines: {node: '>= 16.4.0'}
- '@electron-forge/maker-rpm@7.8.1':
- resolution: {integrity: sha512-TF6wylft3BHkw9zdHcxmjEPBZYgTIc0jE31skFnMEQ/aExbNRiNaCZvsXy+7ptTWZxhxUKRc9KHhLFRMCmOK8g==}
+ '@electron-forge/maker-rpm@7.10.2':
+ resolution: {integrity: sha512-LQoeYzbY/z1yuBBA+bNutCJmhCA4NcXUbFO4OTqsIX8B6y1zNTYZT4JEuhoK7eBsP4/Rz6u/JnNp0XOyjftOUQ==}
engines: {node: '>= 16.4.0'}
- '@electron-forge/maker-snap@7.8.1':
- resolution: {integrity: sha512-OXs51p2SIoBYr+2Z2CG6QwebHntIzZzlATHqloUngKLqnNjAiWRWSFyr9j/ATM71PWETxqAX9YGXr32EPIlqgw==}
+ '@electron-forge/maker-snap@7.10.2':
+ resolution: {integrity: sha512-kl9D65qNFD7Fc2npFpXQYHt7fhnvU7yEgECS52Ytw+Sy0KBdPm+mKX9xvMnW6pvG0VO7xLirpBkPEm8kpc/I1g==}
engines: {node: '>= 16.4.0'}
- '@electron-forge/maker-squirrel@7.8.1':
- resolution: {integrity: sha512-qT1PMvT7ALF0ONOkxlA0oc0PiFuKCAKgoMPoxYo9gGOqFvnAb+TBcnLxflQ4ashE/ZkrHpykr4LcDJxqythQTA==}
+ '@electron-forge/maker-squirrel@7.10.2':
+ resolution: {integrity: sha512-Y5EhNSBXf4a7qcq+BK/x5qVDlQ1Gez5V+arUpDvVxf1zwvsB1aSyAjmoBrOKGYD9A5pJzjkMWMDw95MStl1W4A==}
engines: {node: '>= 16.4.0'}
- '@electron-forge/maker-zip@7.8.1':
- resolution: {integrity: sha512-unIxEoV1lnK4BLVqCy3L2y897fTyg8nKY1WT4rrpv0MUKnQG4qmigDfST5zZNNHHaulEn/ElAic2GEiP7d6bhQ==}
+ '@electron-forge/maker-zip@7.10.2':
+ resolution: {integrity: sha512-APRqVPM+O1rj4O7sk5f8tqJpS5UgxcUJEsCnXN4JRpdRvsOlMopzYZdazlCLH9l7S+r4ZKirjtMluIGeYq8YOg==}
engines: {node: '>= 16.4.0'}
- '@electron-forge/plugin-auto-unpack-natives@7.8.1':
- resolution: {integrity: sha512-4URAgWX9qqqKe6Bfad0VmpFRrwINYMODfKGd2nFQrfHxmBtdpXnsWlLwVGE/wGssIQaTMI5bWQ6F2RNeXTgnhA==}
+ '@electron-forge/plugin-auto-unpack-natives@7.10.2':
+ resolution: {integrity: sha512-uQnahm1DECwqI8hBC7PKccyfovY/YqHNz8de3OxyjQDmwsqQfCA8Ucyh1E9n4NMEpw6Co8KLn+qF2BuIOsftLA==}
engines: {node: '>= 16.4.0'}
- '@electron-forge/plugin-base@7.8.1':
- resolution: {integrity: sha512-iCZC2d7CbsZ9l6j5d+KPIiyQx0U1QBfWAbKnnQhWCSizjcrZ7A9V4sMFZeTO6+PVm48b/r9GFPm+slpgZtYQLg==}
+ '@electron-forge/plugin-base@7.10.2':
+ resolution: {integrity: sha512-+4YLmkLZxvS6JFXYNI4dHt8Il8iIvwk2o6lCJGwNysOUq2KOZ3Wu1He4Ko8HhKcO1VWbFvslbh57oQn963Aryw==}
engines: {node: '>= 16.4.0'}
- '@electron-forge/plugin-base@7.9.0':
- resolution: {integrity: sha512-2cnShgfes0sqH7A3+54fWhfJEfU++1OC2HE50a4sWtWEDwyWLGbwW7tp9BgSXrvIexO2AGKHQ1pKIjpZYVC0fA==}
+ '@electron-forge/plugin-vite@7.10.2':
+ resolution: {integrity: sha512-aHotwaVlbSwVDb+Z+JdU6cMYhestt8ncmXKv4Uwm7of/gWAdvS7o/ohQVWkjXhzSidriCTwFMRz4jELJbnkNeg==}
engines: {node: '>= 16.4.0'}
- '@electron-forge/plugin-vite@7.9.0':
- resolution: {integrity: sha512-e2fRFsG4VPtIDiELF4Q7Y+WvFHfzQVk0dO9vo9u/giaFEoTGOHq09+uH8tLZqsQpRfOO3mAszKEee9z/E78poA==}
+ '@electron-forge/publisher-base@7.10.2':
+ resolution: {integrity: sha512-2k2VOY0wOoAgQoQXn/u3EJ2Ka2v363+wC/+zUMTWGeRHW8pRwX84WX2SpsTttRzbsqAEMJYw5FAzgMBEQUTfpg==}
engines: {node: '>= 16.4.0'}
- '@electron-forge/publisher-base@7.8.1':
- resolution: {integrity: sha512-z2C+C4pcFxyCXIFwXGDcxhU8qtVUPZa3sPL6tH5RuMxJi77768chLw2quDWk2/dfupcSELXcOMYCs7aLysCzeQ==}
- engines: {node: '>= 16.4.0'}
-
- '@electron-forge/shared-types@7.6.1':
- resolution: {integrity: sha512-i6VdZGG8SYEBirpk+FP7bEMYtCNf9wBkK81IcPco8LP0KbsvgR8y7aUSVxG8DLoVwYB5yr0N9MYXOfNp1gkQ7A==}
+ '@electron-forge/shared-types@7.10.2':
+ resolution: {integrity: sha512-e2pd9RsdbKwsNf6UtKoolmJGy92Nc0/XO4SI91doV8cM954hM2XSYz3VHoqXebMFAF1JDfXoEUt6UCRbEDgMgw==}
engines: {node: '>= 16.4.0'}
'@electron-forge/shared-types@7.8.1':
resolution: {integrity: sha512-guLyGjIISKQQRWHX+ugmcjIOjn2q/BEzCo3ioJXFowxiFwmZw/oCZ2KlPig/t6dMqgUrHTH5W/F0WKu0EY4M+Q==}
engines: {node: '>= 16.4.0'}
- '@electron-forge/shared-types@7.9.0':
- resolution: {integrity: sha512-6jZF+zq3SYMnweQpgr5fwlSgOd2yOZ5qlfz/CgXyVljiv0e0UThzpOjfTLuwuVgZX7a60xV+h0mg1h82Glu3wQ==}
+ '@electron-forge/template-base@7.10.2':
+ resolution: {integrity: sha512-D9DbEx3rtikIhUyn4tcz2pJqHNU/+FXKNnzSvmrJoJ9LusR3C42OU9GtbU8oT3nawpnCGgPFIOGXrzexFPp6DA==}
engines: {node: '>= 16.4.0'}
- '@electron-forge/template-base@7.8.1':
- resolution: {integrity: sha512-k8jEUr0zWFWb16ZGho+Es2OFeKkcbTgbC6mcH4eNyF/sumh/4XZMcwRtX1i7EiZAYiL9sVxyI6KVwGu254g+0g==}
+ '@electron-forge/template-vite-typescript@7.10.2':
+ resolution: {integrity: sha512-df7rpxxIOIyZn0RfQ1GIlLW7dXhxkerc9uZ3ozO4C7zfvip3z0Mg+wS1synktPfr4WISaPktIdnj3mVu6Uu7Mw==}
engines: {node: '>= 16.4.0'}
- '@electron-forge/template-vite-typescript@7.8.1':
- resolution: {integrity: sha512-CccQhwUjZcc6svzuOi3BtbDal591DzyX2J5GPa6mwVutDP8EMtqJL1VyOHdcWO/7XjI6GNAD0fiXySOJiUAECA==}
+ '@electron-forge/template-vite@7.10.2':
+ resolution: {integrity: sha512-hR9HBOM902yq7zhFl8bO3w5ufMgitdd5ZwDzAdKITFh2ttZemHy9ha5S0K+R+4GoXHz8t7hUTHk8+iPy09qrpA==}
engines: {node: '>= 16.4.0'}
- '@electron-forge/template-vite@7.8.1':
- resolution: {integrity: sha512-qzSlJaBYYqQAbBdLk4DqAE3HCNz4yXbpkb+VC74ddL4JGwPdPU57DjCthr6YetKJ2FsOVy9ipovA8HX5UbXpAg==}
+ '@electron-forge/template-webpack-typescript@7.10.2':
+ resolution: {integrity: sha512-JtrLUAFbxxWJ1kU7b8MNyL5SO9/rY5UeNz1b9hvMvilW8GxyMWUen58dafgdnx3OpKLNZnhOOhgRagNppEzJOA==}
engines: {node: '>= 16.4.0'}
- '@electron-forge/template-webpack-typescript@7.8.1':
- resolution: {integrity: sha512-h922E+6zWwym1RT6WKD79BLTc4H8YxEMJ7wPWkBX59kw/exsTB/KFdiJq6r82ON5jSJ+Q8sDGqSmDWdyCfo+Gg==}
+ '@electron-forge/template-webpack@7.10.2':
+ resolution: {integrity: sha512-VIUXA+XHM5SLjg7fIpOOmBsgi0LstkjrEz4gUzVL0AaITM7e+BCziIHld1ceXLbQ1FnKtrUGnQ9X/cHYxYvhHg==}
engines: {node: '>= 16.4.0'}
- '@electron-forge/template-webpack@7.8.1':
- resolution: {integrity: sha512-DA77o9kTCHrq+W211pyNP49DyAt0d1mzMp2gisyNz7a+iKvlv2DsMAeRieLoCQ44akb/z8ZsL0YLteSjKLy4AA==}
- engines: {node: '>= 16.4.0'}
-
- '@electron-forge/tracer@7.6.1':
- resolution: {integrity: sha512-nZzVzXT4xdueWYoSbgStS5LfcifW/e/WJj9VOt6xYpFxYOsQHpLkkCAc6nH0gxn+60kiU4FMU0p2kSQ2eQhWuA==}
+ '@electron-forge/tracer@7.10.2':
+ resolution: {integrity: sha512-jhLLQbttfZViSOYn/3SJc8HML+jNZAytPVJwgGGd3coUiFysWJ2Xald99iqOiouPAhIigBfNPxQb/q/EbcDu4g==}
engines: {node: '>= 14.17.5'}
'@electron-forge/tracer@7.8.1':
resolution: {integrity: sha512-r2i7aHVp2fylGQSPDw3aTcdNfVX9cpL1iL2MKHrCRNwgrfR+nryGYg434T745GGm1rNQIv5Egdkh5G9xf00oWA==}
engines: {node: '>= 14.17.5'}
- '@electron-forge/tracer@7.9.0':
- resolution: {integrity: sha512-7itsjW1WJQADg7Ly61ggI5CCRt+QDVx3HOZC1w69jMUtnipKyPRCbvTBf1oplsNqbIzZxceXdfex6W53YNehvA==}
- engines: {node: '>= 14.17.5'}
-
'@electron/asar@3.2.17':
resolution: {integrity: sha512-OcWImUI686w8LkghQj9R2ynZ2ME693Ek6L1SiaAgqGKzBaTIZw3fHDqN82Rcl+EU1Gm9EgkJ5KLIY/q5DCRbbA==}
engines: {node: '>=10.12.0'}
@@ -1470,6 +1451,66 @@ packages:
resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==}
engines: {node: '>=18.18'}
+ '@inquirer/checkbox@3.0.1':
+ resolution: {integrity: sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==}
+ engines: {node: '>=18'}
+
+ '@inquirer/confirm@4.0.1':
+ resolution: {integrity: sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==}
+ engines: {node: '>=18'}
+
+ '@inquirer/core@9.2.1':
+ resolution: {integrity: sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==}
+ engines: {node: '>=18'}
+
+ '@inquirer/editor@3.0.1':
+ resolution: {integrity: sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==}
+ engines: {node: '>=18'}
+
+ '@inquirer/expand@3.0.1':
+ resolution: {integrity: sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==}
+ engines: {node: '>=18'}
+
+ '@inquirer/figures@1.0.13':
+ resolution: {integrity: sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==}
+ engines: {node: '>=18'}
+
+ '@inquirer/input@3.0.1':
+ resolution: {integrity: sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==}
+ engines: {node: '>=18'}
+
+ '@inquirer/number@2.0.1':
+ resolution: {integrity: sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==}
+ engines: {node: '>=18'}
+
+ '@inquirer/password@3.0.1':
+ resolution: {integrity: sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==}
+ engines: {node: '>=18'}
+
+ '@inquirer/prompts@6.0.1':
+ resolution: {integrity: sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==}
+ engines: {node: '>=18'}
+
+ '@inquirer/rawlist@3.0.1':
+ resolution: {integrity: sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==}
+ engines: {node: '>=18'}
+
+ '@inquirer/search@2.0.1':
+ resolution: {integrity: sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==}
+ engines: {node: '>=18'}
+
+ '@inquirer/select@3.0.1':
+ resolution: {integrity: sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==}
+ engines: {node: '>=18'}
+
+ '@inquirer/type@1.5.5':
+ resolution: {integrity: sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==}
+ engines: {node: '>=18'}
+
+ '@inquirer/type@2.0.0':
+ resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==}
+ engines: {node: '>=18'}
+
'@inversifyjs/common@1.4.0':
resolution: {integrity: sha512-qfRJ/3iOlCL/VfJq8+4o5X4oA14cZSBbpAmHsYj8EsIit1xDndoOl0xKOyglKtQD4u4gdNVxMHx4RWARk/I4QA==}
@@ -1645,6 +1686,12 @@ packages:
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
+ '@listr2/prompt-adapter-inquirer@2.0.22':
+ resolution: {integrity: sha512-hV36ZoY+xKL6pYOt1nPNnkciFkn89KZwqLhAFzJvYysAvL5uBQdiADZx/8bIDXIukzzwG0QlPYolgMzQUtKgpQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ '@inquirer/prompts': '>= 3 < 8'
+
'@malept/cross-spawn-promise@1.1.1':
resolution: {integrity: sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==}
engines: {node: '>= 10'}
@@ -1871,12 +1918,12 @@ packages:
'@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
- '@reforged/maker-appimage@5.0.0':
- resolution: {integrity: sha512-25nli9nt5MVMRladnoJ3uP5W+2KpND5mzA36rc/Duj/R71oGcOj3t9Uoc/dDmaED8afAEeaSYpVE7VPPe9T54A==}
+ '@reforged/maker-appimage@5.1.0':
+ resolution: {integrity: sha512-N4IkxfaquzgoH/zEC2uiR4smndDhgvBtwfAYnuVM5F2Lpy5IGSDakqvtPfMCy1r645qQRMYdh3gEQu5cvIro4g==}
engines: {node: '>=19.0.0 || ^18.11.0'}
- '@reforged/maker-types@1.0.1':
- resolution: {integrity: sha512-gjLr6O7rS8XzjbqCEo/BxT4mrevWuYKdMzc0uO6dNcWDXinfhJVHT3aEZmtMyn1nx+ZbffzpCFPgGNYZtwGXxQ==}
+ '@reforged/maker-types@2.0.0':
+ resolution: {integrity: sha512-Vc8xblKLfo+CP7CE/5Yshtyo6NwBkE4ZW00boCI50yePHG2wN04w1qrFlSxAmuau70J3alMhUrByeMrddlxAyw==}
'@rjsf/core@6.0.0-beta.8':
resolution: {integrity: sha512-pfbnwOLospssSrZ9YG6T4yVHLRnIyveX0mcsI+q4pJIUAgVUM2a4PYnh+go4OjLGzbY2Tq56kb3Bw0CXB/jEqw==}
@@ -1955,67 +2002,56 @@ packages:
resolution: {integrity: sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==}
cpu: [arm]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.43.0':
resolution: {integrity: sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==}
cpu: [arm]
os: [linux]
- libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.43.0':
resolution: {integrity: sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.43.0':
resolution: {integrity: sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@rollup/rollup-linux-loongarch64-gnu@4.43.0':
resolution: {integrity: sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==}
cpu: [loong64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-powerpc64le-gnu@4.43.0':
resolution: {integrity: sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==}
cpu: [ppc64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.43.0':
resolution: {integrity: sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==}
cpu: [riscv64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.43.0':
resolution: {integrity: sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==}
cpu: [riscv64]
os: [linux]
- libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.43.0':
resolution: {integrity: sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==}
cpu: [s390x]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.43.0':
resolution: {integrity: sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.43.0':
resolution: {integrity: sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==}
cpu: [x64]
os: [linux]
- libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.43.0':
resolution: {integrity: sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==}
@@ -2080,28 +2116,24 @@ packages:
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@swc/core-linux-arm64-musl@1.12.0':
resolution: {integrity: sha512-OeFHz/5Hl9v75J9TYA5jQxNIYAZMqaiPpd9dYSTK2Xyqa/ZGgTtNyPhIwVfxx+9mHBf6+9c1mTlXUtACMtHmaQ==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@swc/core-linux-x64-gnu@1.12.0':
resolution: {integrity: sha512-ltIvqNi7H0c5pRawyqjeYSKEIfZP4vv/datT3mwT6BW7muJtd1+KIDCPFLMIQ4wm/h76YQwPocsin3fzmnFdNA==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@swc/core-linux-x64-musl@1.12.0':
resolution: {integrity: sha512-Z/DhpjehaTK0uf+MhNB7mV9SuewpGs3P/q9/8+UsJeYoFr7yuOoPbAvrD6AqZkf6Bh7MRZ5OtG+KQgG5L+goiA==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
- libc: [musl]
'@swc/core-win32-arm64-msvc@1.12.0':
resolution: {integrity: sha512-wHnvbfHIh2gfSbvuFT7qP97YCMUDh+fuiso+pcC6ug8IsMxuViNapHET4o0ZdFNWHhXJ7/s0e6w7mkOalsqQiQ==}
@@ -2300,6 +2332,9 @@ packages:
'@types/minimatch@5.1.2':
resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
+ '@types/mute-stream@0.0.4':
+ resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==}
+
'@types/node@16.9.1':
resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==}
@@ -2361,6 +2396,9 @@ packages:
'@types/uuid@9.0.8':
resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
+ '@types/wrap-ansi@3.0.0':
+ resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==}
+
'@types/yauzl@2.10.0':
resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==}
@@ -2554,49 +2592,41 @@ packages:
resolution: {integrity: sha512-uNpLKxlDF+NF6aUztbAVhhFSF65zf/6QEfk5NifUgYFbpBObzvMnl2ydEsXV96spwPcmeNTpG9byvq+Twwd3HQ==}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.7.13':
resolution: {integrity: sha512-mEFL6q7vtxA6YJ9sLbxCnKOBynOvClVOcqwUErmaCxA94hgP11rlstouySxJCGeFAb8KfUX9mui82waYrqoBlQ==}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.7.13':
resolution: {integrity: sha512-MjJaNk8HK3rCOIPS6AQPJXlrDfG1LaePum+CZddHZygPqDNZyVrVdWTadT+U51vIx5QOdEE0oXcgTY+7VYsU1g==}
cpu: [ppc64]
os: [linux]
- libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.7.13':
resolution: {integrity: sha512-9gAuT1+ed2eIuOXHSu4SdJOe7SUEzPTpOTEuTjGePvMEoWHywY5pvlcY7xMn3d8rhKHpwMzEhl8F8Oy+rkudzA==}
cpu: [riscv64]
os: [linux]
- libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.7.13':
resolution: {integrity: sha512-CNrJythJN9jC8SIJGoawebYylzGNJuWAWTKxxxx5Fr3DGEXbex/We4U7N4u6/dQAK3cLVOuAE/9a4D2JH35JIA==}
cpu: [riscv64]
os: [linux]
- libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.7.13':
resolution: {integrity: sha512-J0MVXXPvM2Bv+f+gzOZHLHEmXUJNKwJqkfMDTwE763w/tD+OA7UlTMLQihrcYRXwW5jZ8nbM2cEWTeFsTiH2JQ==}
cpu: [s390x]
os: [linux]
- libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.7.13':
resolution: {integrity: sha512-Ii2WhtIpeWUe6XG/YhPUX3JNL3PiyXe56PJzqAYDUyB0gctkk/nngpuPnNKlLMcN9FID0T39mIJPhA6YpRcGDQ==}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.7.13':
resolution: {integrity: sha512-8F5E9EhtGYkfEM1OhyVgq76+SnMF5NfZS4v5Rq9JlfuqPnqXWgUjg903hxnG54PQr4I3jmG5bEeT77pGAA3Vvg==}
cpu: [x64]
os: [linux]
- libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.7.13':
resolution: {integrity: sha512-7RXGTyDtyR/5o1FlBcjEaQQmQ2rKvu5Jq0Uhvce3PsbreZ61M4LQ5Mey2OMomIq4opphAkfDdm/lkHhWJNKNrw==}
@@ -2667,6 +2697,9 @@ packages:
'@vitest/utils@3.2.3':
resolution: {integrity: sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==}
+ '@vscode/sudo-prompt@9.3.1':
+ resolution: {integrity: sha512-9ORTwwS74VaTn38tNbQhsA5U44zkJfcb0BdTSyyG6frP4e8KMtHuTXYmwefe5dpL8XB1aGSIVTaLjD3BbWb5iA==}
+
'@webassemblyjs/ast@1.11.6':
resolution: {integrity: sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==}
@@ -2817,6 +2850,10 @@ packages:
resolution: {integrity: sha512-iriwDyAqedYgi9YTpVwJbE/TQJwelclpVFfDgNBfhdIhIzAdKo+Kitwinn+krx9tjDsnzRt3tqTQdbJ0E6OwNw==}
engines: {node: '>= 14.0.0'}
+ ansi-escapes@4.3.2:
+ resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
+ engines: {node: '>=8'}
+
ansi-escapes@5.0.0:
resolution: {integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==}
engines: {node: '>=12'}
@@ -3017,10 +3054,6 @@ packages:
brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
- braces@3.0.2:
- resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
- engines: {node: '>=8'}
-
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
@@ -3118,6 +3151,9 @@ packages:
resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+ chardet@0.7.0:
+ resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
+
check-error@2.1.1:
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
engines: {node: '>= 16'}
@@ -3185,6 +3221,10 @@ packages:
resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ cli-width@4.1.0:
+ resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
+ engines: {node: '>= 12'}
+
cliui@7.0.4:
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
@@ -3561,8 +3601,8 @@ packages:
engines: {node: '>= 10.0'}
hasBin: true
- electron-ipc-cat@2.0.1:
- resolution: {integrity: sha512-lPGLv//HQLeC+9SdhC6tx5tgv8wir/Ws5GIAcfHtbIEN47FARBbQsNaZks61dzveS4zb4LxbRzOV3mweyoTUDg==}
+ electron-ipc-cat@2.1.1:
+ resolution: {integrity: sha512-1cDTPZbPBDUKlpE0w+W5nhIqpTvy2qXu3QB8i5ohVLG5cptD2NQ9wCLE1uIrWxdpT01/TQ4fdCfK/RqGQw9AGQ==}
peerDependencies:
electron: '>= 13.0.0'
rxjs: '>= 7.5.0'
@@ -4016,6 +4056,10 @@ packages:
resolution: {integrity: sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==}
engines: {node: '>=4'}
+ external-editor@3.1.0:
+ resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
+ engines: {node: '>=4'}
+
extract-files@11.0.0:
resolution: {integrity: sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==}
engines: {node: ^12.20 || >= 14.13}
@@ -4031,10 +4075,6 @@ packages:
fast-diff@1.3.0:
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
- fast-glob@3.3.2:
- resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
- engines: {node: '>=8.6.0'}
-
fast-glob@3.3.3:
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
engines: {node: '>=8.6.0'}
@@ -4115,10 +4155,6 @@ packages:
resolution: {integrity: sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==}
engines: {node: '>=8'}
- fill-range@7.0.1:
- resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
- engines: {node: '>=8'}
-
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
@@ -4479,6 +4515,10 @@ packages:
typescript:
optional: true
+ iconv-lite@0.4.24:
+ resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
+ engines: {node: '>=0.10.0'}
+
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
@@ -5118,8 +5158,8 @@ packages:
resolution: {integrity: sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==}
engines: {node: '>=6'}
- memize@2.1.0:
- resolution: {integrity: sha512-yywVJy8ctVlN5lNPxsep5urnZ6TTclwPEyigM9M3Bi8vseJBOfqNrGWN/r8NzuIt3PovM323W04blJfGQfQSVg==}
+ memize@2.1.1:
+ resolution: {integrity: sha512-8Nl+i9S5D6KXnruM03Jgjb+LwSupvR13WBr4hJegaaEyobvowCVupi79y2WSiWvO1mzBWxPwEYE5feCe8vyA5w==}
memoize-one@4.0.3:
resolution: {integrity: sha512-QmpUu4KqDmX0plH4u+tf0riMc1KHE1+lw95cMrLlXQAFOx/xnBtwhZ52XJxd9X2O6kwKBqX32kmhbhlobD0cuw==}
@@ -5140,10 +5180,6 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
- micromatch@4.0.5:
- resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
- engines: {node: '>=8.6'}
-
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
@@ -5302,6 +5338,10 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+ mute-stream@1.0.0:
+ resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==}
+ engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
@@ -5515,6 +5555,10 @@ packages:
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
engines: {node: '>=10'}
+ os-tmpdir@1.0.2:
+ resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
+ engines: {node: '>=0.10.0'}
+
own-keys@1.0.1:
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
engines: {node: '>= 0.4'}
@@ -6189,9 +6233,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
- serialize-error@11.0.0:
- resolution: {integrity: sha512-YKrURWDqcT3VGX/s/pCwaWtpfJEEaEw5Y4gAnQDku92b/HjVj4r4UhA5QrMVMFotymK2wIWs5xthny5SMFu7Vw==}
- engines: {node: '>=14.16'}
+ serialize-error@12.0.0:
+ resolution: {integrity: sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw==}
+ engines: {node: '>=18'}
serialize-error@7.0.1:
resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==}
@@ -6268,6 +6312,10 @@ packages:
resolution: {integrity: sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==}
engines: {node: '>=14'}
+ signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+
simple-concat@1.0.1:
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
@@ -6526,10 +6574,6 @@ packages:
stylis@4.3.2:
resolution: {integrity: sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==}
- sudo-prompt@9.2.1:
- resolution: {integrity: sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==}
- deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
-
sumchecker@3.0.1:
resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==}
engines: {node: '>= 8.0'}
@@ -6624,7 +6668,6 @@ packages:
resolution: {integrity: sha512-BVVmqGUj47BC+wFZgNLU5qKuNJbTnWWTOB9mEELHtovdKaAchMF5DEXFvZEl0SOTjhF29ljecKhjla19vmoHPA==}
engines: {node: '>=0.8.2'}
hasBin: true
- bundledDependencies: []
tiny-case@1.0.3:
resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==}
@@ -6675,6 +6718,10 @@ packages:
tmp-promise@3.0.3:
resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==}
+ tmp@0.0.33:
+ resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
+ engines: {node: '>=0.6.0'}
+
tmp@0.2.3:
resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==}
engines: {node: '>=14.14'}
@@ -6775,6 +6822,10 @@ packages:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
engines: {node: '>=10'}
+ type-fest@0.21.3:
+ resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
+ engines: {node: '>=10'}
+
type-fest@1.4.0:
resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==}
engines: {node: '>=10'}
@@ -6876,6 +6927,11 @@ packages:
react-dom: ^16.13.1
styled-components: ^5.1.1
+ typescript@5.4.5:
+ resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
typescript@5.8.3:
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
engines: {node: '>=14.17'}
@@ -7216,6 +7272,10 @@ packages:
peerDependencies:
react: '>=16.8.0'
+ wrap-ansi@6.2.0:
+ resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
+ engines: {node: '>=8'}
+
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
@@ -7315,6 +7375,10 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
+ yoctocolors-cjs@2.1.3:
+ resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}
+ engines: {node: '>=18'}
+
yup@1.2.0:
resolution: {integrity: sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==}
@@ -7991,27 +8055,33 @@ snapshots:
'@babel/runtime': 7.27.1
react: 19.0.0
- '@electron-forge/cli@7.8.1(bluebird@3.7.2)(encoding@0.1.13)':
+ '@electron-forge/cli@7.10.2(@swc/core@1.12.0)(bluebird@3.7.2)(encoding@0.1.13)(esbuild@0.25.2)':
dependencies:
- '@electron-forge/core': 7.8.1(bluebird@3.7.2)(encoding@0.1.13)
- '@electron-forge/core-utils': 7.8.1(bluebird@3.7.2)
- '@electron-forge/shared-types': 7.8.1(bluebird@3.7.2)
+ '@electron-forge/core': 7.10.2(@swc/core@1.12.0)(bluebird@3.7.2)(encoding@0.1.13)(esbuild@0.25.2)
+ '@electron-forge/core-utils': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
'@electron/get': 3.0.0
+ '@inquirer/prompts': 6.0.1
+ '@listr2/prompt-adapter-inquirer': 2.0.22(@inquirer/prompts@6.0.1)
chalk: 4.1.2
commander: 11.1.0
- debug: 4.4.0
+ debug: 4.4.1
fs-extra: 10.1.0
listr2: 7.0.2
log-symbols: 4.1.0
semver: 7.7.2
transitivePeerDependencies:
+ - '@swc/core'
- bluebird
- encoding
+ - esbuild
- supports-color
+ - uglify-js
+ - webpack-cli
- '@electron-forge/core-utils@7.8.1(bluebird@3.7.2)':
+ '@electron-forge/core-utils@7.10.2(bluebird@3.7.2)':
dependencies:
- '@electron-forge/shared-types': 7.8.1(bluebird@3.7.2)
+ '@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
'@electron/rebuild': 3.7.2(bluebird@3.7.2)
'@malept/cross-spawn-promise': 2.0.0
chalk: 4.1.2
@@ -8019,31 +8089,33 @@ snapshots:
find-up: 5.0.0
fs-extra: 10.1.0
log-symbols: 4.1.0
+ parse-author: 2.0.0
semver: 7.7.2
transitivePeerDependencies:
- bluebird
- supports-color
- '@electron-forge/core@7.8.1(bluebird@3.7.2)(encoding@0.1.13)':
+ '@electron-forge/core@7.10.2(@swc/core@1.12.0)(bluebird@3.7.2)(encoding@0.1.13)(esbuild@0.25.2)':
dependencies:
- '@electron-forge/core-utils': 7.8.1(bluebird@3.7.2)
- '@electron-forge/maker-base': 7.8.1(bluebird@3.7.2)
- '@electron-forge/plugin-base': 7.8.1(bluebird@3.7.2)
- '@electron-forge/publisher-base': 7.8.1(bluebird@3.7.2)
- '@electron-forge/shared-types': 7.8.1(bluebird@3.7.2)
- '@electron-forge/template-base': 7.8.1(bluebird@3.7.2)
- '@electron-forge/template-vite': 7.8.1(bluebird@3.7.2)
- '@electron-forge/template-vite-typescript': 7.8.1(bluebird@3.7.2)
- '@electron-forge/template-webpack': 7.8.1(bluebird@3.7.2)
- '@electron-forge/template-webpack-typescript': 7.8.1(bluebird@3.7.2)
- '@electron-forge/tracer': 7.8.1
+ '@electron-forge/core-utils': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/maker-base': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/plugin-base': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/publisher-base': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/template-base': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/template-vite': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/template-vite-typescript': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/template-webpack': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/template-webpack-typescript': 7.10.2(@swc/core@1.12.0)(bluebird@3.7.2)(esbuild@0.25.2)
+ '@electron-forge/tracer': 7.10.2
'@electron/get': 3.0.0
'@electron/packager': 18.3.6
'@electron/rebuild': 3.7.2(bluebird@3.7.2)
'@malept/cross-spawn-promise': 2.0.0
+ '@vscode/sudo-prompt': 9.3.1
chalk: 4.1.2
debug: 4.4.1
- fast-glob: 3.3.2
+ fast-glob: 3.3.3
filenamify: 4.3.0
find-up: 5.0.0
fs-extra: 10.1.0
@@ -8058,22 +8130,24 @@ snapshots:
rechoir: 0.8.0
semver: 7.7.2
source-map-support: 0.5.21
- sudo-prompt: 9.2.1
username: 5.1.0
transitivePeerDependencies:
+ - '@swc/core'
- bluebird
- encoding
+ - esbuild
- supports-color
+ - uglify-js
+ - webpack-cli
- '@electron-forge/maker-base@7.6.1(bluebird@3.7.2)':
+ '@electron-forge/maker-base@7.10.2(bluebird@3.7.2)':
dependencies:
- '@electron-forge/shared-types': 7.6.1(bluebird@3.7.2)
+ '@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
fs-extra: 10.1.0
which: 2.0.2
transitivePeerDependencies:
- bluebird
- supports-color
- optional: true
'@electron-forge/maker-base@7.8.1(bluebird@3.7.2)':
dependencies:
@@ -8083,11 +8157,12 @@ snapshots:
transitivePeerDependencies:
- bluebird
- supports-color
+ optional: true
- '@electron-forge/maker-deb@7.8.1(bluebird@3.7.2)':
+ '@electron-forge/maker-deb@7.10.2(bluebird@3.7.2)':
dependencies:
- '@electron-forge/maker-base': 7.8.1(bluebird@3.7.2)
- '@electron-forge/shared-types': 7.8.1(bluebird@3.7.2)
+ '@electron-forge/maker-base': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
optionalDependencies:
electron-installer-debian: 3.2.0
transitivePeerDependencies:
@@ -8095,10 +8170,10 @@ snapshots:
- supports-color
optional: true
- '@electron-forge/maker-flatpak@7.8.1(bluebird@3.7.2)':
+ '@electron-forge/maker-flatpak@7.10.2(bluebird@3.7.2)':
dependencies:
- '@electron-forge/maker-base': 7.8.1(bluebird@3.7.2)
- '@electron-forge/shared-types': 7.8.1(bluebird@3.7.2)
+ '@electron-forge/maker-base': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
fs-extra: 10.1.0
optionalDependencies:
'@malept/electron-installer-flatpak': 0.11.4
@@ -8107,10 +8182,10 @@ snapshots:
- supports-color
optional: true
- '@electron-forge/maker-rpm@7.8.1(bluebird@3.7.2)':
+ '@electron-forge/maker-rpm@7.10.2(bluebird@3.7.2)':
dependencies:
- '@electron-forge/maker-base': 7.8.1(bluebird@3.7.2)
- '@electron-forge/shared-types': 7.8.1(bluebird@3.7.2)
+ '@electron-forge/maker-base': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
optionalDependencies:
electron-installer-redhat: 3.4.0
transitivePeerDependencies:
@@ -8118,10 +8193,10 @@ snapshots:
- supports-color
optional: true
- '@electron-forge/maker-snap@7.8.1(bluebird@3.7.2)':
+ '@electron-forge/maker-snap@7.10.2(bluebird@3.7.2)':
dependencies:
- '@electron-forge/maker-base': 7.8.1(bluebird@3.7.2)
- '@electron-forge/shared-types': 7.8.1(bluebird@3.7.2)
+ '@electron-forge/maker-base': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
optionalDependencies:
electron-installer-snap: 5.2.0
transitivePeerDependencies:
@@ -8129,10 +8204,10 @@ snapshots:
- supports-color
optional: true
- '@electron-forge/maker-squirrel@7.8.1(bluebird@3.7.2)':
+ '@electron-forge/maker-squirrel@7.10.2(bluebird@3.7.2)':
dependencies:
- '@electron-forge/maker-base': 7.8.1(bluebird@3.7.2)
- '@electron-forge/shared-types': 7.8.1(bluebird@3.7.2)
+ '@electron-forge/maker-base': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
fs-extra: 10.1.0
optionalDependencies:
electron-winstaller: 5.3.0
@@ -8141,10 +8216,10 @@ snapshots:
- supports-color
optional: true
- '@electron-forge/maker-zip@7.8.1(bluebird@3.7.2)':
+ '@electron-forge/maker-zip@7.10.2(bluebird@3.7.2)':
dependencies:
- '@electron-forge/maker-base': 7.8.1(bluebird@3.7.2)
- '@electron-forge/shared-types': 7.8.1(bluebird@3.7.2)
+ '@electron-forge/maker-base': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
cross-zip: 4.0.0
fs-extra: 10.1.0
got: 11.8.6
@@ -8153,32 +8228,25 @@ snapshots:
- supports-color
optional: true
- '@electron-forge/plugin-auto-unpack-natives@7.8.1(bluebird@3.7.2)':
+ '@electron-forge/plugin-auto-unpack-natives@7.10.2(bluebird@3.7.2)':
dependencies:
- '@electron-forge/plugin-base': 7.8.1(bluebird@3.7.2)
- '@electron-forge/shared-types': 7.8.1(bluebird@3.7.2)
+ '@electron-forge/plugin-base': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
transitivePeerDependencies:
- bluebird
- supports-color
- '@electron-forge/plugin-base@7.8.1(bluebird@3.7.2)':
+ '@electron-forge/plugin-base@7.10.2(bluebird@3.7.2)':
dependencies:
- '@electron-forge/shared-types': 7.8.1(bluebird@3.7.2)
+ '@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
transitivePeerDependencies:
- bluebird
- supports-color
- '@electron-forge/plugin-base@7.9.0(bluebird@3.7.2)':
+ '@electron-forge/plugin-vite@7.10.2(bluebird@3.7.2)':
dependencies:
- '@electron-forge/shared-types': 7.9.0(bluebird@3.7.2)
- transitivePeerDependencies:
- - bluebird
- - supports-color
-
- '@electron-forge/plugin-vite@7.9.0(bluebird@3.7.2)':
- dependencies:
- '@electron-forge/plugin-base': 7.9.0(bluebird@3.7.2)
- '@electron-forge/shared-types': 7.9.0(bluebird@3.7.2)
+ '@electron-forge/plugin-base': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
chalk: 4.1.2
debug: 4.4.1
fs-extra: 10.1.0
@@ -8187,23 +8255,22 @@ snapshots:
- bluebird
- supports-color
- '@electron-forge/publisher-base@7.8.1(bluebird@3.7.2)':
+ '@electron-forge/publisher-base@7.10.2(bluebird@3.7.2)':
dependencies:
- '@electron-forge/shared-types': 7.8.1(bluebird@3.7.2)
+ '@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
transitivePeerDependencies:
- bluebird
- supports-color
- '@electron-forge/shared-types@7.6.1(bluebird@3.7.2)':
+ '@electron-forge/shared-types@7.10.2(bluebird@3.7.2)':
dependencies:
- '@electron-forge/tracer': 7.6.1
+ '@electron-forge/tracer': 7.10.2
'@electron/packager': 18.3.6
'@electron/rebuild': 3.7.2(bluebird@3.7.2)
listr2: 7.0.2
transitivePeerDependencies:
- bluebird
- supports-color
- optional: true
'@electron-forge/shared-types@7.8.1(bluebird@3.7.2)':
dependencies:
@@ -8214,77 +8281,71 @@ snapshots:
transitivePeerDependencies:
- bluebird
- supports-color
+ optional: true
- '@electron-forge/shared-types@7.9.0(bluebird@3.7.2)':
+ '@electron-forge/template-base@7.10.2(bluebird@3.7.2)':
dependencies:
- '@electron-forge/tracer': 7.9.0
- '@electron/packager': 18.3.6
- '@electron/rebuild': 3.7.2(bluebird@3.7.2)
- listr2: 7.0.2
- transitivePeerDependencies:
- - bluebird
- - supports-color
-
- '@electron-forge/template-base@7.8.1(bluebird@3.7.2)':
- dependencies:
- '@electron-forge/core-utils': 7.8.1(bluebird@3.7.2)
- '@electron-forge/shared-types': 7.8.1(bluebird@3.7.2)
+ '@electron-forge/core-utils': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
'@malept/cross-spawn-promise': 2.0.0
debug: 4.4.1
fs-extra: 10.1.0
+ semver: 7.7.2
username: 5.1.0
transitivePeerDependencies:
- bluebird
- supports-color
- '@electron-forge/template-vite-typescript@7.8.1(bluebird@3.7.2)':
+ '@electron-forge/template-vite-typescript@7.10.2(bluebird@3.7.2)':
dependencies:
- '@electron-forge/shared-types': 7.8.1(bluebird@3.7.2)
- '@electron-forge/template-base': 7.8.1(bluebird@3.7.2)
+ '@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/template-base': 7.10.2(bluebird@3.7.2)
fs-extra: 10.1.0
transitivePeerDependencies:
- bluebird
- supports-color
- '@electron-forge/template-vite@7.8.1(bluebird@3.7.2)':
+ '@electron-forge/template-vite@7.10.2(bluebird@3.7.2)':
dependencies:
- '@electron-forge/shared-types': 7.8.1(bluebird@3.7.2)
- '@electron-forge/template-base': 7.8.1(bluebird@3.7.2)
+ '@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/template-base': 7.10.2(bluebird@3.7.2)
fs-extra: 10.1.0
transitivePeerDependencies:
- bluebird
- supports-color
- '@electron-forge/template-webpack-typescript@7.8.1(bluebird@3.7.2)':
+ '@electron-forge/template-webpack-typescript@7.10.2(@swc/core@1.12.0)(bluebird@3.7.2)(esbuild@0.25.2)':
dependencies:
- '@electron-forge/shared-types': 7.8.1(bluebird@3.7.2)
- '@electron-forge/template-base': 7.8.1(bluebird@3.7.2)
+ '@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/template-base': 7.10.2(bluebird@3.7.2)
+ fs-extra: 10.1.0
+ typescript: 5.4.5
+ webpack: 5.88.1(@swc/core@1.12.0)(esbuild@0.25.2)
+ transitivePeerDependencies:
+ - '@swc/core'
+ - bluebird
+ - esbuild
+ - supports-color
+ - uglify-js
+ - webpack-cli
+
+ '@electron-forge/template-webpack@7.10.2(bluebird@3.7.2)':
+ dependencies:
+ '@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
+ '@electron-forge/template-base': 7.10.2(bluebird@3.7.2)
fs-extra: 10.1.0
transitivePeerDependencies:
- bluebird
- supports-color
- '@electron-forge/template-webpack@7.8.1(bluebird@3.7.2)':
- dependencies:
- '@electron-forge/shared-types': 7.8.1(bluebird@3.7.2)
- '@electron-forge/template-base': 7.8.1(bluebird@3.7.2)
- fs-extra: 10.1.0
- transitivePeerDependencies:
- - bluebird
- - supports-color
-
- '@electron-forge/tracer@7.6.1':
+ '@electron-forge/tracer@7.10.2':
dependencies:
chrome-trace-event: 1.0.3
- optional: true
'@electron-forge/tracer@7.8.1':
dependencies:
chrome-trace-event: 1.0.3
-
- '@electron-forge/tracer@7.9.0':
- dependencies:
- chrome-trace-event: 1.0.3
+ optional: true
'@electron/asar@3.2.17':
dependencies:
@@ -8691,6 +8752,106 @@ snapshots:
'@humanwhocodes/retry@0.4.2': {}
+ '@inquirer/checkbox@3.0.1':
+ dependencies:
+ '@inquirer/core': 9.2.1
+ '@inquirer/figures': 1.0.13
+ '@inquirer/type': 2.0.0
+ ansi-escapes: 4.3.2
+ yoctocolors-cjs: 2.1.3
+
+ '@inquirer/confirm@4.0.1':
+ dependencies:
+ '@inquirer/core': 9.2.1
+ '@inquirer/type': 2.0.0
+
+ '@inquirer/core@9.2.1':
+ dependencies:
+ '@inquirer/figures': 1.0.13
+ '@inquirer/type': 2.0.0
+ '@types/mute-stream': 0.0.4
+ '@types/node': 22.13.0
+ '@types/wrap-ansi': 3.0.0
+ ansi-escapes: 4.3.2
+ cli-width: 4.1.0
+ mute-stream: 1.0.0
+ signal-exit: 4.1.0
+ strip-ansi: 6.0.1
+ wrap-ansi: 6.2.0
+ yoctocolors-cjs: 2.1.3
+
+ '@inquirer/editor@3.0.1':
+ dependencies:
+ '@inquirer/core': 9.2.1
+ '@inquirer/type': 2.0.0
+ external-editor: 3.1.0
+
+ '@inquirer/expand@3.0.1':
+ dependencies:
+ '@inquirer/core': 9.2.1
+ '@inquirer/type': 2.0.0
+ yoctocolors-cjs: 2.1.3
+
+ '@inquirer/figures@1.0.13': {}
+
+ '@inquirer/input@3.0.1':
+ dependencies:
+ '@inquirer/core': 9.2.1
+ '@inquirer/type': 2.0.0
+
+ '@inquirer/number@2.0.1':
+ dependencies:
+ '@inquirer/core': 9.2.1
+ '@inquirer/type': 2.0.0
+
+ '@inquirer/password@3.0.1':
+ dependencies:
+ '@inquirer/core': 9.2.1
+ '@inquirer/type': 2.0.0
+ ansi-escapes: 4.3.2
+
+ '@inquirer/prompts@6.0.1':
+ dependencies:
+ '@inquirer/checkbox': 3.0.1
+ '@inquirer/confirm': 4.0.1
+ '@inquirer/editor': 3.0.1
+ '@inquirer/expand': 3.0.1
+ '@inquirer/input': 3.0.1
+ '@inquirer/number': 2.0.1
+ '@inquirer/password': 3.0.1
+ '@inquirer/rawlist': 3.0.1
+ '@inquirer/search': 2.0.1
+ '@inquirer/select': 3.0.1
+
+ '@inquirer/rawlist@3.0.1':
+ dependencies:
+ '@inquirer/core': 9.2.1
+ '@inquirer/type': 2.0.0
+ yoctocolors-cjs: 2.1.3
+
+ '@inquirer/search@2.0.1':
+ dependencies:
+ '@inquirer/core': 9.2.1
+ '@inquirer/figures': 1.0.13
+ '@inquirer/type': 2.0.0
+ yoctocolors-cjs: 2.1.3
+
+ '@inquirer/select@3.0.1':
+ dependencies:
+ '@inquirer/core': 9.2.1
+ '@inquirer/figures': 1.0.13
+ '@inquirer/type': 2.0.0
+ ansi-escapes: 4.3.2
+ yoctocolors-cjs: 2.1.3
+
+ '@inquirer/type@1.5.5':
+ dependencies:
+ mute-stream: 1.0.0
+
+ '@inquirer/type@2.0.0':
+ dependencies:
+ mute-stream: 1.0.0
+
'@inversifyjs/common@1.4.0': {}
'@inversifyjs/core@1.3.5(reflect-metadata@0.2.2)':
@@ -8954,6 +9115,11 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.1
'@jridgewell/sourcemap-codec': 1.4.15
+ '@listr2/prompt-adapter-inquirer@2.0.22(@inquirer/prompts@6.0.1)':
+ dependencies:
+ '@inquirer/prompts': 6.0.1
+ '@inquirer/type': 1.5.5
+
'@malept/cross-spawn-promise@1.1.1':
dependencies:
cross-spawn: 7.0.6
@@ -9182,10 +9348,10 @@ snapshots:
'@popperjs/core@2.11.8': {}
- '@reforged/maker-appimage@5.0.0(bluebird@3.7.2)':
+ '@reforged/maker-appimage@5.1.0(bluebird@3.7.2)':
dependencies:
- '@electron-forge/maker-base': 7.6.1(bluebird@3.7.2)
- '@reforged/maker-types': 1.0.1
+ '@electron-forge/maker-base': 7.8.1(bluebird@3.7.2)
+ '@reforged/maker-types': 2.0.0
'@spacingbat3/lss': 1.2.0
semver: 7.7.2
transitivePeerDependencies:
@@ -9193,7 +9359,7 @@ snapshots:
- supports-color
optional: true
- '@reforged/maker-types@1.0.1':
+ '@reforged/maker-types@2.0.0':
optional: true
'@rjsf/core@6.0.0-beta.8(@rjsf/utils@6.0.0-beta.10(react@19.0.0))(react@19.0.0)':
@@ -9568,6 +9734,10 @@ snapshots:
'@types/minimatch@5.1.2':
optional: true
+ '@types/mute-stream@0.0.4':
+ dependencies:
+ '@types/node': 22.13.0
+
'@types/node@16.9.1': {}
'@types/node@20.14.9':
@@ -9630,6 +9800,8 @@ snapshots:
'@types/uuid@9.0.8': {}
+ '@types/wrap-ansi@3.0.0': {}
+
'@types/yauzl@2.10.0':
dependencies:
'@types/node': 22.13.0
@@ -10017,6 +10189,8 @@ snapshots:
loupe: 3.1.3
tinyrainbow: 2.0.0
+ '@vscode/sudo-prompt@9.3.1': {}
+
'@webassemblyjs/ast@1.11.6':
dependencies:
'@webassemblyjs/helper-numbers': 1.11.6
@@ -10190,6 +10364,10 @@ snapshots:
'@algolia/requester-fetch': 5.26.0
'@algolia/requester-node-http': 5.26.0
+ ansi-escapes@4.3.2:
+ dependencies:
+ type-fest: 0.21.3
+
ansi-escapes@5.0.0:
dependencies:
type-fest: 1.4.0
@@ -10416,10 +10594,6 @@ snapshots:
dependencies:
balanced-match: 1.0.2
- braces@3.0.2:
- dependencies:
- fill-range: 7.0.1
-
braces@3.0.3:
dependencies:
fill-range: 7.1.1
@@ -10569,6 +10743,8 @@ snapshots:
chalk@5.4.1: {}
+ chardet@0.7.0: {}
+
check-error@2.1.1: {}
chownr@1.1.4: {}
@@ -10621,6 +10797,8 @@ snapshots:
slice-ansi: 5.0.0
string-width: 5.1.2
+ cli-width@4.1.0: {}
+
cliui@7.0.4:
dependencies:
string-width: 4.2.3
@@ -11036,13 +11214,12 @@ snapshots:
- supports-color
optional: true
- electron-ipc-cat@2.0.1(electron@36.4.0)(rxjs@7.8.2):
+ electron-ipc-cat@2.1.1(electron@36.4.0)(rxjs@7.8.2):
dependencies:
electron: 36.4.0
- memize: 2.1.0
+ memize: 2.1.1
rxjs: 7.8.2
- serialize-error: 11.0.0
- type-fest: 2.19.0
+ serialize-error: 12.0.0
electron-is-dev@2.0.0: {}
@@ -11704,6 +11881,12 @@ snapshots:
ext-list: 2.2.2
sort-keys-length: 1.0.1
+ external-editor@3.1.0:
+ dependencies:
+ chardet: 0.7.0
+ iconv-lite: 0.4.24
+ tmp: 0.0.33
+
extract-files@11.0.0: {}
extract-zip@2.0.1:
@@ -11720,14 +11903,6 @@ snapshots:
fast-diff@1.3.0: {}
- fast-glob@3.3.2:
- dependencies:
- '@nodelib/fs.stat': 2.0.5
- '@nodelib/fs.walk': 1.2.8
- glob-parent: 5.1.2
- merge2: 1.4.1
- micromatch: 4.0.5
-
fast-glob@3.3.3:
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -11799,10 +11974,6 @@ snapshots:
strip-outer: 1.0.1
trim-repeated: 1.0.0
- fill-range@7.0.1:
- dependencies:
- to-regex-range: 5.0.1
-
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
@@ -12246,6 +12417,10 @@ snapshots:
optionalDependencies:
typescript: 5.8.3
+ iconv-lite@0.4.24:
+ dependencies:
+ safer-buffer: 2.1.2
+
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
@@ -12933,7 +13108,7 @@ snapshots:
mimic-fn: 2.1.0
p-is-promise: 2.1.0
- memize@2.1.0: {}
+ memize@2.1.1: {}
memoize-one@4.0.3: {}
@@ -12951,11 +13126,6 @@ snapshots:
merge2@1.4.1: {}
- micromatch@4.0.5:
- dependencies:
- braces: 3.0.2
- picomatch: 2.3.1
-
micromatch@4.0.8:
dependencies:
braces: 3.0.3
@@ -13095,6 +13265,8 @@ snapshots:
ms@2.1.3: {}
+ mute-stream@1.0.0: {}
+
mz@2.7.0:
dependencies:
any-promise: 1.3.0
@@ -13332,6 +13504,8 @@ snapshots:
strip-ansi: 6.0.1
wcwidth: 1.0.1
+ os-tmpdir@1.0.2: {}
+
own-keys@1.0.1:
dependencies:
get-intrinsic: 1.3.0
@@ -13974,9 +14148,9 @@ snapshots:
semver@7.7.2: {}
- serialize-error@11.0.0:
+ serialize-error@12.0.0:
dependencies:
- type-fest: 2.19.0
+ type-fest: 4.41.0
serialize-error@7.0.1:
dependencies:
@@ -14068,6 +14242,8 @@ snapshots:
signal-exit@4.0.2: {}
+ signal-exit@4.1.0: {}
+
simple-concat@1.0.1: {}
simple-get@4.0.1:
@@ -14345,8 +14521,6 @@ snapshots:
stylis@4.3.2: {}
- sudo-prompt@9.2.1: {}
-
sumchecker@3.0.1:
dependencies:
debug: 4.4.1
@@ -14499,6 +14673,10 @@ snapshots:
tmp: 0.2.3
optional: true
+ tmp@0.0.33:
+ dependencies:
+ os-tmpdir: 1.0.2
+
tmp@0.2.3: {}
to-regex-range@5.0.1:
@@ -14595,6 +14773,8 @@ snapshots:
type-fest@0.20.2: {}
+ type-fest@0.21.3: {}
+
type-fest@1.4.0: {}
type-fest@2.19.0: {}
@@ -14674,6 +14854,8 @@ snapshots:
optionalDependencies:
'@types/styled-components': 5.1.34
+ typescript@5.4.5: {}
+
typescript@5.8.3: {}
typesync@0.14.3:
@@ -15099,6 +15281,12 @@ snapshots:
regexparam: 3.0.0
use-sync-external-store: 1.5.0(react@19.0.0)
+ wrap-ansi@6.2.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
@@ -15179,6 +15367,8 @@ snapshots:
yocto-queue@0.1.0: {}
+ yoctocolors-cjs@2.1.3: {}
+
yup@1.2.0:
dependencies:
property-expr: 2.0.5
diff --git a/src/components/KeyboardShortcutRegister.tsx b/src/components/KeyboardShortcutRegister.tsx
new file mode 100644
index 00000000..b17a9879
--- /dev/null
+++ b/src/components/KeyboardShortcutRegister.tsx
@@ -0,0 +1,212 @@
+import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material';
+import React, { useCallback, useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+
+interface KeyboardShortcutRegisterProps {
+ /** Current shortcut key value */
+ value: string;
+ /** Callback function when shortcut key changes */
+ onChange: (value: string) => void;
+ /** Feature name, displayed on the button */
+ label: string;
+ /** Button variant */
+ variant?: 'text' | 'outlined' | 'contained';
+ /** Button color */
+ color?: 'inherit' | 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning';
+ /** Disabled state */
+ disabled?: boolean;
+}
+
+/**
+ * Keyboard shortcut register component
+ * Provides a button that opens a modal dialog when clicked, allowing users to set shortcuts via keystrokes
+ */
+export const KeyboardShortcutRegister: React.FC = ({
+ value,
+ onChange,
+ label,
+ variant = 'outlined',
+ color = 'primary',
+ disabled = false,
+}) => {
+ const { t } = useTranslation();
+ const [dialogOpen, setDialogOpen] = useState(false);
+ const [currentKeyCombo, setCurrentKeyCombo] = useState(value);
+
+ /**
+ * Format shortcut text for display
+ */
+ const formatShortcutText = useCallback((shortcut: string): string => {
+ if (!shortcut) return t('KeyboardShortcut.None');
+ return shortcut;
+ }, [t]);
+
+ /**
+ * Handle keyboard events to update current shortcut combination
+ */
+ const handleKeyDown = useCallback(async (event: KeyboardEvent) => {
+ // Handle special keys for dialog control
+ if (event.key === 'Escape') {
+ event.preventDefault();
+ setDialogOpen(false);
+ return;
+ }
+
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ onChange(currentKeyCombo);
+ setDialogOpen(false);
+ return;
+ }
+
+ // Prevent default behavior to avoid page scrolling
+ event.preventDefault();
+
+ // Get the currently pressed key
+ const key = event.key;
+
+ // Ignore modifier keys pressed alone
+ if (['Shift', 'Control', 'Alt', 'Meta', 'OS'].includes(key)) {
+ return;
+ }
+
+ // Build the shortcut combination
+ const combo: string[] = [];
+
+ try {
+ if (event.ctrlKey || event.metaKey) {
+ const platform = await window.service.context.get('platform');
+ const modifier = platform === 'darwin' ? 'Cmd' : 'Ctrl';
+ combo.push(modifier);
+ }
+
+ if (event.altKey) {
+ combo.push('Alt');
+ }
+
+ if (event.shiftKey) {
+ combo.push('Shift');
+ }
+
+ // Add the main key
+ combo.push(key);
+
+ // Update current shortcut combination
+ const newCombo = combo.join('+');
+ setCurrentKeyCombo(newCombo);
+ } catch {
+ // Handle error silently
+ }
+ }, [onChange, currentKeyCombo]);
+
+ /**
+ * Open the dialog
+ */
+ const handleOpenDialog = useCallback(() => {
+ setCurrentKeyCombo(value);
+ setDialogOpen(true);
+ }, [value]);
+
+ /**
+ * Close the dialog
+ */
+ const handleCloseDialog = useCallback(() => {
+ setDialogOpen(false);
+ }, []);
+
+ /**
+ * Confirm shortcut key setting
+ */
+ const handleConfirm = useCallback(() => {
+ onChange(currentKeyCombo);
+ setDialogOpen(false);
+ }, [onChange, currentKeyCombo]);
+
+ /**
+ * Clear shortcut key
+ */
+ const handleClear = useCallback(() => {
+ setCurrentKeyCombo('');
+ }, []);
+
+ /**
+ * Add or remove keyboard event listeners when dialog opens or closes
+ */
+ useEffect(() => {
+ const keyDownHandler = (event: KeyboardEvent) => {
+ void handleKeyDown(event);
+ };
+
+ if (dialogOpen) {
+ document.addEventListener('keydown', keyDownHandler);
+ } else {
+ document.removeEventListener('keydown', keyDownHandler);
+ }
+
+ return () => {
+ document.removeEventListener('keydown', keyDownHandler);
+ };
+ }, [dialogOpen, handleKeyDown]);
+
+ return (
+ <>
+
+
+
+ >
+ );
+};
diff --git a/src/components/TokenForm/gitTokenHooks.ts b/src/components/TokenForm/gitTokenHooks.ts
index 378ea267..79c23812 100644
--- a/src/components/TokenForm/gitTokenHooks.ts
+++ b/src/components/TokenForm/gitTokenHooks.ts
@@ -11,7 +11,7 @@ export function useAuth(storageService: SupportedStorageServices): [() => Promis
await window.service.auth.set(`${storageService}-token`, '');
// await window.service.window.clearStorageData();
} catch (error) {
- void window.service.native.log('error', 'TokenForm: auth operation failed', { function: 'useAuth', error: String(error) });
+ void window.service.native.log('error', 'TokenForm: auth operation failed', { function: 'useAuth', error });
}
}, [storageService]);
@@ -66,7 +66,7 @@ export function useGetGithubUserInfoOnLoad(): void {
await window.service.auth.setUserInfos(userInfo);
}
} catch (error) {
- void window.service.native.log('error', 'TokenForm: get github user info failed', { function: 'useGetGithubUserInfoOnLoad', error: String(error) });
+ void window.service.native.log('error', 'TokenForm: get github user info failed', { function: 'useGetGithubUserInfoOnLoad', error });
}
});
}, []);
diff --git a/src/components/__tests__/KeyboardShortcutRegister.test.tsx b/src/components/__tests__/KeyboardShortcutRegister.test.tsx
new file mode 100644
index 00000000..47d0706e
--- /dev/null
+++ b/src/components/__tests__/KeyboardShortcutRegister.test.tsx
@@ -0,0 +1,507 @@
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import '@testing-library/jest-dom/vitest';
+import { ThemeProvider } from '@mui/material/styles';
+import { lightTheme } from '@services/theme/defaultTheme';
+
+import { KeyboardShortcutRegister } from '../KeyboardShortcutRegister';
+
+const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
+
+ {children}
+
+);
+
+describe('KeyboardShortcutRegister Component', () => {
+ let mockOnChange: ReturnType;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockOnChange = vi.fn();
+
+ // Mock window.service.context.get
+ Object.defineProperty(window.service.context, 'get', {
+ value: vi.fn().mockImplementation((key: string) => {
+ if (key === 'platform') {
+ // Return platform based on process.platform for testing
+ return Promise.resolve(process.platform === 'darwin' ? 'darwin' : 'win32');
+ }
+ return Promise.resolve('win32');
+ }),
+ writable: true,
+ });
+ });
+
+ const renderComponent = (overrides: {
+ value?: string;
+ onChange?: (value: string) => void;
+ label?: string;
+ disabled?: boolean;
+ } = {}) => {
+ const defaultProps = {
+ value: '',
+ onChange: mockOnChange,
+ label: 'Test Shortcut',
+ disabled: false,
+ ...overrides,
+ };
+
+ return render(
+
+
+ ,
+ );
+ };
+
+ describe('Initial state and rendering', () => {
+ it('should display button with label and current shortcut value', () => {
+ renderComponent({
+ value: 'Ctrl+Shift+T',
+ label: 'Toggle Window',
+ });
+
+ const button = screen.getByTestId('shortcut-register-button');
+ expect(button).toBeInTheDocument();
+ expect(button).toHaveTextContent('Toggle Window');
+ expect(button).toHaveTextContent('Ctrl+Shift+T');
+ });
+
+ it('should display "None" when no shortcut is set', () => {
+ renderComponent({
+ value: '',
+ label: 'Toggle Window',
+ });
+
+ const button = screen.getByTestId('shortcut-register-button');
+ expect(button).toHaveTextContent('KeyboardShortcut.None');
+ });
+
+ it('should respect disabled state', () => {
+ renderComponent({
+ disabled: true,
+ });
+
+ const button = screen.getByTestId('shortcut-register-button');
+ expect(button).toBeDisabled();
+ });
+
+ it('should not show dialog initially', () => {
+ renderComponent();
+
+ expect(screen.queryByTestId('shortcut-dialog')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('Dialog interaction', () => {
+ it('should open dialog when button is clicked', async () => {
+ const user = userEvent.setup();
+ renderComponent();
+
+ const button = screen.getByTestId('shortcut-register-button');
+ await user.click(button);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
+ });
+
+ expect(screen.getByText('KeyboardShortcut.RegisterShortcut')).toBeInTheDocument();
+ });
+
+ it('should display current shortcut in dialog initially', async () => {
+ const user = userEvent.setup();
+ renderComponent({
+ value: 'Ctrl+A',
+ });
+
+ const button = screen.getByTestId('shortcut-register-button');
+ await user.click(button);
+
+ await waitFor(() => {
+ const display = screen.getByTestId('shortcut-display');
+ expect(display).toHaveTextContent('Ctrl+A');
+ });
+ });
+
+ it('should close dialog when cancel button is clicked', async () => {
+ const user = userEvent.setup();
+ renderComponent();
+
+ const button = screen.getByTestId('shortcut-register-button');
+ await user.click(button);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
+ });
+
+ // Press ESC to close dialog (MUI Dialog default behavior)
+ fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' });
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('shortcut-dialog')).not.toBeInTheDocument();
+ }, { timeout: 500 });
+ });
+ });
+
+ describe('Keyboard shortcut capture', () => {
+ it('should capture Ctrl key combination', async () => {
+ const user = userEvent.setup();
+ renderComponent();
+
+ const button = screen.getByTestId('shortcut-register-button');
+ await user.click(button);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
+ });
+
+ const dialogContent = screen.getByTestId('shortcut-dialog-content');
+
+ // Simulate keyboard event with Ctrl+Shift+T
+ fireEvent.keyDown(dialogContent, {
+ key: 'T',
+ ctrlKey: true,
+ shiftKey: true,
+ bubbles: true,
+ });
+
+ await waitFor(() => {
+ const display = screen.getByTestId('shortcut-display');
+ expect(display).toHaveTextContent('Ctrl+Shift+T');
+ });
+ });
+
+ it('should capture Cmd key combination on macOS', async () => {
+ const originalPlatform = process.platform;
+ Object.defineProperty(process, 'platform', {
+ value: 'darwin',
+ writable: true,
+ configurable: true,
+ });
+
+ const user = userEvent.setup();
+ renderComponent();
+
+ const button = screen.getByTestId('shortcut-register-button');
+ await user.click(button);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
+ });
+
+ // Simulate Cmd+K key press on document
+ fireEvent.keyDown(document, {
+ key: 'K',
+ metaKey: true,
+ bubbles: true,
+ });
+
+ await waitFor(() => {
+ const display = screen.getByTestId('shortcut-display');
+ expect(display).toHaveTextContent('Cmd+K');
+ });
+
+ Object.defineProperty(process, 'platform', {
+ value: originalPlatform,
+ writable: true,
+ configurable: true,
+ });
+ });
+
+ it('should capture Alt key combination', async () => {
+ const user = userEvent.setup();
+ renderComponent();
+
+ const button = screen.getByTestId('shortcut-register-button');
+ await user.click(button);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
+ });
+
+ const dialogContent = screen.getByTestId('shortcut-dialog-content');
+
+ fireEvent.keyDown(dialogContent, {
+ key: 'F4',
+ altKey: true,
+ bubbles: true,
+ });
+
+ await waitFor(() => {
+ const display = screen.getByTestId('shortcut-display');
+ expect(display).toHaveTextContent('Alt+F4');
+ });
+ });
+
+ it('should ignore modifier keys pressed alone', async () => {
+ const user = userEvent.setup();
+ renderComponent();
+
+ const button = screen.getByTestId('shortcut-register-button');
+ await user.click(button);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
+ });
+
+ const dialogContent = screen.getByTestId('shortcut-dialog-content');
+ const display = screen.getByTestId('shortcut-display');
+
+ // Should show initial "Press keys" message
+ expect(display).toHaveTextContent('KeyboardShortcut.PressKeys');
+
+ const modifierKeys = ['Shift', 'Control', 'Alt', 'Meta'];
+ for (const key of modifierKeys) {
+ fireEvent.keyDown(dialogContent, {
+ key,
+ bubbles: true,
+ });
+ }
+
+ // Should still show "Press keys" message
+ expect(display).toHaveTextContent('KeyboardShortcut.PressKeys');
+ });
+
+ it('should update display when keys are pressed', async () => {
+ const user = userEvent.setup();
+ renderComponent();
+
+ const button = screen.getByTestId('shortcut-register-button');
+ await user.click(button);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
+ });
+
+ const dialogContent = screen.getByTestId('shortcut-dialog-content');
+
+ // Press first combination
+ fireEvent.keyDown(dialogContent, {
+ key: 'A',
+ ctrlKey: true,
+ bubbles: true,
+ });
+
+ await waitFor(() => {
+ const display = screen.getByTestId('shortcut-display');
+ expect(display).toHaveTextContent('Ctrl+A');
+ });
+
+ // Press second combination - should replace
+ fireEvent.keyDown(dialogContent, {
+ key: 'B',
+ ctrlKey: true,
+ shiftKey: true,
+ bubbles: true,
+ });
+
+ await waitFor(() => {
+ const display = screen.getByTestId('shortcut-display');
+ expect(display).toHaveTextContent('Ctrl+Shift+B');
+ });
+ });
+ });
+
+ describe('Clear functionality', () => {
+ it('should clear current shortcut when clear button is clicked', async () => {
+ const user = userEvent.setup();
+ renderComponent({
+ value: 'Ctrl+T',
+ });
+
+ const button = screen.getByTestId('shortcut-register-button');
+ await user.click(button);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
+ });
+
+ // Initially should show the current value
+ const display = screen.getByTestId('shortcut-display');
+ expect(display).toHaveTextContent('Ctrl+T');
+
+ // Verify clear button exists and is enabled
+ const clearButton = screen.getByTestId('shortcut-clear-button');
+ expect(clearButton).toBeInTheDocument();
+ expect(clearButton).toBeEnabled();
+ });
+
+ it('should call onChange with empty string when cleared and confirmed', async () => {
+ const user = userEvent.setup();
+ renderComponent({
+ value: '', // Start with empty to test setting empty and confirming
+ });
+
+ const button = screen.getByTestId('shortcut-register-button');
+ await user.click(button);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
+ });
+
+ // Don't set any keys, just press Enter to confirm empty
+ fireEvent.keyDown(document, { key: 'Enter', code: 'Enter' });
+
+ await waitFor(() => {
+ expect(mockOnChange).toHaveBeenCalledWith('');
+ });
+ });
+ });
+
+ describe('Confirm functionality', () => {
+ it('should call onChange with new shortcut on confirm', async () => {
+ const user = userEvent.setup();
+ renderComponent({
+ value: '',
+ });
+
+ const button = screen.getByTestId('shortcut-register-button');
+ await user.click(button);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
+ });
+
+ const dialogContent = screen.getByTestId('shortcut-dialog-content');
+
+ fireEvent.keyDown(dialogContent, {
+ key: 'N',
+ ctrlKey: true,
+ bubbles: true,
+ });
+
+ await waitFor(() => {
+ const display = screen.getByTestId('shortcut-display');
+ expect(display).toHaveTextContent('Ctrl+N');
+ });
+
+ // Press Enter to confirm
+ fireEvent.keyDown(document, { key: 'Enter', code: 'Enter' });
+
+ await waitFor(() => {
+ expect(mockOnChange).toHaveBeenCalledWith('Ctrl+N');
+ });
+ });
+
+ it('should not call onChange when cancel is clicked', async () => {
+ const user = userEvent.setup();
+ renderComponent({
+ value: 'Ctrl+A',
+ });
+
+ const button = screen.getByTestId('shortcut-register-button');
+ await user.click(button);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
+ });
+
+ const dialogContent = screen.getByTestId('shortcut-dialog-content');
+
+ fireEvent.keyDown(dialogContent, {
+ key: 'B',
+ ctrlKey: true,
+ bubbles: true,
+ });
+
+ await waitFor(() => {
+ const display = screen.getByTestId('shortcut-display');
+ expect(display).toHaveTextContent('Ctrl+B');
+ });
+
+ // Press ESC to cancel without saving
+ fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' });
+
+ await waitFor(() => {
+ expect(mockOnChange).not.toHaveBeenCalled();
+ });
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('shortcut-dialog')).not.toBeInTheDocument();
+ }, { timeout: 500 });
+ });
+
+ it('should close dialog after confirm', async () => {
+ const user = userEvent.setup();
+ renderComponent();
+
+ const button = screen.getByTestId('shortcut-register-button');
+ await user.click(button);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
+ });
+
+ const dialogContent = screen.getByTestId('shortcut-dialog-content');
+
+ fireEvent.keyDown(dialogContent, {
+ key: 'T',
+ ctrlKey: true,
+ bubbles: true,
+ });
+
+ // Press Enter to confirm and close dialog
+ fireEvent.keyDown(document, { key: 'Enter', code: 'Enter' });
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('shortcut-dialog')).not.toBeInTheDocument();
+ }, { timeout: 500 });
+ });
+ });
+
+ describe('Help text', () => {
+ it('should display help text in dialog', async () => {
+ const user = userEvent.setup();
+ renderComponent();
+
+ const button = screen.getByTestId('shortcut-register-button');
+ await user.click(button);
+
+ await waitFor(() => {
+ expect(screen.getByText('KeyboardShortcut.HelpText')).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('Props validation', () => {
+ it('should use custom label', () => {
+ renderComponent({ label: 'Custom Label' });
+
+ const button = screen.getByTestId('shortcut-register-button');
+ expect(button).toHaveTextContent('Custom Label');
+ });
+
+ it('should handle onChange callback', async () => {
+ const customOnChange = vi.fn();
+ const user = userEvent.setup();
+
+ renderComponent({ onChange: customOnChange });
+
+ const button = screen.getByTestId('shortcut-register-button');
+ await user.click(button);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('shortcut-dialog')).toBeInTheDocument();
+ });
+
+ // Simulate Ctrl+X key press on document
+ fireEvent.keyDown(document, {
+ key: 'X',
+ ctrlKey: true,
+ bubbles: true,
+ });
+
+ // Wait for the key combination to be processed
+ await waitFor(() => {
+ expect(screen.getByText('Ctrl+X')).toBeInTheDocument();
+ });
+
+ // Press Enter to confirm
+ fireEvent.keyDown(document, { key: 'Enter', code: 'Enter' });
+
+ await waitFor(() => {
+ expect(customOnChange).toHaveBeenCalledWith('Ctrl+X');
+ });
+ });
+ });
+});
diff --git a/src/constants/paths.ts b/src/constants/paths.ts
index d9efd79e..53d71aee 100644
--- a/src/constants/paths.ts
+++ b/src/constants/paths.ts
@@ -24,14 +24,15 @@ export const sourcePath = isPackaged
? path.resolve(process.resourcesPath, '..') // Packaged: go up from resources/ to app root
: path.resolve(__dirname, '..', '..'); // Dev/Unit test: from src/constants to project root
// Build resources (only used in dev/test)
-export const buildResourcePath = path.resolve(sourcePath, '..', 'build-resources');
+// In dev the `sourcePath` already points to project root, so join directly to `build-resources`.
+export const buildResourcePath = path.resolve(sourcePath, 'build-resources');
export const developmentImageFolderPath = path.resolve(sourcePath, 'images');
-// Menubar icon
-const menuBarIconFileName = isMac ? 'menubarTemplate@2x.png' : 'menubar@2x.png';
-export const MENUBAR_ICON_PATH = isPackaged
- ? path.resolve(process.resourcesPath, menuBarIconFileName) // Packaged: resources/icon
- : path.resolve(buildResourcePath, menuBarIconFileName); // Dev/Unit test: build-resources/icon
+// TidGi Mini Window icon
+const tidgiMiniWindowIconFileName = isMac ? 'tidgiMiniWindowTemplate@2x.png' : 'tidgiMiniWindow@2x.png';
+export const TIDGI_MINI_WINDOW_ICON_PATH = isPackaged
+ ? path.resolve(process.resourcesPath, tidgiMiniWindowIconFileName) // Packaged: resources/
+ : path.resolve(buildResourcePath, tidgiMiniWindowIconFileName); // Dev/Unit test: /build-resources/
// System paths
export const CHROME_ERROR_PATH = 'chrome-error://chromewebdata/';
diff --git a/src/helpers/testKeyboardShortcuts.ts b/src/helpers/testKeyboardShortcuts.ts
new file mode 100644
index 00000000..15d0191f
--- /dev/null
+++ b/src/helpers/testKeyboardShortcuts.ts
@@ -0,0 +1,121 @@
+/**
+ * Test-only keyboard shortcut fallback for E2E testing environments.
+ *
+ * TECHNICAL LIMITATION EXPLANATION:
+ * E2E testing frameworks (like Playwright, Puppeteer, or Selenium) cannot simulate
+ * system-level global keyboard shortcuts because:
+ * 1. Operating systems restrict access to global hotkeys for security reasons
+ * 2. Browser sandboxing prevents web content from intercepting OS-level key events
+ * 3. Test automation tools operate within browser context, not at OS level
+ * 4. Global shortcuts are typically handled by native applications outside browser scope
+ *
+ * WORKAROUND SOLUTION:
+ * This fallback listens to document-level keydown events in the renderer process
+ * and manually routes matching key combinations to their corresponding service methods.
+ * This approach only works during testing when the application window has focus,
+ * but allows E2E tests to verify keyboard shortcut functionality without requiring
+ * actual OS-level global hotkey simulation.
+ *
+ * GENERIC DESIGN:
+ * Unlike hardcoding specific shortcuts, this system dynamically handles ALL registered
+ * keyboard shortcuts by parsing the "ServiceName.methodName" format and calling the
+ * appropriate service method through the window.service API.
+ *
+ * In production, global shortcuts are properly handled by the main process via
+ * NativeService.registerKeyboardShortcut using Electron's globalShortcut API.
+ */
+
+export function initTestKeyboardShortcutFallback(): () => void {
+ const isTestEnvironment = process.env.NODE_ENV === 'test';
+ void window.service.native.log('debug', 'Renderer(Test): initTestKeyboardShortcutFallback called', {
+ isTestEnvironment,
+ nodeEnv: process.env.NODE_ENV,
+ });
+ if (!isTestEnvironment) return () => {};
+
+ let allShortcuts: Record = {};
+ let platform = 'win32';
+
+ // Load platform and all current shortcut mappings
+ void (async () => {
+ platform = await window.service.context.get('platform').catch(() => platform);
+ void window.service.native.log('debug', 'Renderer(Test): Platform detected', { platform });
+ allShortcuts = await window.service.native.getKeyboardShortcuts().catch(() => ({}));
+ void window.service.native.log('debug', 'Renderer(Test): Loaded all keyboard shortcuts', {
+ shortcuts: allShortcuts,
+ shortcutCount: Object.keys(allShortcuts).length,
+ });
+ })();
+
+ // Subscribe to preference changes to keep all shortcuts up to date
+ const subscription = window.observables.preference?.preference$?.subscribe?.((pref: unknown) => {
+ const p = pref as { keyboardShortcuts?: Record } | undefined;
+ if (p?.keyboardShortcuts) {
+ allShortcuts = { ...p.keyboardShortcuts };
+ void window.service.native.log('debug', 'Renderer(Test): Updated shortcuts from preferences', {
+ shortcuts: allShortcuts,
+ });
+ }
+ });
+
+ const formatComboFromEvent = (event: KeyboardEvent): string => {
+ const combo: string[] = [];
+ const isMac = platform === 'darwin';
+ // On macOS, Cmd (metaKey) and Ctrl are distinct and both may be used.
+ // On other platforms, only Ctrl is used as the primary modifier here.
+ if (isMac) {
+ if (event.ctrlKey) combo.push('Ctrl');
+ if (event.metaKey) combo.push('Cmd');
+ } else {
+ if (event.ctrlKey) combo.push('Ctrl');
+ }
+ if (event.altKey) combo.push('Alt');
+ if (event.shiftKey) combo.push('Shift');
+ combo.push(event.key);
+ return combo.join('+');
+ };
+
+ const handleKeyDown = (event: KeyboardEvent): void => {
+ const pressed = formatComboFromEvent(event);
+ void window.service.native.log('debug', 'Renderer(Test): Key pressed', {
+ pressed,
+ ctrlKey: event.ctrlKey,
+ metaKey: event.metaKey,
+ shiftKey: event.shiftKey,
+ altKey: event.altKey,
+ key: event.key,
+ availableShortcuts: Object.keys(allShortcuts).length,
+ });
+
+ // Find matching shortcut key for the pressed combination
+ let matched = false;
+ for (const [key, shortcut] of Object.entries(allShortcuts)) {
+ if (shortcut && pressed === shortcut) {
+ event.preventDefault();
+ matched = true;
+ void window.service.native.log('debug', 'Renderer(Test): Shortcut matched', {
+ pressed,
+ key,
+ shortcut,
+ });
+ void window.service.native.executeShortcutCallback(key);
+ break; // Only execute the first match
+ }
+ }
+
+ if (!matched) {
+ void window.service.native.log('debug', 'Renderer(Test): No shortcut matched', {
+ pressed,
+ allShortcuts,
+ });
+ }
+ };
+
+ void window.service.native.log('debug', 'Renderer(Test): Adding keydown listener to document');
+ document.addEventListener('keydown', handleKeyDown);
+ return () => {
+ void window.service.native.log('debug', 'Renderer(Test): Cleanup - removing keydown listener');
+ document.removeEventListener('keydown', handleKeyDown);
+ subscription?.unsubscribe?.();
+ };
+}
diff --git a/src/main.ts b/src/main.ts
index 337799dc..d1bf75d0 100755
--- a/src/main.ts
+++ b/src/main.ts
@@ -24,6 +24,7 @@ import type { IDeepLinkService } from '@services/deepLink/interface';
import type { IExternalAPIService } from '@services/externalAPI/interface';
import type { IGitService } from '@services/git/interface';
import { initializeObservables } from '@services/libs/initializeObservables';
+import type { INativeService } from '@services/native/interface';
import { reportErrorToGithubWithTemplates } from '@services/native/reportError';
import type { IThemeService } from '@services/theme/interface';
import type { IUpdaterService } from '@services/updater/interface';
@@ -77,6 +78,7 @@ const externalAPIService = container.get(serviceIdentifier.
const gitService = container.get(serviceIdentifier.Git);
const themeService = container.get(serviceIdentifier.ThemeService);
const viewService = container.get(serviceIdentifier.View);
+const nativeService = container.get(serviceIdentifier.NativeService);
app.on('second-instance', async () => {
// see also src/helpers/singleInstance.ts
@@ -116,21 +118,18 @@ const commonInit = async (): Promise => {
externalAPIService.initialize(),
]);
- // if user want a menubar, we create a new window for that
+ // if user want a tidgi mini window, we create a new window for that
// handle workspace name + tiddler name in uri https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app
deepLinkService.initializeDeepLink('tidgi');
- const attachToMenubar = await preferenceService.get('attachToMenubar');
- await Promise.all([
- windowService.open(WindowNames.main),
- attachToMenubar ? windowService.open(WindowNames.menuBar) : Promise.resolve(),
- ]);
+ await windowService.open(WindowNames.main);
// Initialize services that depend on windows being created
await Promise.all([
gitService.initialize(),
themeService.initialize(),
viewService.initialize(),
+ nativeService.initialize(),
]);
initializeObservables();
@@ -140,6 +139,10 @@ const commonInit = async (): Promise => {
await workspaceService.initializeDefaultPageWorkspaces();
// perform wiki startup and git sync for each workspace
await workspaceViewService.initializeAllWorkspaceView();
+ const tidgiMiniWindow = await preferenceService.get('tidgiMiniWindow');
+ if (tidgiMiniWindow) {
+ await windowService.openTidgiMiniWindow(true, false);
+ }
ipcMain.emit('request-update-pause-notifications-info');
// Fix webview is not resized automatically
@@ -195,8 +198,7 @@ app.on('ready', async () => {
}
await updaterService.checkForUpdates();
} catch (error) {
- const error_ = error instanceof Error ? error : new Error(String(error));
- logger.error('Error during app ready handler', { function: "app.on('ready')", error: error_.message, stack: error_.stack ?? '' });
+ logger.error('Error during app ready handler', { function: "app.on('ready')", error });
}
});
app.on(MainChannel.windowAllClosed, async () => {
@@ -222,7 +224,7 @@ app.on(
unhandled({
showDialog: !isDevelopmentOrTest,
logger: (error: Error) => {
- logger.error(error.message + (error.stack ?? ''));
+ logger.error('unhandled', { error });
},
reportButton: (error) => {
reportErrorToGithubWithTemplates(error);
diff --git a/src/pages/Agent/components/TabStoreInitializer.tsx b/src/pages/Agent/components/TabStoreInitializer.tsx
index 6ee42575..ccf00680 100644
--- a/src/pages/Agent/components/TabStoreInitializer.tsx
+++ b/src/pages/Agent/components/TabStoreInitializer.tsx
@@ -12,7 +12,7 @@ export function TabStoreInitializer() {
useEffect(() => {
// Initialize the tab store when the component mounts
initialize().catch((error: unknown) => {
- void window.service.native.log('error', 'Failed to initialize tab store', { function: 'TabStoreInitializer.initialize', error: String(error) });
+ void window.service.native.log('error', 'Failed to initialize tab store', { function: 'TabStoreInitializer.initialize', error });
});
}, [initialize]);
diff --git a/src/pages/Agent/store/agentChatStore/actions/agentActions.ts b/src/pages/Agent/store/agentChatStore/actions/agentActions.ts
index ad3896df..607785f2 100644
--- a/src/pages/Agent/store/agentChatStore/actions/agentActions.ts
+++ b/src/pages/Agent/store/agentChatStore/actions/agentActions.ts
@@ -49,7 +49,7 @@ export const agentActions = (
`Failed to fetch agent definition for ${agentWithoutMessages.agentDefId}`,
{
function: 'agentActions.processAgentData',
- error: String(error),
+ error,
},
);
}
@@ -88,9 +88,9 @@ export const agentActions = (
error: null,
loading: false,
});
- } catch (error_) {
- set({ error: error_ instanceof Error ? error_ : new Error(String(error_)) });
- void window.service.native.log('error', 'Failed to load agent', { function: 'agentActions.loadAgent', error: String(error_) });
+ } catch (error) {
+ set({ error: error as Error });
+ void window.service.native.log('error', 'Failed to load agent', { function: 'agentActions.loadAgent', error });
} finally {
set({ loading: false });
}
@@ -114,9 +114,9 @@ export const agentActions = (
});
return processedData.agent;
- } catch (error_) {
- set({ error: error_ instanceof Error ? error_ : new Error(String(error_)) });
- void window.service.native.log('error', 'Failed to create agent', { function: 'agentActions.createAgent', error: String(error_) });
+ } catch (error) {
+ set({ error: error as Error });
+ void window.service.native.log('error', 'Failed to create agent', { function: 'agentActions.createAgent', error });
return null;
} finally {
set({ loading: false });
@@ -147,9 +147,9 @@ export const agentActions = (
});
return processedData.agent;
- } catch (error_) {
- set({ error: error_ instanceof Error ? error_ : new Error(String(error_)) });
- void window.service.native.log('error', 'Failed to update agent', { function: 'agentActions.updateAgent', error: String(error_) });
+ } catch (error) {
+ set({ error: error as Error });
+ void window.service.native.log('error', 'Failed to update agent', { function: 'agentActions.updateAgent', error });
return null;
} finally {
set({ loading: false });
@@ -179,7 +179,7 @@ export const agentActions = (
} catch (error) {
const isInitialCall = !get().agent;
set({
- error: error instanceof Error ? error : new Error(String(error)),
+ error: error as Error,
...(isInitialCall ? { loading: false } : {}),
});
}
@@ -246,7 +246,7 @@ export const agentActions = (
`Error in message subscription for ${message.id}`,
{
function: 'agentActions.subscribeToUpdates.messageSubscription',
- error: String(error_),
+ error: error_,
},
);
},
@@ -281,10 +281,10 @@ export const agentActions = (
'Error in agent subscription',
{
function: 'agentActions.subscribeToUpdates.agentSubscription',
- error: String(error_),
+ error: error_,
},
);
- set({ error: error_ instanceof Error ? error_ : new Error(String(error_)) });
+ set({ error: error_ as Error });
},
});
@@ -295,9 +295,9 @@ export const agentActions = (
subscription.unsubscribe();
});
};
- } catch (error_) {
- void window.service.native.log('error', 'Failed to subscribe to agent updates', { function: 'agentActions.subscribeToUpdates', error: String(error_) });
- set({ error: error_ instanceof Error ? error_ : new Error(String(error_)) });
+ } catch (error) {
+ void window.service.native.log('error', 'Failed to subscribe to agent updates', { function: 'agentActions.subscribeToUpdates', error });
+ set({ error: error as Error });
return undefined;
}
},
@@ -347,8 +347,8 @@ export const agentActions = (
try {
await window.service.agentInstance.cancelAgent(storeAgent.id);
- } catch (error_) {
- void window.service.native.log('error', 'Store: cancelAgent backend call failed', { function: 'agentActions.cancelAgent', agentId: storeAgent.id, error: String(error_) });
+ } catch (error) {
+ void window.service.native.log('error', 'Store: cancelAgent backend call failed', { function: 'agentActions.cancelAgent', agentId: storeAgent.id, error });
}
},
});
diff --git a/src/pages/Agent/store/agentChatStore/actions/messageActions.ts b/src/pages/Agent/store/agentChatStore/actions/messageActions.ts
index 5664ce63..c38b8a4f 100644
--- a/src/pages/Agent/store/agentChatStore/actions/messageActions.ts
+++ b/src/pages/Agent/store/agentChatStore/actions/messageActions.ts
@@ -44,11 +44,11 @@ export const messageActions = (
set({ loading: true });
await window.service.agentInstance.sendMsgToAgent(storeAgent.id, { text: content });
} catch (error) {
- set({ error: error instanceof Error ? error : new Error(String(error)) });
+ set({ error: error as Error });
void window.service.native.log(
'error',
'Failed to send message',
- { function: 'messageActions.sendMessage', error: String(error) },
+ { function: 'messageActions.sendMessage', error },
);
} finally {
set({ loading: false });
diff --git a/src/pages/Agent/store/agentChatStore/actions/previewActions.ts b/src/pages/Agent/store/agentChatStore/actions/previewActions.ts
index d4967375..add8fc3d 100644
--- a/src/pages/Agent/store/agentChatStore/actions/previewActions.ts
+++ b/src/pages/Agent/store/agentChatStore/actions/previewActions.ts
@@ -148,7 +148,7 @@ export const previewActionsMiddleware: StateCreator {
completed = true;
diff --git a/src/pages/Agent/store/tabStore/actions/basicActions.ts b/src/pages/Agent/store/tabStore/actions/basicActions.ts
index 9a024c2c..0f8b5a26 100644
--- a/src/pages/Agent/store/tabStore/actions/basicActions.ts
+++ b/src/pages/Agent/store/tabStore/actions/basicActions.ts
@@ -265,7 +265,7 @@ export const basicActionsMiddleware: StateCreator<
activeTabId,
});
} catch (error) {
- void window.service.native.log('error', 'Failed to add tab:', { error: String(error) });
+ void window.service.native.log('error', 'Failed to add tab:', { error });
console.error('Failed to add tab:', error);
}
diff --git a/src/pages/ChatTabContent/components/ChatTitle.tsx b/src/pages/ChatTabContent/components/ChatTitle.tsx
index 514f47fe..e2cb7bc0 100644
--- a/src/pages/ChatTabContent/components/ChatTitle.tsx
+++ b/src/pages/ChatTabContent/components/ChatTitle.tsx
@@ -65,7 +65,7 @@ export const ChatTitle: React.FC = ({ title, agent, updateAgent
await updateAgent({ name: newTitle });
}
} catch (error) {
- void window.service?.native?.log?.('error', 'Failed to save agent title', { function: 'ChatTitle.handleSaveEdit', error: String(error) });
+ void window.service?.native?.log?.('error', 'Failed to save agent title', { function: 'ChatTitle.handleSaveEdit', error });
}
};
diff --git a/src/pages/Help/useLoadHelpPagesList.ts b/src/pages/Help/useLoadHelpPagesList.ts
index 98e8a4c1..2986f19c 100644
--- a/src/pages/Help/useLoadHelpPagesList.ts
+++ b/src/pages/Help/useLoadHelpPagesList.ts
@@ -20,7 +20,7 @@ export function useLoadHelpPagesList(language = 'en-GB') {
const data = await fetch(source).then(async response => await (response.json() as Promise));
return data.map(makeFallbackUrlsArray);
} catch (error) {
- await window.service.native.log('error', `Help page Failed to load online source: ${source} ${(error as Error).message}`);
+ await window.service.native.log('error', `Help page Failed to load online source: ${source}`, { error });
return [];
}
}),
@@ -28,7 +28,7 @@ export function useLoadHelpPagesList(language = 'en-GB') {
const newItems = responses.flat();
setItems(currentItems => uniqBy([...currentItems, ...newItems], 'url'));
} catch (error) {
- void window.service.native.log('error', 'Failed to load online sources', { function: 'useLoadHelpPagesList.loadMoreItems', error: String(error) });
+ void window.service.native.log('error', 'Failed to load online sources', { function: 'useLoadHelpPagesList.loadMoreItems', error });
}
};
diff --git a/src/pages/Main/Sidebar.tsx b/src/pages/Main/Sidebar.tsx
index fec473b4..52d7d63c 100644
--- a/src/pages/Main/Sidebar.tsx
+++ b/src/pages/Main/Sidebar.tsx
@@ -71,13 +71,13 @@ const IconButton = styled(IconButtonRaw)`
color: ${({ theme }) => theme.palette.action.active};
`;
-const SidebarContainer = ({ children }: { children: React.ReactNode }): React.JSX.Element => {
+const SidebarContainer = ({ children, ...props }: { children: React.ReactNode } & React.HTMLAttributes): React.JSX.Element => {
const platform = usePromiseValue(async () => await window.service.context.get('platform'));
// use native scroll bar on macOS
if (platform === 'darwin') {
- return {children};
+ return {children};
}
- return {children};
+ return {children};
};
export function SideBar(): React.JSX.Element {
@@ -92,7 +92,7 @@ export function SideBar(): React.JSX.Element {
const { showSideBarText, showSideBarIcon } = preferences;
return (
-
+
{workspacesList === undefined
? {t('Loading')}
diff --git a/src/pages/Main/WorkspaceIconAndSelector/SortableWorkspaceSelectorButton.tsx b/src/pages/Main/WorkspaceIconAndSelector/SortableWorkspaceSelectorButton.tsx
index b4a1d0ea..3ccb83be 100644
--- a/src/pages/Main/WorkspaceIconAndSelector/SortableWorkspaceSelectorButton.tsx
+++ b/src/pages/Main/WorkspaceIconAndSelector/SortableWorkspaceSelectorButton.tsx
@@ -1,16 +1,17 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
-import { getWorkspaceMenuTemplate } from '@services/workspaces/getWorkspaceMenuTemplate';
-import { isWikiWorkspace, IWorkspaceWithMetadata } from '@services/workspaces/interface';
import { MouseEvent, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { WorkspaceSelectorBase } from './WorkspaceSelectorBase';
+import { useLocation } from 'wouter';
import { PageType } from '@/constants/pageTypes';
import { getBuildInPageIcon } from '@/pages/Main/WorkspaceIconAndSelector/getBuildInPageIcon';
import { getBuildInPageName } from '@/pages/Main/WorkspaceIconAndSelector/getBuildInPageName';
+import { usePreferenceObservable } from '@services/preferences/hooks';
import { WindowNames } from '@services/windows/WindowProperties';
-import { useLocation } from 'wouter';
+import { getWorkspaceMenuTemplate } from '@services/workspaces/getWorkspaceMenuTemplate';
+import { isWikiWorkspace, IWorkspaceWithMetadata } from '@services/workspaces/interface';
+import { WorkspaceSelectorBase } from './WorkspaceSelectorBase';
export interface ISortableItemProps {
index: number;
@@ -22,6 +23,7 @@ export interface ISortableItemProps {
export function SortableWorkspaceSelectorButton({ index, workspace, showSidebarTexts, showSideBarIcon }: ISortableItemProps): React.JSX.Element {
const { t } = useTranslation();
const { active, id, name, picturePath, pageType } = workspace;
+ const preference = usePreferenceObservable();
const isWiki = isWikiWorkspace(workspace);
const hibernated = isWiki ? workspace.hibernated : false;
@@ -49,20 +51,41 @@ export function SortableWorkspaceSelectorButton({ index, workspace, showSidebarT
}
return undefined;
}, [pageType]);
+
+ const isMiniWindow = window.meta().windowName === WindowNames.tidgiMiniWindow;
+
+ // Determine active state based on window type
+ const isActive = useMemo(() => {
+ if (isMiniWindow) {
+ // In mini window, compare with tidgiMiniWindowFixedWorkspaceId
+ return preference?.tidgiMiniWindowFixedWorkspaceId === id;
+ }
+ // In main window, use workspace's active state
+ return active;
+ }, [isMiniWindow, preference?.tidgiMiniWindowFixedWorkspaceId, id, active]);
+
const onWorkspaceClick = useCallback(async () => {
workspaceClickedLoadingSetter(true);
try {
+ // Special "add" workspace always opens add workspace window
+ if (workspace.pageType === PageType.add) {
+ await window.service.window.open(WindowNames.addWorkspace);
+ return;
+ }
+
+ // In mini window, only update the fixed workspace ID
+ if (isMiniWindow) {
+ await window.service.preference.set('tidgiMiniWindowFixedWorkspaceId', id);
+ return;
+ }
+
+ // In main window, handle different workspace types
if (workspace.pageType) {
- // Handle special "add" workspace
- if (workspace.pageType === PageType.add) {
- await window.service.window.open(WindowNames.addWorkspace);
- } else {
- // Handle other page workspaces - navigate to the page and set as active workspace
- setLocation(`/${workspace.pageType}`);
- await window.service.workspaceView.setActiveWorkspaceView(id);
- }
+ // Page workspaces (dashboard, etc.)
+ setLocation(`/${workspace.pageType}`);
+ await window.service.workspaceView.setActiveWorkspaceView(id);
} else {
- // Handle regular wiki workspace
+ // Regular wiki workspace
setLocation(`/${PageType.wiki}/${id}/`);
await window.service.workspace.openWorkspaceTiddler(workspace);
}
@@ -73,7 +96,7 @@ export function SortableWorkspaceSelectorButton({ index, workspace, showSidebarT
} finally {
workspaceClickedLoadingSetter(false);
}
- }, [id, setLocation, workspace]);
+ }, [id, setLocation, workspace, isMiniWindow]);
const onWorkspaceContextMenu = useCallback(
async (event: MouseEvent) => {
event.preventDefault();
@@ -90,7 +113,7 @@ export function SortableWorkspaceSelectorButton({ index, workspace, showSidebarT
restarting={workspace.metadata.isRestarting}
showSideBarIcon={showSideBarIcon}
onClick={onWorkspaceClick}
- active={active}
+ active={isActive}
id={id}
key={id}
pageType={pageType || undefined}
diff --git a/src/pages/Main/WorkspaceIconAndSelector/SortableWorkspaceSelectorList.tsx b/src/pages/Main/WorkspaceIconAndSelector/SortableWorkspaceSelectorList.tsx
index 8d0880d3..2227e6ec 100644
--- a/src/pages/Main/WorkspaceIconAndSelector/SortableWorkspaceSelectorList.tsx
+++ b/src/pages/Main/WorkspaceIconAndSelector/SortableWorkspaceSelectorList.tsx
@@ -1,6 +1,10 @@
import { DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
+import { useMemo } from 'react';
+
+import { PageType } from '@/constants/pageTypes';
+import { WindowNames } from '@services/windows/WindowProperties';
import { IWorkspace, IWorkspaceWithMetadata } from '@services/workspaces/interface';
import { SortableWorkspaceSelectorButton } from './SortableWorkspaceSelectorButton';
@@ -19,7 +23,17 @@ export function SortableWorkspaceSelectorList({ workspacesList, showSideBarText,
}),
);
- const workspaceIDs = workspacesList.map((workspace) => workspace.id);
+ const isMiniWindow = window.meta().windowName === WindowNames.tidgiMiniWindow;
+
+ // Filter out 'add' workspace in mini window
+ const filteredWorkspacesList = useMemo(() => {
+ if (isMiniWindow) {
+ return workspacesList.filter((workspace) => workspace.pageType !== PageType.add);
+ }
+ return workspacesList;
+ }, [isMiniWindow, workspacesList]);
+
+ const workspaceIDs = filteredWorkspacesList.map((workspace) => workspace.id);
return (
- {workspacesList
+ {filteredWorkspacesList
.sort((a, b) => a.order - b.order)
.map((workspace, index) => (
{} : onClick}
data-testid={pageType ? `workspace-${pageType}` : `workspace-${id}`}
+ data-active={active ? 'true' : 'false'}
>
{icon}
diff --git a/src/pages/Main/__tests__/index.test.tsx b/src/pages/Main/__tests__/index.test.tsx
index da45b3a5..aff63b12 100644
--- a/src/pages/Main/__tests__/index.test.tsx
+++ b/src/pages/Main/__tests__/index.test.tsx
@@ -14,7 +14,7 @@ import Main from '../index';
// Mock window.observables to provide realistic API behavior
const preferencesSubject = new BehaviorSubject({
sidebar: true,
- sidebarOnMenubar: true,
+ tidgiMiniWindowShowSidebar: true,
showSideBarText: true,
showSideBarIcon: true,
});
diff --git a/src/pages/Main/index.tsx b/src/pages/Main/index.tsx
index baaf136b..ef9f57ec 100644
--- a/src/pages/Main/index.tsx
+++ b/src/pages/Main/index.tsx
@@ -57,19 +57,19 @@ const ContentRoot = styled('div')<{ $sidebar: boolean }>`
height: 100%;
`;
-const windowName = window.meta().windowName;
-
export default function Main(): React.JSX.Element {
const { t } = useTranslation();
useInitialPage();
+ const windowName = window.meta().windowName;
const preferences = usePreferenceObservable();
- const showSidebar = (windowName === WindowNames.menuBar ? preferences?.sidebarOnMenubar : preferences?.sidebar) ?? true;
+ const isTidgiMiniWindow = windowName === WindowNames.tidgiMiniWindow;
+ const showSidebar = (isTidgiMiniWindow ? preferences?.tidgiMiniWindowShowSidebar : preferences?.sidebar) ?? true;
return (
- {t('Menu.TidGi')}
+ {t('Menu.TidGi')}{isTidgiMiniWindow ? ` - ${t('Menu.TidGiMiniWindow')}` : ''}
-
+
{showSidebar && }
diff --git a/src/pages/Main/useInitialPage.ts b/src/pages/Main/useInitialPage.ts
index 4ce786b6..44872221 100644
--- a/src/pages/Main/useInitialPage.ts
+++ b/src/pages/Main/useInitialPage.ts
@@ -1,31 +1,92 @@
import { PageType } from '@/constants/pageTypes';
+import { usePreferenceObservable } from '@services/preferences/hooks';
+import type { IPreferences } from '@services/preferences/interface';
+import { WindowNames } from '@services/windows/WindowProperties';
import { useWorkspacesListObservable } from '@services/workspaces/hooks';
+import type { IWorkspaceWithMetadata } from '@services/workspaces/interface';
import { useEffect, useRef } from 'react';
import { useLocation } from 'wouter';
+/**
+ * Helper function to determine the target workspace for tidgi mini window based on preferences
+ */
+function getTidgiMiniWindowTargetWorkspace(
+ workspacesList: IWorkspaceWithMetadata[],
+ preferences: IPreferences,
+): IWorkspaceWithMetadata | undefined {
+ const { tidgiMiniWindowSyncWorkspaceWithMainWindow, tidgiMiniWindowFixedWorkspaceId } = preferences;
+ // Default to sync (undefined means default to true, or explicitly true)
+ const shouldSync = tidgiMiniWindowSyncWorkspaceWithMainWindow === undefined || tidgiMiniWindowSyncWorkspaceWithMainWindow;
+
+ if (shouldSync) {
+ // Sync with main window - use active workspace
+ return workspacesList.find(workspace => workspace.active);
+ } else if (tidgiMiniWindowFixedWorkspaceId) {
+ // Use fixed workspace
+ return workspacesList.find(ws => ws.id === tidgiMiniWindowFixedWorkspaceId);
+ }
+ // No fixed workspace set - return undefined
+ return undefined;
+}
+
export function useInitialPage() {
const [location, setLocation] = useLocation();
const workspacesList = useWorkspacesListObservable();
+ const preferences = usePreferenceObservable();
const hasInitialized = useRef(false);
+ const windowName = window.meta().windowName;
+
useEffect(() => {
// Only initialize once and only when at root
if (workspacesList && !hasInitialized.current && (location === '/' || location === '')) {
hasInitialized.current = true;
- const activeWorkspace = workspacesList.find(workspace => workspace.active);
- if (!activeWorkspace) {
+
+ let targetWorkspace = workspacesList.find(workspace => workspace.active);
+
+ // For tidgi mini window, determine which workspace to show based on preferences
+ if (windowName === WindowNames.tidgiMiniWindow && preferences) {
+ targetWorkspace = getTidgiMiniWindowTargetWorkspace(workspacesList, preferences) || targetWorkspace;
+ }
+
+ if (!targetWorkspace) {
// If there's no active workspace, navigate to root instead of guide.
// Root lets the UI stay neutral and prevents forcing the guide view.
setLocation(`/`);
- } else if (activeWorkspace.pageType) {
+ } else if (targetWorkspace.pageType) {
// Don't navigate to add page, fallback to guide instead
- if (activeWorkspace.pageType === PageType.add) {
+ if (targetWorkspace.pageType === PageType.add) {
setLocation(`/`);
} else {
- setLocation(`/${activeWorkspace.pageType}`);
+ setLocation(`/${targetWorkspace.pageType}`);
}
} else {
- setLocation(`/${PageType.wiki}/${activeWorkspace.id}/`);
+ setLocation(`/${PageType.wiki}/${targetWorkspace.id}/`);
}
}
- }, [location, workspacesList, setLocation]);
+ }, [location, workspacesList, preferences, windowName, setLocation]);
+
+ // For tidgi mini window, also listen to active workspace changes
+ useEffect(() => {
+ if (windowName !== WindowNames.tidgiMiniWindow || !workspacesList || !preferences) {
+ return;
+ }
+
+ // Determine target workspace using helper function
+ const targetWorkspace = getTidgiMiniWindowTargetWorkspace(workspacesList, preferences);
+
+ if (!targetWorkspace) return;
+
+ // Navigate to the target workspace's page
+ let targetPath = '/';
+ if (targetWorkspace.pageType && targetWorkspace.pageType !== PageType.add) {
+ targetPath = `/${targetWorkspace.pageType}`;
+ } else if (!targetWorkspace.pageType) {
+ targetPath = `/${PageType.wiki}/${targetWorkspace.id}/`;
+ }
+
+ // Only navigate if we're not already on the target path
+ if (location !== targetPath) {
+ setLocation(targetPath);
+ }
+ }, [windowName, workspacesList, preferences, location, setLocation]);
}
diff --git a/src/preload/common/remote.ts b/src/preload/common/remote.ts
index 596b9e58..1baed2ed 100644
--- a/src/preload/common/remote.ts
+++ b/src/preload/common/remote.ts
@@ -34,7 +34,7 @@ export const remoteMethods = {
* @returns — the index of the clicked button. -1 means unknown or errored. 0 if canceled (this can be configured by `cancelId` in the options).
*/
showElectronMessageBoxSync: (options: Electron.MessageBoxSyncOptions): number => {
- // only main window can show message box, view window (browserView) can't. Currently didn't handle menubar window, hope it won't show message box...
+ // only main window can show message box, view window (browserView) can't. Currently didn't handle tidgi mini window, hope it won't show message box...
const clickedButtonIndex = ipcRenderer.sendSync(NativeChannel.showElectronMessageBoxSync, options, WindowNames.main) as unknown;
if (typeof clickedButtonIndex === 'number') {
return clickedButtonIndex;
diff --git a/src/renderer.tsx b/src/renderer.tsx
index 75449dc6..90bda852 100644
--- a/src/renderer.tsx
+++ b/src/renderer.tsx
@@ -1,6 +1,6 @@
import { ThemeProvider } from '@mui/material/styles';
import i18next from 'i18next';
-import React, { JSX, StrictMode, Suspense } from 'react';
+import React, { JSX, StrictMode, Suspense, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import { I18nextProvider } from 'react-i18next';
import { Router } from 'wouter';
@@ -24,10 +24,12 @@ import { initRendererI18N } from './services/libs/i18n/renderer';
import 'electron-ipc-cat/fixContextIsolation';
import { useHashLocation } from 'wouter/use-hash-location';
import { RootStyle } from './components/RootStyle';
+import { initTestKeyboardShortcutFallback } from './helpers/testKeyboardShortcuts';
import { Pages } from './windows';
function App(): JSX.Element {
const theme = useThemeObservable();
+ useEffect(() => initTestKeyboardShortcutFallback(), []);
return (
diff --git a/src/services/agentBrowser/index.ts b/src/services/agentBrowser/index.ts
index b3a8935d..57807e55 100644
--- a/src/services/agentBrowser/index.ts
+++ b/src/services/agentBrowser/index.ts
@@ -49,8 +49,7 @@ export class AgentBrowserService implements IAgentBrowserService {
this.tabRepository = this.dataSource.getRepository(AgentBrowserTabEntity);
logger.debug('Agent browser repository initialized');
} catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- logger.error(`Failed to initialize agent browser service: ${errorMessage}`);
+ logger.error('Failed to initialize agent browser service', { error });
throw error;
}
}
@@ -220,7 +219,7 @@ export class AgentBrowserService implements IAgentBrowserService {
} catch (error) {
logger.error('Failed to get tabs', {
function: 'getAllTabs',
- error: error instanceof Error ? error.message : String(error),
+ error,
});
throw error;
}
@@ -242,7 +241,7 @@ export class AgentBrowserService implements IAgentBrowserService {
} catch (error) {
logger.error('Failed to get active tab', {
function: 'getActiveTabId',
- error: error instanceof Error ? error.message : String(error),
+ error,
});
throw error;
}
@@ -277,7 +276,7 @@ export class AgentBrowserService implements IAgentBrowserService {
} catch (error) {
logger.error('Failed to set active tab', {
function: 'setActiveTab',
- error: error instanceof Error ? error.message : String(error),
+ error,
});
throw error;
}
@@ -321,7 +320,7 @@ export class AgentBrowserService implements IAgentBrowserService {
return savedTab;
} catch (error) {
- logger.error(`Failed to add tab: ${error as Error}`);
+ logger.error('Failed to add tab', { error });
throw error;
}
}
@@ -392,7 +391,7 @@ export class AgentBrowserService implements IAgentBrowserService {
await this.tabRepository!.save(existingTab);
await this.updateTabsObservable();
} catch (error) {
- logger.error(`Failed to update tab: ${error as Error}`);
+ logger.error('Failed to update tab', { error });
throw error;
}
}
@@ -458,7 +457,7 @@ export class AgentBrowserService implements IAgentBrowserService {
await this.reindexTabPositions();
await this.updateTabsObservable();
} catch (error) {
- logger.error(`Failed to close tab: ${error instanceof Error ? `${error.message} ${error.stack}` : String(error)}`);
+ logger.error('Failed to close tab', { error });
throw error;
}
}
@@ -560,7 +559,7 @@ export class AgentBrowserService implements IAgentBrowserService {
} catch (error) {
logger.error('Failed to get closed tabs', {
function: 'getClosedTabs',
- error: error instanceof Error ? error.message : String(error),
+ error,
});
throw error;
}
@@ -606,7 +605,7 @@ export class AgentBrowserService implements IAgentBrowserService {
} catch (error) {
logger.error('Failed to restore closed tab', {
function: 'restoreClosedTab',
- error: error instanceof Error ? error.message : String(error),
+ error,
});
throw error;
}
@@ -648,7 +647,7 @@ export class AgentBrowserService implements IAgentBrowserService {
} catch (error) {
logger.error('Failed to reindex tab positions', {
function: 'reindexTabPositions',
- error: error instanceof Error ? error.message : String(error),
+ error,
});
}
}
diff --git a/src/services/agentInstance/buildInAgentHandlers/basicPromptConcatHandler.ts b/src/services/agentInstance/buildInAgentHandlers/basicPromptConcatHandler.ts
index 2e4a863f..9946a103 100644
--- a/src/services/agentInstance/buildInAgentHandlers/basicPromptConcatHandler.ts
+++ b/src/services/agentInstance/buildInAgentHandlers/basicPromptConcatHandler.ts
@@ -241,13 +241,13 @@ export async function* basicPromptConcatHandler(context: AgentHandlerContext) {
(tm).metadata = { ...(tm).metadata, isPersisted: true };
} catch (error1) {
logger.warn('Failed to persist pending tool result before error', {
- error: error1 instanceof Error ? error1.message : String(error1),
+ error: error1,
messageId: tm.id,
});
}
}
} catch (error2) {
- logger.warn('Failed to flush pending tool messages before persisting error', { error: error2 instanceof Error ? error2.message : String(error2) });
+ logger.warn('Failed to flush pending tool messages before persisting error', { error: error2 });
}
// Push an explicit error message into history for UI rendering
@@ -269,7 +269,7 @@ export async function* basicPromptConcatHandler(context: AgentHandlerContext) {
await agentInstanceService.saveUserMessage(errorMessageForHistory);
} catch (persistError) {
logger.warn('Failed to persist error message to database', {
- error: persistError instanceof Error ? persistError.message : String(persistError),
+ error: persistError,
messageId: errorMessageForHistory.id,
agentId: context.agent.id,
});
@@ -286,11 +286,8 @@ export async function* basicPromptConcatHandler(context: AgentHandlerContext) {
});
currentRequestId = undefined;
} catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- logger.error('Unexpected error during AI generation', {
- error: errorMessage,
- });
- yield completed(`Unexpected error: ${errorMessage}`, context);
+ logger.error('Unexpected error during AI generation', { error });
+ yield completed(`Unexpected error: ${(error as Error).message}`, context);
} finally {
if (context.isCancelled() && currentRequestId) {
logger.debug('Cancelling AI request in finally block', {
@@ -304,12 +301,11 @@ export async function* basicPromptConcatHandler(context: AgentHandlerContext) {
// Start processing with the initial user message
yield* processLLMCall();
} catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('Error processing prompt', {
method: 'basicPromptConcatHandler',
agentId: context.agent.id,
- error: errorMessage,
+ error,
});
- yield completed(`Error processing prompt: ${errorMessage}`, context);
+ yield completed(`Error processing prompt: ${(error as Error).message}`, context);
}
}
diff --git a/src/services/agentInstance/index.ts b/src/services/agentInstance/index.ts
index bc2fbbd4..9b6fbe6c 100644
--- a/src/services/agentInstance/index.ts
+++ b/src/services/agentInstance/index.ts
@@ -44,8 +44,7 @@ export class AgentInstanceService implements IAgentInstanceService {
await this.initializeDatabase();
await this.initializeHandlers();
} catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- logger.error(`Failed to initialize agent instance service: ${errorMessage}`);
+ logger.error('Failed to initialize agent instance service', { error });
throw error;
}
}
@@ -58,8 +57,7 @@ export class AgentInstanceService implements IAgentInstanceService {
this.agentMessageRepository = this.dataSource.getRepository(AgentInstanceMessageEntity);
logger.debug('AgentInstance repositories initialized');
} catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- logger.error(`Failed to initialize agent instance database: ${errorMessage}`);
+ logger.error('Failed to initialize agent instance database', { error });
throw error;
}
}
@@ -74,8 +72,7 @@ export class AgentInstanceService implements IAgentInstanceService {
this.registerBuiltinHandlers();
logger.debug('AgentInstance handlers registered');
} catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- logger.error(`Failed to initialize agent instance handlers: ${errorMessage}`);
+ logger.error('Failed to initialize agent instance handlers', { error });
throw error;
}
}
@@ -163,8 +160,7 @@ export class AgentInstanceService implements IAgentInstanceService {
modified: now,
};
} catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- logger.error(`Failed to create agent instance: ${errorMessage}`);
+ logger.error('Failed to create agent instance', { error });
throw error;
}
}
@@ -195,8 +191,7 @@ export class AgentInstanceService implements IAgentInstanceService {
messages,
};
} catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- logger.error(`Failed to get agent instance: ${errorMessage}`);
+ logger.error('Failed to get agent instance', { error });
throw error;
}
}
@@ -271,8 +266,7 @@ export class AgentInstanceService implements IAgentInstanceService {
return updatedAgent;
} catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- logger.error(`Failed to update agent instance: ${errorMessage}`);
+ logger.error('Failed to update agent instance', { error });
throw error;
}
}
@@ -292,8 +286,7 @@ export class AgentInstanceService implements IAgentInstanceService {
logger.info(`Deleted agent instance: ${agentId}`);
} catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- logger.error(`Failed to delete agent instance: ${errorMessage}`);
+ logger.error('Failed to delete agent instance', { error });
throw error;
}
}
@@ -337,8 +330,7 @@ export class AgentInstanceService implements IAgentInstanceService {
return instances.map(entity => pick(entity, AGENT_INSTANCE_FIELDS));
} catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- logger.error(`Failed to get agent instances: ${errorMessage}`);
+ logger.error('Failed to get agent instances', { error });
throw error;
}
}
@@ -540,7 +532,7 @@ export class AgentInstanceService implements IAgentInstanceService {
}
}
} catch (error) {
- logger.warn('Failed to propagate cancel status to message subscriptions', { function: 'cancelAgent', error: String(error) });
+ logger.warn('Failed to propagate cancel status to message subscriptions', { function: 'cancelAgent', error });
}
// Remove cancel token from map
@@ -634,7 +626,7 @@ export class AgentInstanceService implements IAgentInstanceService {
}
}
}).catch((error: unknown) => {
- logger.error('Failed to get initial status for message', { function: 'subscribeToAgentUpdates', error: String(error) });
+ logger.error('Failed to get initial status for message', { function: 'subscribeToAgentUpdates', error });
});
}
@@ -649,7 +641,7 @@ export class AgentInstanceService implements IAgentInstanceService {
this.getAgent(agentId).then(agent => {
this.agentInstanceSubjects.get(agentId)?.next(agent);
}).catch((error: unknown) => {
- logger.error('Failed to get initial agent data', { function: 'subscribeToAgentUpdates', error: String(error) });
+ logger.error('Failed to get initial agent data', { function: 'subscribeToAgentUpdates', error });
});
}
@@ -700,8 +692,8 @@ export class AgentInstanceService implements IAgentInstanceService {
source: 'saveUserMessage',
});
} catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- logger.error(`Failed to save user message: ${errorMessage}`, {
+ logger.error('Failed to save user message', {
+ error,
messageId: userMessage.id,
agentId: userMessage.agentId,
});
@@ -840,8 +832,7 @@ export class AgentInstanceService implements IAgentInstanceService {
}
});
} catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- logger.error(`Failed to update/create message content: ${errorMessage}`);
+ logger.error('Failed to update/create message content', { error });
}
},
debounceMs,
@@ -891,7 +882,7 @@ export class AgentInstanceService implements IAgentInstanceService {
}
} catch (error) {
logger.error('Error in AgentInstanceService.concatPrompt', {
- error: error instanceof Error ? error.message : String(error),
+ error,
promptDescriptionId: (promptDescription as AgentPromptDescription).id,
messagesCount: messages.length,
});
@@ -915,7 +906,7 @@ export class AgentInstanceService implements IAgentInstanceService {
return { type: 'object', properties: {} };
} catch (error) {
logger.error('Error in AgentInstanceService.getHandlerConfigSchema', {
- error: error instanceof Error ? error.message : String(error),
+ error,
handlerId,
});
throw error;
diff --git a/src/services/agentInstance/plugins/messageManagementPlugin.ts b/src/services/agentInstance/plugins/messageManagementPlugin.ts
index 8b99752b..496e667b 100644
--- a/src/services/agentInstance/plugins/messageManagementPlugin.ts
+++ b/src/services/agentInstance/plugins/messageManagementPlugin.ts
@@ -47,7 +47,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
callback();
} catch (error) {
logger.error('Message management plugin error in userMessageReceived', {
- error: error instanceof Error ? error.message : String(error),
+ error,
messageId: context.messageId,
agentId: context.handlerContext.agent.id,
});
@@ -79,7 +79,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
callback();
} catch (error) {
logger.error('Message management plugin error in agentStatusChanged', {
- error: error instanceof Error ? error.message : String(error),
+ error,
agentId: context.handlerContext.agent.id,
status: context.status,
});
@@ -119,7 +119,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
aiMessage.metadata = { ...aiMessage.metadata, isPersisted: true };
} catch (persistError) {
logger.warn('Failed to persist initial streaming AI message', {
- error: persistError instanceof Error ? persistError.message : String(persistError),
+ error: persistError,
messageId: aiMessage.id,
});
}
@@ -135,14 +135,14 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
agentInstanceService.debounceUpdateMessage(aiMessage, handlerContext.agent.id);
} catch (serviceError) {
logger.warn('Failed to update UI for streaming message', {
- error: serviceError instanceof Error ? serviceError.message : String(serviceError),
+ error: serviceError,
messageId: aiMessage.id,
});
}
}
} catch (error) {
logger.error('Message management plugin error in responseUpdate', {
- error: error instanceof Error ? error.message : String(error),
+ error,
});
} finally {
callback();
@@ -194,7 +194,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
agentInstanceService.debounceUpdateMessage(aiMessage, handlerContext.agent.id);
} catch (serviceError) {
logger.warn('Failed to update UI for completed message', {
- error: serviceError instanceof Error ? serviceError.message : String(serviceError),
+ error: serviceError,
messageId: aiMessage.id,
});
}
@@ -208,7 +208,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
callback();
} catch (error) {
logger.error('Message management plugin error in responseComplete', {
- error: error instanceof Error ? error.message : String(error),
+ error,
});
callback();
}
@@ -245,7 +245,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
});
} catch (serviceError) {
logger.error('Failed to persist tool result message', {
- error: serviceError instanceof Error ? serviceError.message : String(serviceError),
+ error: serviceError,
messageId: message.id,
});
}
@@ -261,7 +261,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
callback();
} catch (error) {
logger.error('Message management plugin error in toolExecuted', {
- error: error instanceof Error ? error.message : String(error),
+ error,
});
callback();
}
diff --git a/src/services/agentInstance/plugins/wikiOperationPlugin.ts b/src/services/agentInstance/plugins/wikiOperationPlugin.ts
index 6ecb01a3..f33dbe72 100644
--- a/src/services/agentInstance/plugins/wikiOperationPlugin.ts
+++ b/src/services/agentInstance/plugins/wikiOperationPlugin.ts
@@ -168,7 +168,7 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
callback();
} catch (error) {
logger.error('Error in wiki operation tool list injection', {
- error: error instanceof Error ? error.message : String(error),
+ error,
pluginId: pluginConfig.id,
});
callback();
@@ -340,7 +340,7 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
toolResultMessage.metadata = { ...toolResultMessage.metadata, isPersisted: true };
} catch (persistError) {
logger.warn('Failed to persist tool result immediately in wikiOperationPlugin', {
- error: persistError instanceof Error ? persistError.message : String(persistError),
+ error: persistError,
messageId: toolResultMessage.id,
});
}
@@ -369,7 +369,7 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
});
} catch (error) {
logger.error('Wiki operation tool execution failed', {
- error: error instanceof Error ? error.message : String(error),
+ error,
agentId: handlerContext.agent.id,
toolParameters: toolMatch.parameters,
});
@@ -425,9 +425,7 @@ Error: ${error instanceof Error ? error.message : String(error)}
callback();
} catch (error) {
- logger.error('Error in wiki operation plugin response handler', {
- error: error instanceof Error ? error.message : String(error),
- });
+ logger.error('Error in wiki operation plugin response handler', { error });
callback();
}
});
diff --git a/src/services/agentInstance/plugins/wikiSearchPlugin.ts b/src/services/agentInstance/plugins/wikiSearchPlugin.ts
index 2436c52d..3c888b51 100644
--- a/src/services/agentInstance/plugins/wikiSearchPlugin.ts
+++ b/src/services/agentInstance/plugins/wikiSearchPlugin.ts
@@ -262,7 +262,7 @@ async function executeWikiSearchTool(
}
} catch (error) {
logger.warn(`Error retrieving full tiddler content for ${result.title}`, {
- error: error instanceof Error ? error.message : String(error),
+ error,
});
fullContentResults.push(result);
}
@@ -278,7 +278,7 @@ async function executeWikiSearchTool(
};
} catch (error) {
logger.error('Vector search failed', {
- error: error instanceof Error ? error.message : String(error),
+ error,
workspaceID,
query,
});
@@ -327,7 +327,7 @@ async function executeWikiSearchTool(
}
} catch (error) {
logger.warn(`Error retrieving tiddler content for ${title}`, {
- error: error instanceof Error ? error.message : String(error),
+ error,
});
results.push({ title });
}
@@ -377,7 +377,7 @@ async function executeWikiSearchTool(
};
} catch (error) {
logger.error('Wiki search tool execution error', {
- error: error instanceof Error ? error.message : String(error),
+ error,
parameters,
});
@@ -471,7 +471,7 @@ async function executeWikiUpdateEmbeddingsTool(
};
} catch (error) {
logger.error('Wiki update embeddings tool execution error', {
- error: error instanceof Error ? error.message : String(error),
+ error,
parameters,
});
@@ -551,7 +551,7 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
callback();
} catch (error) {
logger.error('Error in wiki search tool list injection', {
- error: error instanceof Error ? error.message : String(error),
+ error,
pluginId: pluginConfig.id,
});
callback();
@@ -608,7 +608,7 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
latestAiMessage.metadata = { ...latestAiMessage.metadata, isPersisted: true };
} catch (error) {
logger.warn('Failed to persist AI message containing tool call immediately', {
- error: error instanceof Error ? error.message : String(error),
+ error,
messageId: latestAiMessage.id,
});
}
@@ -745,7 +745,7 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
});
} catch (error) {
logger.error('Wiki search tool execution failed', {
- error: error instanceof Error ? error.message : String(error),
+ error,
toolCall: toolMatch,
});
@@ -802,9 +802,7 @@ Error: ${error instanceof Error ? error.message : String(error)}
callback();
} catch (error) {
- logger.error('Error in wiki search handler plugin', {
- error: error instanceof Error ? error.message : String(error),
- });
+ logger.error('Error in wiki search handler plugin', { error });
callback();
}
});
diff --git a/src/services/agentInstance/plugins/workspacesListPlugin.ts b/src/services/agentInstance/plugins/workspacesListPlugin.ts
index 07555770..5d99ff84 100644
--- a/src/services/agentInstance/plugins/workspacesListPlugin.ts
+++ b/src/services/agentInstance/plugins/workspacesListPlugin.ts
@@ -130,7 +130,7 @@ export const workspacesListPlugin: PromptConcatPlugin = (hooks) => {
callback();
} catch (error) {
logger.error('Error in workspaces list injection', {
- error: error instanceof Error ? error.message : String(error),
+ error,
pluginId: pluginConfig.id,
});
callback();
diff --git a/src/services/context/interface.ts b/src/services/context/interface.ts
index aa0d2891..002f8f74 100644
--- a/src/services/context/interface.ts
+++ b/src/services/context/interface.ts
@@ -11,7 +11,7 @@ export interface IPaths {
LOCALIZATION_FOLDER: string;
LOG_FOLDER: string;
MAIN_WINDOW_WEBPACK_ENTRY: string;
- MENUBAR_ICON_PATH: string;
+ TIDGI_MINI_WINDOW_ICON_PATH: string;
SETTINGS_FOLDER: string;
TIDDLERS_PATH: string;
TIDDLYWIKI_TEMPLATE_FOLDER_PATH: string;
diff --git a/src/services/database/configSetting.ts b/src/services/database/configSetting.ts
index 11a8ab41..e33613c1 100644
--- a/src/services/database/configSetting.ts
+++ b/src/services/database/configSetting.ts
@@ -22,8 +22,7 @@ function fixEmptyAndErrorSettingFileOnStartUp() {
fs.writeJSONSync(settings.file(), {});
}
} catch (error) {
- const error_ = error instanceof Error ? error : new Error(String(error));
- logger.error('Error when checking Setting file format', { function: 'fixEmptyAndErrorSettingFileOnStartUp', error: error_.message, stack: error_.stack ?? '' });
+ logger.error('Error when checking Setting file format', { function: 'fixEmptyAndErrorSettingFileOnStartUp', error });
}
}
@@ -44,8 +43,8 @@ export function fixSettingFileWhenError(jsonError: Error, providedJSONContent?:
fs.writeJSONSync(settings.file(), repaired);
logger.info('Fix JSON content done, saved', { repaired });
} catch (fixJSONError) {
- const fixError = fixJSONError instanceof Error ? fixJSONError : new Error(String(fixJSONError));
- logger.error('Setting file format bad, and cannot be fixed', { function: 'fixSettingFileWhenError', error: fixError.message, stack: fixError.stack ?? '', jsonContent });
+ const fixError = fixJSONError as Error;
+ logger.error('Setting file format bad, and cannot be fixed', { function: 'fixSettingFileWhenError', error: fixError, jsonContent });
}
}
@@ -56,7 +55,6 @@ try {
atomicSave: !isWin,
});
} catch (error) {
- const error_ = error instanceof Error ? error : new Error(String(error));
- logger.error('Error when configuring settings', { function: 'settings.configure', error: error_.message, stack: error_.stack ?? '' });
+ logger.error('Error when configuring settings', { function: 'settings.configure', error });
}
fixEmptyAndErrorSettingFileOnStartUp();
diff --git a/src/services/database/index.ts b/src/services/database/index.ts
index d0d7d55e..d5100cb2 100644
--- a/src/services/database/index.ts
+++ b/src/services/database/index.ts
@@ -52,6 +52,9 @@ export class DatabaseService implements IDatabaseService {
logger.info('loaded settings', {
hasContent: !!this.settingFileContent,
keys: this.settingFileContent ? Object.keys(this.settingFileContent).length : 0,
+ hasPreferences: !!this.settingFileContent?.preferences,
+ tidgiMiniWindow: this.settingFileContent?.preferences?.tidgiMiniWindow,
+ settingsFilePath: settings.file(),
function: 'DatabaseService.initializeForApp',
});
@@ -64,8 +67,7 @@ export class DatabaseService implements IDatabaseService {
path: settings.file().replace(/settings\.json$/, ''),
});
} catch (error) {
- const error_ = error instanceof Error ? error : new Error(String(error));
- logger.error('Error initializing setting backup file', { function: 'DatabaseService.initializeForApp', error: error_.message, stack: error_.stack ?? '' });
+ logger.error('Error initializing setting backup file', { function: 'DatabaseService.initializeForApp', error });
}
// Ensure database folder exists
@@ -171,7 +173,7 @@ export class DatabaseService implements IDatabaseService {
await this.loadSqliteVecExtension(dataSource);
} catch (error) {
logger.warn(`sqlite-vec extension failed to load during initialization for key: ${key}, continuing without vector search functionality`, {
- error: (error as Error).message,
+ error,
});
// Don't throw - allow the database to work without vector functionality
}
@@ -184,7 +186,7 @@ export class DatabaseService implements IDatabaseService {
await dataSource.destroy();
logger.info(`Database initialized for key: ${key}`);
} catch (error) {
- logger.error(`Error initializing database for key: ${key}`, { error: (error as Error).message });
+ logger.error(`Error initializing database for key: ${key}`, { error });
throw error;
}
}
@@ -216,7 +218,7 @@ export class DatabaseService implements IDatabaseService {
await this.loadSqliteVecExtension(dataSource);
} catch (error) {
logger.warn(`sqlite-vec extension failed to load for key: ${key}, continuing without vector search functionality`, {
- error: (error as Error).message,
+ error,
});
}
}
@@ -226,7 +228,7 @@ export class DatabaseService implements IDatabaseService {
return dataSource;
} catch (error) {
- logger.error(`Failed to get database for key: ${key}`, { error: (error as Error).message });
+ logger.error(`Failed to get database for key: ${key}`, { error });
if (!isRetry) {
try {
@@ -234,7 +236,7 @@ export class DatabaseService implements IDatabaseService {
await this.fixDatabaseLock(key);
return await this.getDatabase(key, {}, true);
} catch (retryError) {
- logger.error(`Failed to retry getting database for key: ${key}`, { error: (retryError as Error).message });
+ logger.error(`Failed to retry getting database for key: ${key}`, { error: retryError });
}
}
@@ -242,7 +244,7 @@ export class DatabaseService implements IDatabaseService {
await this.dataSources.get(key)?.destroy();
this.dataSources.delete(key);
} catch (closeError) {
- logger.error(`Failed to close database in error handler for key: ${key}`, { error: (closeError as Error).message });
+ logger.error(`Failed to close database in error handler for key: ${key}`, { error: closeError });
}
throw error;
@@ -267,7 +269,7 @@ export class DatabaseService implements IDatabaseService {
const stat = await fs.stat(databasePath);
return { exists: true, size: stat.size };
} catch (error) {
- logger.error(`getDatabaseInfo failed for key: ${key}`, { error: (error as Error).message });
+ logger.error(`getDatabaseInfo failed for key: ${key}`, { error });
return { exists: false };
}
}
@@ -289,7 +291,7 @@ export class DatabaseService implements IDatabaseService {
try {
await this.dataSources.get(key)?.destroy();
} catch (error) {
- logger.warn(`Failed to destroy datasource for key: ${key} before deletion`, { error: (error as Error).message });
+ logger.warn(`Failed to destroy datasource for key: ${key} before deletion`, { error });
}
this.dataSources.delete(key);
}
@@ -300,7 +302,7 @@ export class DatabaseService implements IDatabaseService {
logger.info(`Database file deleted for key: ${key}`);
}
} catch (error) {
- logger.error(`deleteDatabase failed for key: ${key}`, { error: (error as Error).message });
+ logger.error(`deleteDatabase failed for key: ${key}`, { error });
throw error;
}
}
@@ -323,7 +325,7 @@ export class DatabaseService implements IDatabaseService {
logger.info(`Database connection closed for key: ${key}`);
}
} catch (error) {
- logger.error(`Failed to close database for key: ${key}`, { error: (error as Error).message });
+ logger.error(`Failed to close database for key: ${key}`, { error });
throw error;
}
}
@@ -367,7 +369,7 @@ export class DatabaseService implements IDatabaseService {
await fs.unlink(temporaryPath);
logger.info(`Fixed database lock for key: ${key}`);
} catch (error) {
- logger.error(`Failed to fix database lock for key: ${key}`, { error: (error as Error).message });
+ logger.error(`Failed to fix database lock for key: ${key}`, { error });
throw error;
}
}
@@ -413,8 +415,7 @@ export class DatabaseService implements IDatabaseService {
// based on the dimensions needed
} catch (error) {
logger.error('Failed to load sqlite-vec extension:', {
- error: (error as Error).message,
- stack: (error as Error).stack,
+ error,
sqliteVecAvailable: typeof sqliteVec !== 'undefined',
});
throw new Error(`sqlite-vec extension failed to load: ${(error as Error).message}`);
diff --git a/src/services/git/index.ts b/src/services/git/index.ts
index dc6c75ed..090c7614 100644
--- a/src/services/git/index.ts
+++ b/src/services/git/index.ts
@@ -57,11 +57,9 @@ export class Git implements IGitService {
this.gitWorker = createWorkerProxy(worker);
logger.debug('gitWorker initialized successfully', { function: 'Git.initWorker' });
} catch (error) {
- const error_ = error instanceof Error ? error : new Error(String(error));
logger.error('Failed to initialize gitWorker', {
function: 'Git.initWorker',
- error: error_.message,
- errorObj: error_,
+ error,
});
throw error;
}
@@ -178,8 +176,8 @@ export class Git implements IGitService {
await this.nativeService.openInGitGuiApp(wikiFolderPath);
}
})
- .catch((_error: unknown) => {
- logger.error('createFailedDialog failed', _error instanceof Error ? _error : new Error(String(_error)));
+ .catch((error: unknown) => {
+ logger.error('createFailedDialog failed', { error });
});
}
}
@@ -205,14 +203,14 @@ export class Git implements IGitService {
try {
try {
await this.updateGitInfoTiddler(workspace, configs.remoteUrl, configs.userInfo?.branch);
- } catch (_error: unknown) {
- logger.error('updateGitInfoTiddler failed when commitAndSync', _error instanceof Error ? _error : new Error(String(_error)));
+ } catch (error: unknown) {
+ logger.error('updateGitInfoTiddler failed when commitAndSync', { error });
}
const observable = this.gitWorker?.commitAndSyncWiki(workspace, configs, getErrorMessageI18NDict());
return await this.getHasChangeHandler(observable, workspace.wikiFolderLocation, workspaceIDToShowNotification);
- } catch (_error: unknown) {
- const error = _error instanceof Error ? _error : new Error(String(_error));
- this.createFailedNotification(error.message, workspaceIDToShowNotification);
+ } catch (error: unknown) {
+ const error_ = error as Error;
+ this.createFailedNotification(error_.message, workspaceIDToShowNotification);
return true;
}
}
diff --git a/src/services/libs/getViewBounds.ts b/src/services/libs/getViewBounds.ts
index f01dd8e9..e2440d20 100644
--- a/src/services/libs/getViewBounds.ts
+++ b/src/services/libs/getViewBounds.ts
@@ -11,8 +11,8 @@ export default async function getViewBounds(
): Promise<{ height: number; width: number; x: number; y: number }> {
const { findInPage = false, windowName } = config;
const preferencesService = container.get(serviceIdentifier.Preference);
- const [sidebar, sidebarOnMenubar] = await Promise.all([preferencesService.get('sidebar'), preferencesService.get('sidebarOnMenubar')]);
- const showSidebar = windowName === WindowNames.menuBar ? sidebarOnMenubar : sidebar;
+ const [sidebar, tidgiMiniWindowShowSidebar] = await Promise.all([preferencesService.get('sidebar'), preferencesService.get('tidgiMiniWindowShowSidebar')]);
+ const showSidebar = windowName === WindowNames.tidgiMiniWindow ? tidgiMiniWindowShowSidebar : sidebar;
// Now showing sidebar on secondary window
const secondary = windowName === WindowNames.secondary;
const x = (showSidebar && !secondary) ? 68 : 0;
diff --git a/src/services/libs/i18n/i18next-electron-fs-backend.ts b/src/services/libs/i18n/i18next-electron-fs-backend.ts
index 97ee08b4..c5456674 100644
--- a/src/services/libs/i18n/i18next-electron-fs-backend.ts
+++ b/src/services/libs/i18n/i18next-electron-fs-backend.ts
@@ -160,7 +160,7 @@ export class Backend implements BackendModule {
try {
result = JSON.parse(payload.data ?? 'null');
} catch (parseError) {
- const parseError_ = parseError instanceof Error ? parseError : new Error(String(parseError));
+ const parseError_ = parseError as Error;
parseError_.message = `Error parsing '${String(payload.filename)}'. Message: '${String(parseError)}'.`;
const entry = this.readCallbacks[payload.key];
const callback__ = entry?.callback;
diff --git a/src/services/libs/log/index.ts b/src/services/libs/log/index.ts
index 7fd7d51a..49f7b291 100644
--- a/src/services/libs/log/index.ts
+++ b/src/services/libs/log/index.ts
@@ -1,9 +1,30 @@
import { LOG_FOLDER } from '@/constants/appPaths';
import winston, { format } from 'winston';
import 'winston-daily-rotate-file';
+import type { TransformableInfo } from 'logform';
+import { serializeError } from 'serialize-error';
import RendererTransport from './rendererTransport';
export * from './wikiOutput';
+/**
+ * Custom formatter to serialize Error objects using serialize-error package.
+ * Falls back to template string if serialization fails.
+ */
+const errorSerializer = format((info: TransformableInfo) => {
+ const infoRecord = info as Record;
+
+ // Serialize error objects
+ if (infoRecord.error instanceof Error) {
+ try {
+ infoRecord.error = serializeError(infoRecord.error);
+ } catch {
+ // Fallback to template string with optional chaining
+ const error = infoRecord.error as Error;
+ infoRecord.error = `${error?.message ?? ''} stack: ${error?.stack ?? ''}`;
+ }
+ }
+ return info;
+});
const logger = winston.createLogger({
transports: [
@@ -19,7 +40,7 @@ const logger = winston.createLogger({
}),
new RendererTransport(),
],
- format: format.combine(format.timestamp(), format.json()),
+ format: format.combine(errorSerializer(), format.timestamp(), format.json()),
});
export { logger };
diff --git a/src/services/libs/url.ts b/src/services/libs/url.ts
index 31ec66fd..6f1c07cd 100644
--- a/src/services/libs/url.ts
+++ b/src/services/libs/url.ts
@@ -27,8 +27,8 @@ export function getUrlWithCorrectProtocol(workspace: IWorkspace, originalUrl: st
return parsedUrl.toString();
} catch (error) {
logger.error(
- `Failed to getUrlWithCorrectProtocol for originalUrl ${originalUrl}, fallback to originalUrl. Error: ${(error as Error).message}`,
- { isHttps },
+ 'Failed to getUrlWithCorrectProtocol for originalUrl, fallback to originalUrl',
+ { isHttps, error },
);
return originalUrl;
}
@@ -42,8 +42,10 @@ export function replaceUrlPortWithSettingPort(originalUrl: string, newPort: numb
parsedUrl.port = String(newPort);
return parsedUrl.toString();
} catch (error) {
+ const error_ = error as Error;
logger.error(
- `Failed to replaceUrlPortWithSettingPort for originalUrl ${originalUrl} to newPort ${newPort} , fallback to originalUrl. Error: ${(error as Error).message}`,
+ 'Failed to replaceUrlPortWithSettingPort for originalUrl, fallback to originalUrl',
+ { error: error_ },
);
return originalUrl;
}
diff --git a/src/services/menu/index.ts b/src/services/menu/index.ts
index 94053708..f934a3cb 100644
--- a/src/services/menu/index.ts
+++ b/src/services/menu/index.ts
@@ -86,8 +86,7 @@ export class MenuService implements IMenuService {
Menu.setApplicationMenu(menu);
} catch (error) {
logger.error('buildMenu failed', {
- message: (error as Error).message,
- stack: (error as Error).stack ?? '',
+ error,
function: 'buildMenu',
});
try {
diff --git a/src/services/native/externalApp/darwin.ts b/src/services/native/externalApp/darwin.ts
index 24e5b7b9..4f41a8ab 100644
--- a/src/services/native/externalApp/darwin.ts
+++ b/src/services/native/externalApp/darwin.ts
@@ -139,9 +139,9 @@ async function findApplication(editor: IDarwinExternalEditor): Promise {
- const error = _error instanceof Error ? _error : new Error(String(_error));
- logger.info('gets appPath Error', { error: error.message ?? String(error), function: 'darwin.findApplication' });
+ const installPath = await appPath(identifier).catch(async (error_: unknown) => {
+ const error = error_ as Error;
+ logger.info('gets appPath Error', { error, function: 'darwin.findApplication' });
if (error.message === "Couldn't find the app") {
return await Promise.resolve(null);
}
@@ -166,9 +166,9 @@ async function findApplication(editor: IDarwinExternalEditor): Promise {
event.returnValue = this.showElectronMessageBoxSync(options, windowName);
});
}
+ public async initialize(): Promise {
+ await this.initializeKeyboardShortcuts();
+ }
+
+ private async initializeKeyboardShortcuts(): Promise {
+ const shortcuts = await this.getKeyboardShortcuts();
+ logger.debug('shortcuts from preferences', { shortcuts, function: 'initializeKeyboardShortcuts' });
+ // Register all saved shortcuts
+ for (const [key, shortcut] of Object.entries(shortcuts)) {
+ if (shortcut && shortcut.trim() !== '') {
+ try {
+ await registerShortcutByKey(key, shortcut);
+ } catch (error) {
+ logger.error(`Failed to register shortcut ${key}: ${shortcut}`, { error });
+ }
+ }
+ }
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
+ public async registerKeyboardShortcut(serviceName: keyof typeof serviceIdentifier, methodName: keyof T, shortcut: string): Promise {
+ try {
+ const key = `${String(serviceName)}.${String(methodName)}`;
+ logger.info('Starting keyboard shortcut registration', { key, shortcut, serviceName, methodName, function: 'NativeService.registerKeyboardShortcut' });
+
+ // Save to preferences
+ const preferenceService = container.get(serviceIdentifier.Preference);
+ const shortcuts = await this.getKeyboardShortcuts();
+ logger.debug('Current shortcuts before registration', { shortcuts, function: 'NativeService.registerKeyboardShortcut' });
+
+ shortcuts[key] = shortcut;
+ await preferenceService.set('keyboardShortcuts', shortcuts);
+ logger.info('Saved shortcut to preferences', { key, shortcut, function: 'NativeService.registerKeyboardShortcut' });
+
+ // Register the shortcut
+ await registerShortcutByKey(key, shortcut);
+ logger.info('Successfully registered new keyboard shortcut', { key, shortcut, function: 'NativeService.registerKeyboardShortcut' });
+ } catch (error) {
+ logger.error('Failed to register keyboard shortcut', { error, serviceIdentifier: serviceName, methodName, shortcut, function: 'NativeService.registerKeyboardShortcut' });
+ throw error;
+ }
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
+ public async unregisterKeyboardShortcut(serviceName: keyof typeof serviceIdentifier, methodName: keyof T): Promise {
+ try {
+ const key = `${String(serviceName)}.${String(methodName)}`;
+
+ // Get the current shortcut string before removing from preferences
+ const shortcuts = await this.getKeyboardShortcuts();
+ const shortcutString = shortcuts[key];
+
+ // Remove from preferences
+ const preferenceService = container.get(serviceIdentifier.Preference);
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
+ delete shortcuts[key];
+ await preferenceService.set('keyboardShortcuts', shortcuts);
+
+ // Unregister the shortcut using the actual shortcut string, not the key
+ if (shortcutString && globalShortcut.isRegistered(shortcutString)) {
+ globalShortcut.unregister(shortcutString);
+ logger.info('Successfully unregistered keyboard shortcut', { key, shortcutString });
+ } else {
+ logger.warn('Shortcut was not registered or shortcut string not found', { key, shortcutString });
+ }
+ } catch (error) {
+ logger.error('Failed to unregister keyboard shortcut', { error, serviceIdentifier: serviceName, methodName });
+ throw error;
+ }
+ }
+
+ public async getKeyboardShortcuts(): Promise> {
+ const preferences = this.preferenceService.getPreferences();
+ return preferences.keyboardShortcuts || {};
+ }
+
+ public async executeShortcutCallback(key: string): Promise {
+ logger.debug('Frontend requested shortcut execution', { key, function: 'NativeService.executeShortcutCallback' });
+
+ const callback = getShortcutCallback(key);
+ if (callback) {
+ await callback();
+ logger.info('Successfully executed shortcut callback from frontend', { key, function: 'NativeService.executeShortcutCallback' });
+ } else {
+ logger.warn('No callback found for shortcut key from frontend', { key, function: 'NativeService.executeShortcutCallback' });
+ }
+ }
+
public async openInEditor(filePath: string, editorName?: string): Promise {
// TODO: open vscode by default to speed up, support choose favorite editor later
let defaultEditor = await findEditorOrDefault('Visual Studio Code').catch(() => {});
diff --git a/src/services/native/interface.ts b/src/services/native/interface.ts
index c1d2953b..06c0eb2b 100644
--- a/src/services/native/interface.ts
+++ b/src/services/native/interface.ts
@@ -2,6 +2,7 @@ import { MessageBoxOptions } from 'electron';
import { Observable } from 'rxjs';
import { NativeChannel } from '@/constants/channels';
+import serviceIdentifier from '@services/serviceIdentifier';
import type { IZxFileInput } from '@services/wiki/wikiWorker';
import { WindowNames } from '@services/windows/WindowProperties';
import { ProxyPropertyType } from 'electron-ipc-cat/common';
@@ -18,6 +19,39 @@ export interface IPickDirectoryOptions {
* Wrap call to electron api, so we won't need remote module in renderer process
*/
export interface INativeService {
+ /**
+ * Initialize the native service
+ * This should be called during app startup
+ */
+ initialize(): Promise;
+ /**
+ * Register a keyboard shortcut and save it to preferences
+ * @param serviceName The service identifier name from serviceIdentifier
+ * @param methodName The method name to call when shortcut is triggered
+ * @param shortcut The keyboard shortcut string, e.g. "Ctrl+Shift+T"
+ * @template T The service interface type that contains the method, e.g. IWindowService
+ */
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
+ registerKeyboardShortcut(serviceName: keyof typeof serviceIdentifier, methodName: keyof T, shortcut: string): Promise;
+ /**
+ * Unregister a specific keyboard shortcut
+ * @param serviceName The service identifier name from serviceIdentifier
+ * @param methodName The method name
+ * @template T The service interface type that contains the method, e.g. IWindowService
+ */
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
+ unregisterKeyboardShortcut(serviceName: keyof typeof serviceIdentifier, methodName: keyof T): Promise;
+ /**
+ * Get all registered keyboard shortcuts from preferences, key is combination of service name and method name joined by '.'
+ * @returns A record where keys are formatted as 'serviceName.methodName' and values are the shortcut strings
+ */
+ getKeyboardShortcuts(): Promise>;
+ /**
+ * Execute a keyboard shortcut callback by key
+ * This method wraps the backend getShortcutCallback logic for frontend use
+ * @param key The key in format "ServiceIdentifier.methodName"
+ */
+ executeShortcutCallback(key: string): Promise;
/**
* Copy a file or directory. The directory can have contents.
* @param fromFilePath Note that if src is a directory it will copy everything inside of this directory, not the entire directory itself (see fs.extra issue #537).
@@ -91,6 +125,12 @@ export interface INativeService {
export const NativeServiceIPCDescriptor = {
channel: NativeChannel.name,
properties: {
+ initialize: ProxyPropertyType.Function,
+ initializeKeyboardShortcuts: ProxyPropertyType.Function,
+ registerKeyboardShortcut: ProxyPropertyType.Function,
+ unregisterKeyboardShortcut: ProxyPropertyType.Function,
+ getKeyboardShortcuts: ProxyPropertyType.Function,
+ executeShortcutCallback: ProxyPropertyType.Function,
copyPath: ProxyPropertyType.Function,
executeZxScript$: ProxyPropertyType.Function$,
formatFileUrlToAbsolutePath: ProxyPropertyType.Function,
diff --git a/src/services/native/keyboardShortcutHelpers.ts b/src/services/native/keyboardShortcutHelpers.ts
new file mode 100644
index 00000000..0b2a0551
--- /dev/null
+++ b/src/services/native/keyboardShortcutHelpers.ts
@@ -0,0 +1,106 @@
+import { container } from '@services/container';
+import { logger } from '@services/libs/log';
+import serviceIdentifier from '@services/serviceIdentifier';
+import { globalShortcut } from 'electron';
+
+/**
+ * Get the callback function for a shortcut key
+ * @param key The key in format "ServiceIdentifier.methodName"
+ * @returns The callback function or undefined
+ */
+export function getShortcutCallback(key: string): (() => Promise) | undefined {
+ logger.debug('Getting shortcut callback', { key, function: 'getShortcutCallback' });
+
+ // Split the key into service and method parts
+ const [serviceIdentifierName, methodName] = key.split('.');
+ logger.debug('Parsed key components', { serviceIdentifierName, methodName, function: 'getShortcutCallback' });
+
+ // If we don't have any part, return
+ if (!serviceIdentifierName || !methodName) {
+ logger.warn('Invalid key format', { key, serviceIdentifierName, methodName, function: 'getShortcutCallback' });
+ return;
+ }
+
+ // Get the service identifier symbol
+ const serviceSymbol = (serviceIdentifier as Record)[serviceIdentifierName];
+ logger.debug('Service symbol lookup', {
+ serviceIdentifierName,
+ serviceSymbol: serviceSymbol?.toString(),
+ availableServices: Object.keys(serviceIdentifier),
+ function: 'getShortcutCallback',
+ });
+
+ if (serviceSymbol === undefined) {
+ logger.warn('Service identifier not found', { serviceIdentifierName, availableServices: Object.keys(serviceIdentifier), function: 'getShortcutCallback' });
+ return;
+ }
+ // Return a callback that gets the service from container and calls the method
+ logger.debug('Creating callback function', { key, serviceIdentifierName, methodName, function: 'getShortcutCallback' });
+
+ return async () => {
+ try {
+ logger.info('🔥 SHORTCUT TRIGGERED! 🔥', { key, service: serviceIdentifierName, method: methodName, function: 'getShortcutCallback' });
+ logger.info('Shortcut triggered - starting execution', { key, service: serviceIdentifierName, method: methodName, function: 'getShortcutCallback' });
+
+ // Get the service instance from container lazily
+ const service = container.get unknown>>(serviceSymbol);
+ logger.debug('Service instance retrieved', {
+ key,
+ service: serviceIdentifierName,
+ method: methodName,
+ serviceExists: !!service,
+ serviceType: typeof service,
+ availableMethods: service ? Object.keys(service).filter(k => typeof service[k] === 'function') : [],
+ function: 'getShortcutCallback',
+ });
+
+ if (service && typeof service[methodName] === 'function') {
+ logger.info('Calling service method', { key, service: serviceIdentifierName, method: methodName, function: 'getShortcutCallback' });
+ // Call the method with await if it's an async method
+ await service[methodName]();
+ logger.info('Service method completed', { key, service: serviceIdentifierName, method: methodName, function: 'getShortcutCallback' });
+ } else {
+ logger.warn('Shortcut target method not found', {
+ key,
+ service: serviceIdentifierName,
+ method: methodName,
+ serviceExists: !!service,
+ methodExists: service ? typeof service[methodName] : 'service is null',
+ availableMethods: service ? Object.keys(service).filter(k => typeof service[k] === 'function') : [],
+ function: 'getShortcutCallback',
+ });
+ }
+ } catch (error) {
+ logger.error(`Failed to execute shortcut callback for ${key}`, { error, function: 'getShortcutCallback' });
+ }
+ };
+}
+
+/**
+ * Register a shortcut by key
+ * @param key The key in format "ServiceIdentifier.methodName"
+ * @param shortcut The shortcut string (e.g. "CmdOrCtrl+Shift+T")
+ */
+export async function registerShortcutByKey(key: string, shortcut: string): Promise {
+ // Unregister any existing shortcut first
+ if (globalShortcut.isRegistered(shortcut)) {
+ globalShortcut.unregister(shortcut);
+ }
+
+ // Get the callback for this key
+ const callback = getShortcutCallback(key);
+ if (!callback) {
+ logger.warn('No callback found for shortcut key', { key });
+ return;
+ }
+
+ // Register the new shortcut
+ const success = globalShortcut.register(shortcut, callback);
+
+ if (success) {
+ logger.info('Successfully registered shortcut', { key, shortcut, function: 'registerShortcutByKey' });
+ } else {
+ logger.error('Failed to register shortcut', { key, shortcut, function: 'registerShortcutByKey' });
+ throw new Error(`Failed to register shortcut: ${shortcut}`);
+ }
+}
diff --git a/src/services/native/reportError.ts b/src/services/native/reportError.ts
index 086ccd0a..624c7d4d 100644
--- a/src/services/native/reportError.ts
+++ b/src/services/native/reportError.ts
@@ -63,10 +63,10 @@ export function reportErrorToGithubWithTemplates(error: Error): void {
const nativeService = container.get(serviceIdentifier.NativeService);
return nativeService.openPath(LOG_FOLDER, true);
})
- .catch(async (_error: unknown) => {
- const error = _error instanceof Error ? _error : new Error(String(_error));
+ .catch(async (error_: unknown) => {
+ const error = error_ as Error;
await import('@services/libs/log').then(({ logger }) => {
- logger.error(`Failed to open LOG_FOLDER in reportErrorToGithubWithTemplates`, error);
+ logger.error(`Failed to open LOG_FOLDER in reportErrorToGithubWithTemplates`, { error });
});
});
openNewGitHubIssue({
diff --git a/src/services/preferences/defaultPreferences.ts b/src/services/preferences/defaultPreferences.ts
index 71f928a8..facdfdf9 100644
--- a/src/services/preferences/defaultPreferences.ts
+++ b/src/services/preferences/defaultPreferences.ts
@@ -7,7 +7,6 @@ export const defaultPreferences: IPreferences = {
allowPrerelease: Boolean(semver.prerelease(app.getVersion())),
alwaysOnTop: false,
askForDownloadPath: true,
- attachToMenubar: false,
disableAntiAntiLeech: false,
disableAntiAntiLeechForUrls: [],
downloadPath: DEFAULT_DOWNLOADS_PATH,
@@ -15,8 +14,8 @@ export const defaultPreferences: IPreferences = {
hibernateUnusedWorkspacesAtLaunch: false,
hideMenuBar: false,
ignoreCertificateErrors: false,
+ keyboardShortcuts: {},
language: 'zh-Hans',
- menuBarAlwaysOnTop: false,
pauseNotifications: '',
pauseNotificationsBySchedule: false,
pauseNotificationsByScheduleFrom: getDefaultPauseNotificationsByScheduleFrom(),
@@ -28,7 +27,6 @@ export const defaultPreferences: IPreferences = {
showSideBarIcon: true,
showSideBarText: true,
sidebar: true,
- sidebarOnMenubar: false,
spellcheck: true,
spellcheckLanguages: ['en-US'],
swipeToNavigate: true,
@@ -36,6 +34,12 @@ export const defaultPreferences: IPreferences = {
syncDebounceInterval: 1000 * 60 * 30,
syncOnlyWhenNoDraft: true,
themeSource: 'system' as 'system' | 'light' | 'dark',
+ tidgiMiniWindow: false,
+ tidgiMiniWindowAlwaysOnTop: false,
+ tidgiMiniWindowFixedWorkspaceId: '',
+ tidgiMiniWindowShowSidebar: false,
+ tidgiMiniWindowShowTitleBar: true,
+ tidgiMiniWindowSyncWorkspaceWithMainWindow: true,
titleBar: true,
unreadCountBadge: true,
useHardwareAcceleration: true,
diff --git a/src/services/preferences/index.ts b/src/services/preferences/index.ts
index fa9ac051..cd76000f 100755
--- a/src/services/preferences/index.ts
+++ b/src/services/preferences/index.ts
@@ -87,15 +87,22 @@ export class Preference implements IPreferenceService {
const notificationService = container.get(serviceIdentifier.NotificationService);
await notificationService.updatePauseNotificationsInfo();
}
+
+ // Delegate window-related preference changes to WindowService
+ const windowService = container.get(serviceIdentifier.Window);
+ await windowService.reactWhenPreferencesChanged(key, value);
+
switch (key) {
case 'themeSource': {
nativeTheme.themeSource = value as IPreferences['themeSource'];
- break;
+ return;
}
case 'language': {
await requestChangeLanguage(value as string);
- break;
+ return;
}
+ default:
+ break;
}
}
diff --git a/src/services/preferences/interface.ts b/src/services/preferences/interface.ts
index 0d681af2..1d815e59 100644
--- a/src/services/preferences/interface.ts
+++ b/src/services/preferences/interface.ts
@@ -8,7 +8,7 @@ export interface IPreferences {
allowPrerelease: boolean;
alwaysOnTop: boolean;
askForDownloadPath: boolean;
- attachToMenubar: boolean;
+ tidgiMiniWindow: boolean;
/**
* 完全关闭反盗链
*/
@@ -26,7 +26,7 @@ export interface IPreferences {
hideMenuBar: boolean;
ignoreCertificateErrors: boolean;
language: string;
- menuBarAlwaysOnTop: boolean;
+ tidgiMiniWindowAlwaysOnTop: boolean;
pauseNotifications: string | undefined;
pauseNotificationsBySchedule: boolean;
pauseNotificationsByScheduleFrom: string;
@@ -42,12 +42,28 @@ export interface IPreferences {
*/
sidebar: boolean;
/**
- * Should show sidebar on menubar window?
+ * Should show sidebar on tidgi mini window?
*/
- sidebarOnMenubar: boolean;
+ tidgiMiniWindowShowSidebar: boolean;
spellcheck: boolean;
spellcheckLanguages: HunspellLanguages[];
swipeToNavigate: boolean;
+ /**
+ * Whether menubar window should show the same workspace as main window
+ */
+ tidgiMiniWindowSyncWorkspaceWithMainWindow: boolean;
+ /**
+ * The workspace ID that tidgi mini window should always show when tidgiMiniWindowSyncWorkspaceWithMainWindow is false
+ */
+ tidgiMiniWindowFixedWorkspaceId: string | undefined;
+ /**
+ * Whether to show title bar on tidgi mini window (independent of main window's titleBar setting)
+ */
+ tidgiMiniWindowShowTitleBar: boolean;
+ /**
+ * Keyboard shortcuts configuration stored as serviceIdentifier.methodName -> shortcut
+ */
+ keyboardShortcuts: Record;
syncBeforeShutdown: boolean;
syncDebounceInterval: number;
/**
@@ -66,6 +82,7 @@ export enum PreferenceSections {
friendLinks = 'friendLinks',
general = 'general',
languages = 'languages',
+ tidgiMiniWindow = 'tidgiMiniWindow',
misc = 'misc',
network = 'network',
notifications = 'notifications',
diff --git a/src/services/sync/index.ts b/src/services/sync/index.ts
index 97ea0e6d..a1caa606 100644
--- a/src/services/sync/index.ts
+++ b/src/services/sync/index.ts
@@ -109,11 +109,8 @@ export class Sync implements ISyncService {
}
return true;
} catch (error) {
- logger.error(
- `${(error as Error).message} when checking draft titles. ${
- (error as Error).stack ?? ''
- }\n This might because it just will throw error when on Windows and App is at background (WebContentsView will disappear and not accessible.)`,
- );
+ const error_ = error as Error;
+ logger.error('Error when checking draft titles', { error: error_, function: 'checkCanSyncDueToNoDraft' });
// when app is on background, might have no draft, because user won't edit it. So just return true
return true;
}
diff --git a/src/services/view/handleNewWindow.ts b/src/services/view/handleNewWindow.ts
index 19f1ad6e..ea3002ed 100644
--- a/src/services/view/handleNewWindow.ts
+++ b/src/services/view/handleNewWindow.ts
@@ -55,10 +55,11 @@ export function handleNewWindow(
// open external url in browser
if (nextDomain !== undefined && (disposition === 'foreground-tab' || disposition === 'background-tab')) {
logger.debug('openExternal', { nextUrl, nextDomain, disposition, function: 'handleNewWindow' });
- void shell.openExternal(nextUrl).catch((_error: unknown) => {
+ void shell.openExternal(nextUrl).catch((error_: unknown) => {
+ const error = error_ as Error;
logger.error(
- `handleNewWindow() openExternal error ${_error instanceof Error ? _error.message : String(_error)}`,
- _error instanceof Error ? _error : new Error(String(_error)),
+ `handleNewWindow() openExternal error ${error.message}`,
+ { error },
);
});
return {
diff --git a/src/services/view/index.ts b/src/services/view/index.ts
index 5ba76c9c..bd80cfb4 100644
--- a/src/services/view/index.ts
+++ b/src/services/view/index.ts
@@ -22,6 +22,7 @@ import { logger } from '@services/libs/log';
import type { INativeService } from '@services/native/interface';
import { type IBrowserViewMetaData, WindowNames } from '@services/windows/WindowProperties';
import { isWikiWorkspace, type IWorkspace } from '@services/workspaces/interface';
+import { getTidgiMiniWindowTargetWorkspace } from '@services/workspacesView/utilities';
import debounce from 'lodash/debounce';
import { setViewEventName } from './constants';
import { ViewLoadUrlError } from './error';
@@ -247,7 +248,7 @@ export class View implements IViewService {
};
const checkNotExistResult = await Promise.all([
checkNotExist(workspace, WindowNames.main),
- this.preferenceService.get('attachToMenubar').then((attachToMenubar) => attachToMenubar && checkNotExist(workspace, WindowNames.menuBar)),
+ this.preferenceService.get('tidgiMiniWindow').then((tidgiMiniWindow) => tidgiMiniWindow && checkNotExist(workspace, WindowNames.tidgiMiniWindow)),
]);
return checkNotExistResult.every((result) => !result);
}
@@ -312,7 +313,11 @@ export class View implements IViewService {
if (this.shouldMuteAudio !== undefined) {
view.webContents.audioMuted = this.shouldMuteAudio;
}
- if (workspace.active || windowName === WindowNames.secondary) {
+ // Add view to window if:
+ // 1. workspace is active (main window)
+ // 2. windowName is secondary (always add)
+ // 3. windowName is tidgiMiniWindow (tidgi mini window can have fixed workspace independent of main window's active workspace)
+ if (workspace.active || windowName === WindowNames.secondary || windowName === WindowNames.tidgiMiniWindow) {
browserWindow.contentView.addChildView(view);
const contentSize = browserWindow.getContentSize();
const newViewBounds = await getViewBounds(contentSize as [number, number], { windowName });
@@ -326,7 +331,7 @@ export class View implements IViewService {
if (updatedWorkspace === undefined) return;
// Prevent update non-active (hiding) wiki workspace, so it won't pop up to cover other active agent workspace
if (windowName === WindowNames.main && !updatedWorkspace.active) return;
- if ([WindowNames.secondary, WindowNames.main, WindowNames.menuBar].includes(windowName)) {
+ if ([WindowNames.secondary, WindowNames.main, WindowNames.tidgiMiniWindow].includes(windowName)) {
const contentSize = browserWindow.getContentSize();
const newViewBounds = await getViewBounds(contentSize as [number, number], { windowName });
view.setBounds(newViewBounds);
@@ -406,12 +411,31 @@ export class View implements IViewService {
}
public async setActiveViewForAllBrowserViews(workspaceID: string): Promise {
- await Promise.all([
- this.setActiveView(workspaceID, WindowNames.main),
- this.preferenceService.get('attachToMenubar').then(async (attachToMenubar) => {
- return await (attachToMenubar && this.setActiveView(workspaceID, WindowNames.menuBar));
- }),
- ]);
+ // Set main window workspace
+ const mainWindowTask = this.setActiveView(workspaceID, WindowNames.main);
+ const tidgiMiniWindow = await this.preferenceService.get('tidgiMiniWindow');
+
+ // For tidgi mini window, decide which workspace to show based on preferences
+ let tidgiMiniWindowTask = Promise.resolve();
+ if (tidgiMiniWindow) {
+ // Default to sync (undefined or true), otherwise use fixed workspace ID (fallback to main if not set)
+ const { targetWorkspaceId } = await getTidgiMiniWindowTargetWorkspace(workspaceID);
+ const tidgiMiniWindowWorkspaceId = targetWorkspaceId || workspaceID;
+
+ logger.debug('setActiveViewForAllBrowserViews tidgi mini window decision', {
+ function: 'setActiveViewForAllBrowserViews',
+ tidgiMiniWindowWorkspaceId,
+ willSetActiveView: true,
+ });
+
+ tidgiMiniWindowTask = this.setActiveView(tidgiMiniWindowWorkspaceId, WindowNames.tidgiMiniWindow);
+ } else {
+ logger.info('setActiveViewForAllBrowserViews tidgi mini window not enabled', {
+ function: 'setActiveViewForAllBrowserViews',
+ });
+ }
+
+ await Promise.all([mainWindowTask, tidgiMiniWindowTask]);
}
public async setActiveView(workspaceID: string, windowName: WindowNames): Promise {
@@ -581,9 +605,9 @@ export class View implements IViewService {
const workspace = await workspaceService.getActiveWorkspace();
if (workspace !== undefined) {
const windowService = container.get(serviceIdentifier.Window);
- const isMenubarOpen = await windowService.isMenubarOpen();
- if (isMenubarOpen) {
- return this.getView(workspace.id, WindowNames.menuBar);
+ const isTidgiMiniWindowOpen = await windowService.isTidgiMiniWindowOpen();
+ if (isTidgiMiniWindowOpen) {
+ return this.getView(workspace.id, WindowNames.tidgiMiniWindow);
} else {
return this.getView(workspace.id, WindowNames.main);
}
@@ -594,7 +618,7 @@ export class View implements IViewService {
const workspaceService = container.get(serviceIdentifier.Workspace);
const workspace = await workspaceService.getActiveWorkspace();
if (workspace !== undefined) {
- return [this.getView(workspace.id, WindowNames.main), this.getView(workspace.id, WindowNames.menuBar)];
+ return [this.getView(workspace.id, WindowNames.main), this.getView(workspace.id, WindowNames.tidgiMiniWindow)];
}
logger.error(`getActiveBrowserViews workspace !== undefined`, { stack: new Error('stack').stack?.replace('Error:', '') });
return [];
diff --git a/src/services/view/interface.ts b/src/services/view/interface.ts
index 8f9c05e3..e3a8e58f 100644
--- a/src/services/view/interface.ts
+++ b/src/services/view/interface.ts
@@ -30,11 +30,11 @@ export interface IViewService {
createViewAddToWindow(workspace: IWorkspace, browserWindow: BrowserWindow, sharedWebPreferences: WebPreferences, windowName: WindowNames): Promise;
forEachView: (functionToRun: (view: WebContentsView, workspaceID: string, windowName: WindowNames) => void) => void;
/**
- * If menubar is open, we get menubar browser view, else we get main window browser view
+ * If tidgi mini window is open, we get tidgi mini window browser view, else we get main window browser view
*/
getActiveBrowserView: () => Promise;
/**
- * Get active workspace's main window and menubar browser view.
+ * Get active workspace's main window and tidgi mini window browser view.
*/
getActiveBrowserViews: () => Promise>;
getLoadedViewEnsure(workspaceID: string, windowName: WindowNames): Promise;
@@ -74,7 +74,7 @@ export interface IViewService {
/**
* Bring an already created view to the front. If it happened to not created, will call `addView()` to create one.
* @param workspaceID id, can only be main workspace id, because only main workspace will have view created.
- * @param windowName you can control main window or menubar window to have this view.
+ * @param windowName you can control main window or tidgi mini window to have this view.
* @returns
*/
setActiveView: (workspaceID: string, windowName: WindowNames) => Promise;
diff --git a/src/services/view/setupIpcServerRoutesHandlers.ts b/src/services/view/setupIpcServerRoutesHandlers.ts
index 68aa086c..4b44a64d 100644
--- a/src/services/view/setupIpcServerRoutesHandlers.ts
+++ b/src/services/view/setupIpcServerRoutesHandlers.ts
@@ -153,13 +153,11 @@ export function setupIpcServerRoutesHandlers(view: WebContentsView, workspaceID:
}
}
} catch (error) {
- const error_ = error instanceof Error ? error : new Error(String(error));
logger.error('setupIpcServerRoutesHandlers.handlerCallback error', {
function: 'setupIpcServerRoutesHandlers.handlerCallback',
- error: error_.message,
- stack: error_.stack ?? '',
+ error,
});
- return new Response(undefined, { status: 500, statusText: `${error_.message} ${error_.stack ?? ''}` });
+ return new Response(undefined, { status: 500, statusText: `${(error as Error).message} ${(error as Error).stack ?? ''}` });
}
const statusText = `setupIpcServerRoutesHandlers.handlerCallback: tidgi protocol 404 ${request.url}`;
logger.warn(statusText);
@@ -174,11 +172,9 @@ export function setupIpcServerRoutesHandlers(view: WebContentsView, workspaceID:
logger.warn('tidgi protocol is not handled', { function: 'setupIpcServerRoutesHandlers.handlerCallback' });
}
} catch (error) {
- const error_ = error instanceof Error ? error : new Error(String(error));
logger.error('setupIpcServerRoutesHandlers.handlerCallback error', {
function: 'setupIpcServerRoutesHandlers.handlerCallback',
- error: error_.message,
- stack: error_.stack ?? '',
+ error,
});
}
}
diff --git a/src/services/view/setupViewEventHandlers.ts b/src/services/view/setupViewEventHandlers.ts
index 67071635..2646db54 100644
--- a/src/services/view/setupViewEventHandlers.ts
+++ b/src/services/view/setupViewEventHandlers.ts
@@ -95,17 +95,17 @@ export default function setupViewEventHandlers(
}
// if is external website
logger.debug('will-navigate openExternal', { newUrl, currentUrl, homeUrl, lastUrl });
- await shell.openExternal(newUrl).catch((_error: unknown) => {
- const error = _error instanceof Error ? _error : new Error(String(_error));
- logger.error(`will-navigate openExternal error ${error.message}`, error);
+ await shell.openExternal(newUrl).catch((error_: unknown) => {
+ const error = error_ as Error;
+ logger.error(`will-navigate openExternal error ${error.message}`, { error });
});
// if is an external website
event.preventDefault();
try {
// TODO: do this until https://github.com/electron/electron/issues/31783 fixed
await view.webContents.loadURL(currentUrl);
- } catch (_error: unknown) {
- const error = _error instanceof Error ? _error : new Error(String(_error));
+ } catch (error_: unknown) {
+ const error = error_ as Error;
logger.warn(new ViewLoadUrlError(lastUrl ?? '', `${error.message} ${error.stack ?? ''}`));
}
// event.stopPropagation();
@@ -299,8 +299,8 @@ export default function setupViewEventHandlers(
view.webContents.on('update-target-url', (_event, url) => {
try {
view.webContents.send('update-target-url', url);
- } catch (_error: unknown) {
- const error = _error instanceof Error ? _error : new Error(String(_error));
+ } catch (error_: unknown) {
+ const error = error_ as Error;
logger.warn(error);
}
});
diff --git a/src/services/view/setupViewFileProtocol.ts b/src/services/view/setupViewFileProtocol.ts
index 4929d1c5..c3c75d70 100644
--- a/src/services/view/setupViewFileProtocol.ts
+++ b/src/services/view/setupViewFileProtocol.ts
@@ -27,25 +27,25 @@ export function handleOpenFileExternalLink(nextUrl: string, newWindowContext: IN
const fileStat = fs.statSync(absoluteFilePath);
if (fileStat.isDirectory()) {
logger.info(`Opening directory ${absoluteFilePath}`, { function: 'handleOpenFileExternalLink' });
- void shell.openPath(absoluteFilePath).catch((_error: unknown) => {
- const error = _error instanceof Error ? _error : new Error(String(_error));
+ void shell.openPath(absoluteFilePath).catch((error_: unknown) => {
+ const error = error_ as Error;
const message = i18n.t('Log.FailedToOpenDirectory', { path: absoluteFilePath, message: error.message });
- logger.warn(message, { function: 'handleOpenFileExternalLink' });
+ logger.warn(message, { function: 'handleOpenFileExternalLink', error });
void wikiService.wikiOperationInBrowser(WikiChannel.generalNotification, newWindowContext.workspace.id, [message]);
});
} else if (fileStat.isFile()) {
logger.info(`Opening file ${absoluteFilePath}`, { function: 'handleOpenFileExternalLink' });
- void shell.openPath(absoluteFilePath).catch((_error: unknown) => {
- const error = _error instanceof Error ? _error : new Error(String(_error));
+ void shell.openPath(absoluteFilePath).catch((error_: unknown) => {
+ const error = error_ as Error;
const message = i18n.t('Log.FailedToOpenFile', { path: absoluteFilePath, message: error.message });
- logger.warn(message, { function: 'handleOpenFileExternalLink' });
+ logger.warn(message, { function: 'handleOpenFileExternalLink', error });
void wikiService.wikiOperationInBrowser(WikiChannel.generalNotification, newWindowContext.workspace.id, [message]);
});
}
- } catch (_error: unknown) {
- const error = _error instanceof Error ? _error : new Error(String(_error));
+ } catch (error_: unknown) {
+ const error = error_ as Error;
const message = `${i18n.t('AddWorkspace.PathNotExist', { path: absoluteFilePath })} ${error.message}`;
- logger.warn(message, { function: 'handleOpenFileExternalLink' });
+ logger.warn(message, { function: 'handleOpenFileExternalLink', error });
void wikiService.wikiOperationInBrowser(WikiChannel.generalNotification, newWindowContext.workspace.id, [message]);
}
return {
diff --git a/src/services/wiki/index.ts b/src/services/wiki/index.ts
index a7cace2b..9d7f5762 100644
--- a/src/services/wiki/index.ts
+++ b/src/services/wiki/index.ts
@@ -77,10 +77,9 @@ export class Wiki implements IWikiService {
});
} catch (error) {
logger.error('failed', {
- error: (error as Error).message,
+ error,
newFolderPath,
folderName,
- stack: (error as Error).stack,
function: 'copyWikiTemplate',
});
throw new CopyWikiTemplateError(`${(error as Error).message}, (${newFolderPath}, ${folderName})`);
@@ -393,8 +392,7 @@ export class Wiki implements IWikiService {
logger.debug(`terminateWorker for ${id}`);
await terminateWorker(nativeWorker);
} catch (error) {
- const error_ = error instanceof Error ? error : new Error(String(error));
- logger.error('wiki worker stop failed', { function: 'stopWiki', error: error_.message, errorObj: error_ });
+ logger.error('wiki worker stop failed', { function: 'stopWiki', error });
}
delete this.wikiWorkers[id];
@@ -709,10 +707,9 @@ export class Wiki implements IWikiService {
function: 'startWiki',
});
} catch (error) {
- const error_ = error instanceof Error ? error : new Error(String(error));
- logger.warn('startWiki failed', { function: 'startWiki', error: error_.message });
+ logger.warn('startWiki failed', { function: 'startWiki', error });
if (error instanceof WikiRuntimeError && error.retry) {
- logger.warn('startWiki retry', { function: 'startWiki', error: error_.message });
+ logger.warn('startWiki retry', { function: 'startWiki', error });
// don't want it to throw here again, so no await here.
const workspaceViewService = container.get(serviceIdentifier.WorkspaceView);
@@ -720,7 +717,7 @@ export class Wiki implements IWikiService {
} else if ((error as Error).message.includes('Did not receive an init message from worker after')) {
// https://github.com/andywer/threads.js/issues/426
// wait some time and restart the wiki will solve this
- logger.warn('startWiki handle error, restarting', { function: 'startWiki', error: error_.message });
+ logger.warn('startWiki handle error, restarting', { function: 'startWiki', error });
await this.restartWiki(workspace);
} else {
logger.warn('unexpected error, throw it', { function: 'startWiki' });
diff --git a/src/services/wiki/plugin/ghPages.ts b/src/services/wiki/plugin/ghPages.ts
index 6e011e58..906ba746 100644
--- a/src/services/wiki/plugin/ghPages.ts
+++ b/src/services/wiki/plugin/ghPages.ts
@@ -13,8 +13,7 @@ export async function updateGhConfig(wikiPath: string, options: IGhOptions): Pro
const newContent = content.replace(/(branches:\n\s+- )(master)$/m, `$1${options.branch}`);
await fs.writeFile(ghPagesConfigPath, newContent, 'utf8');
} catch (error) {
- const error_ = error instanceof Error ? error : new Error(String(error));
- logger.error('updateGhConfig failed', { function: 'updateGhConfig', error: error_.message, errorObj: error_ });
+ logger.error('updateGhConfig failed', { function: 'updateGhConfig', error });
}
}
}
diff --git a/src/services/wiki/plugin/subWikiPlugin.ts b/src/services/wiki/plugin/subWikiPlugin.ts
index df0e6576..efb461db 100644
--- a/src/services/wiki/plugin/subWikiPlugin.ts
+++ b/src/services/wiki/plugin/subWikiPlugin.ts
@@ -117,7 +117,7 @@ export async function getSubWikiPluginContent(mainWikiPath: string): Promise item.folderName.length > 0 && item.tagName.length > 0);
} catch (error) {
- logger.error((error as Error).message, { function: 'getSubWikiPluginContent' });
+ logger.error((error as Error).message, { error, function: 'getSubWikiPluginContent' });
return [];
}
}
diff --git a/src/services/wiki/wikiOperations/executor/wikiOperationInServer.ts b/src/services/wiki/wikiOperations/executor/wikiOperationInServer.ts
index b37be1ee..45f52c97 100644
--- a/src/services/wiki/wikiOperations/executor/wikiOperationInServer.ts
+++ b/src/services/wiki/wikiOperations/executor/wikiOperationInServer.ts
@@ -40,8 +40,8 @@ export class WikiOperationsInWikiWorker {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const result = new Function('$tw', script)(this.wikiInstance) as unknown;
resolve(result);
- } catch (_error: unknown) {
- const error = _error instanceof Error ? _error : new Error(String(_error));
+ } catch (error_: unknown) {
+ const error = error_ as Error;
reject(error);
}
}, 1);
diff --git a/src/services/wiki/wikiWorker/htmlWiki.ts b/src/services/wiki/wikiWorker/htmlWiki.ts
index 7b41e9eb..75533692 100644
--- a/src/services/wiki/wikiWorker/htmlWiki.ts
+++ b/src/services/wiki/wikiWorker/htmlWiki.ts
@@ -19,15 +19,15 @@ export async function extractWikiHTML(htmlWikiPath: string, saveWikiFolderPath:
resolve();
},
});
- } catch (_error: unknown) {
- const error = _error instanceof Error ? _error : new Error(String(_error));
+ } catch (error_: unknown) {
+ const error = error_ as Error;
reject(error);
}
});
- } catch (_error: unknown) {
+ } catch (error_: unknown) {
// removes the folder function that failed to convert.
await remove(saveWikiFolderPath);
- const error = _error instanceof Error ? _error : new Error(String(_error));
+ const error = error_ as Error;
throw error;
}
}
@@ -48,8 +48,8 @@ export async function packetHTMLFromWikiFolder(folderWikiPath: string, pathOfNew
resolve();
},
});
- } catch (_error: unknown) {
- const error = _error instanceof Error ? _error : new Error(String(_error));
+ } catch (error_: unknown) {
+ const error = error_ as Error;
reject(error);
}
});
diff --git a/src/services/wikiEmbedding/index.ts b/src/services/wikiEmbedding/index.ts
index 98a1e073..e70cfaa1 100644
--- a/src/services/wikiEmbedding/index.ts
+++ b/src/services/wikiEmbedding/index.ts
@@ -233,8 +233,7 @@ export class WikiEmbeddingService implements IWikiEmbeddingService {
return Array.isArray(countResult) && countResult.length > 0 ? Number(countResult[0]) : 0;
} catch (error) {
- const errorMessage = error instanceof Error ? error.message : String(error);
- logger.error('Failed to get total notes count', { function: 'getTotalNotesCount', error: errorMessage });
+ logger.error('Failed to get total notes count', { function: 'getTotalNotesCount', error });
return 0;
}
}
@@ -714,7 +713,7 @@ export class WikiEmbeddingService implements IWikiEmbeddingService {
await this.statusRepository!.save(entity);
} catch (error) {
// If saving fails, just return the default status
- logger.debug('could not save default embedding status', { function: 'getEmbeddingStatus', error: error instanceof Error ? error.message : String(error) });
+ logger.debug('could not save default embedding status', { function: 'getEmbeddingStatus', error });
}
return defaultStatus;
@@ -759,7 +758,7 @@ export class WikiEmbeddingService implements IWikiEmbeddingService {
}).catch((error: unknown) => {
logger.error('Failed to initialize embedding status subscription', {
function: 'subscribeToEmbeddingStatus',
- error: String(error),
+ error: error as Error,
});
});
}
diff --git a/src/services/wikiGitWorkspace/index.ts b/src/services/wikiGitWorkspace/index.ts
index c29033e0..ade43c0e 100644
--- a/src/services/wikiGitWorkspace/index.ts
+++ b/src/services/wikiGitWorkspace/index.ts
@@ -55,8 +55,8 @@ export class WikiGitWorkspace implements IWikiGitWorkspaceService {
}),
]);
}
- } catch (_error: unknown) {
- const error = _error instanceof Error ? _error : new Error(String(_error));
+ } catch (error_: unknown) {
+ const error = error_ as Error;
logger.error(`SyncBeforeShutdown failed`, { error });
} finally {
app.quit();
@@ -98,9 +98,9 @@ export class WikiGitWorkspace implements IWikiGitWorkspaceService {
}
}
return newWorkspace;
- } catch (_error: unknown) {
+ } catch (error_: unknown) {
// prepare to rollback changes
- const error = _error instanceof Error ? _error : new Error(String(_error));
+ const error = error_ as Error;
const errorMessage = `initWikiGitTransaction failed, ${error.message} ${error.stack ?? ''}`;
logger.error(errorMessage);
const workspaceService = container.get(serviceIdentifier.Workspace);
@@ -112,9 +112,8 @@ export class WikiGitWorkspace implements IWikiGitWorkspaceService {
} else if (typeof mainWikiToLink === 'string') {
await wikiService.removeWiki(wikiFolderLocation, mainWikiToLink);
}
- } catch (_error_) {
- const error_ = _error_ instanceof Error ? _error_ : new Error(String(_error_));
- throw new InitWikiGitRevertError(error_.message);
+ } catch (error_: unknown) {
+ throw new InitWikiGitRevertError(String(error_));
}
throw new InitWikiGitError(errorMessage);
}
@@ -187,11 +186,10 @@ export class WikiGitWorkspace implements IWikiGitWorkspaceService {
logger.info('Default wiki workspace created successfully', {
function: 'WikiGitWorkspace.initialize',
});
- } catch (_error: unknown) {
- const error = _error instanceof Error ? _error : new Error(String(_error));
+ } catch (error_: unknown) {
+ const error = error_ as Error;
logger.error('Failed to create default wiki workspace', {
- error: error.message,
- stack: error.stack,
+ error,
function: 'WikiGitWorkspace.initialize',
});
}
@@ -223,9 +221,9 @@ export class WikiGitWorkspace implements IWikiGitWorkspaceService {
}
const wikiService = container.get(serviceIdentifier.Wiki);
const workspaceService = container.get(serviceIdentifier.Workspace);
- await wikiService.stopWiki(id).catch((_error: unknown) => {
- const error = _error instanceof Error ? _error : new Error(String(_error));
- logger.error(error.message, error);
+ await wikiService.stopWiki(id).catch((error_: unknown) => {
+ const error = error_ as Error;
+ logger.error(error.message, { error });
});
if (isSubWiki) {
if (mainWikiToLink === null) {
@@ -251,9 +249,9 @@ export class WikiGitWorkspace implements IWikiGitWorkspaceService {
if (firstWorkspace !== undefined) {
await container.get(serviceIdentifier.WorkspaceView).setActiveWorkspaceView(firstWorkspace.id);
}
- } catch (_error: unknown) {
- const error = _error instanceof Error ? _error : new Error(String(_error));
- logger.error(error.message, error);
+ } catch (error_: unknown) {
+ const error = error_ as Error;
+ logger.error(error.message, { error });
}
}
}
diff --git a/src/services/windows/WindowProperties.ts b/src/services/windows/WindowProperties.ts
index bbd28413..c454957a 100644
--- a/src/services/windows/WindowProperties.ts
+++ b/src/services/windows/WindowProperties.ts
@@ -15,7 +15,7 @@ export enum WindowNames {
* We only have a single instance of main window, that is the app window.
*/
main = 'main',
- menuBar = 'menuBar',
+ tidgiMiniWindow = 'tidgiMiniWindow',
notifications = 'notifications',
preferences = 'preferences',
/**
@@ -46,7 +46,7 @@ export const windowDimension: Record {
+export async function handleAttachToTidgiMiniWindow(
+ windowConfig: BrowserWindowConstructorOptions,
+ windowWithBrowserViewState: windowStateKeeper.State | undefined,
+): Promise {
const menuService = container.get(serviceIdentifier.MenuService);
const windowService = container.get(serviceIdentifier.Window);
const viewService = container.get(serviceIdentifier.View);
+ const preferenceService = container.get(serviceIdentifier.Preference);
+
+ // Get tidgi mini window-specific titleBar preference
+ const tidgiMiniWindowShowTitleBar = await preferenceService.get('tidgiMiniWindowShowTitleBar');
// setImage after Tray instance is created to avoid
// "Segmentation fault (core dumped)" bug on Linux
@@ -25,54 +33,71 @@ export async function handleAttachToMenuBar(windowConfig: BrowserWindowConstruct
// https://github.com/atomery/translatium/issues/164
const tray = new Tray(nativeImage.createEmpty());
// icon template is not supported on Windows & Linux
- tray.setImage(MENUBAR_ICON_PATH);
+ tray.setImage(nativeImage.createFromPath(TIDGI_MINI_WINDOW_ICON_PATH));
- const menuBar = menubar({
+ // Create tidgi mini window-specific window configuration
+ // Override titleBar settings from windowConfig with tidgi mini window-specific preference
+ const tidgiMiniWindowConfig: BrowserWindowConstructorOptions = {
+ ...windowConfig,
+ show: false,
+ minHeight: 100,
+ minWidth: 250,
+ // Use tidgi mini window-specific titleBar setting instead of inheriting from main window
+ titleBarStyle: tidgiMiniWindowShowTitleBar ? 'default' : 'hidden',
+ frame: tidgiMiniWindowShowTitleBar,
+ // Always hide the menu bar (File, Edit, View menu), even when showing title bar
+ autoHideMenuBar: true,
+ };
+
+ logger.info('Creating tidgi mini window with titleBar configuration', {
+ function: 'handleAttachToTidgiMiniWindow',
+ tidgiMiniWindowShowTitleBar,
+ titleBarStyle: tidgiMiniWindowConfig.titleBarStyle,
+ frame: tidgiMiniWindowConfig.frame,
+ });
+
+ const tidgiMiniWindow = menubar({
index: getMainWindowEntry(),
tray,
activateWithApp: false,
showDockIcon: true,
preloadWindow: true,
- tooltip: i18n.t('Menu.TidGiMenuBar'),
- browserWindow: mergeDeep(windowConfig, {
- show: false,
- minHeight: 100,
- minWidth: 250,
- }),
+ tooltip: i18n.t('Menu.TidGiMiniWindow'),
+ browserWindow: tidgiMiniWindowConfig,
});
- menuBar.on('after-create-window', () => {
- if (menuBar.window !== undefined) {
- menuBar.window.on('focus', async () => {
- logger.debug('restore window position');
+ tidgiMiniWindow.on('after-create-window', () => {
+ if (tidgiMiniWindow.window !== undefined) {
+ tidgiMiniWindow.window.on('focus', async () => {
+ logger.debug('restore window position', { function: 'handleAttachToTidgiMiniWindow' });
if (windowWithBrowserViewState === undefined) {
- logger.debug('windowWithBrowserViewState is undefined for menuBar');
+ logger.debug('windowWithBrowserViewState is undefined for tidgiMiniWindow', { function: 'handleAttachToTidgiMiniWindow' });
} else {
- if (menuBar.window === undefined) {
- logger.debug('menuBar.window is undefined');
+ if (tidgiMiniWindow.window === undefined) {
+ logger.debug('tidgiMiniWindow.window is undefined', { function: 'handleAttachToTidgiMiniWindow' });
} else {
const haveXYValue = [windowWithBrowserViewState.x, windowWithBrowserViewState.y].every((value) => Number.isFinite(value));
const haveWHValue = [windowWithBrowserViewState.width, windowWithBrowserViewState.height].every((value) => Number.isFinite(value));
if (haveXYValue) {
- menuBar.window.setPosition(windowWithBrowserViewState.x, windowWithBrowserViewState.y, false);
+ tidgiMiniWindow.window.setPosition(windowWithBrowserViewState.x, windowWithBrowserViewState.y, false);
}
if (haveWHValue) {
- menuBar.window.setSize(windowWithBrowserViewState.width, windowWithBrowserViewState.height, false);
+ tidgiMiniWindow.window.setSize(windowWithBrowserViewState.width, windowWithBrowserViewState.height, false);
}
}
}
const view = await viewService.getActiveBrowserView();
view?.webContents.focus();
});
- menuBar.window.removeAllListeners('close');
- menuBar.window.on('close', (event) => {
+ tidgiMiniWindow.window.removeAllListeners('close');
+ tidgiMiniWindow.window.on('close', (event) => {
event.preventDefault();
- menuBar.hideWindow();
+ tidgiMiniWindow.hideWindow();
});
}
});
- menuBar.on('hide', async () => {
- // on mac, calling `menuBar.app.hide()` with main window open will bring background main window up, which we don't want. We want to bring previous other app up. So close main window first.
+ tidgiMiniWindow.on('hide', async () => {
+ // on mac, calling `tidgiMiniWindow.app.hide()` with main window open will bring background main window up, which we don't want. We want to bring previous other app up. So close main window first.
if (isMac) {
const mainWindow = windowService.get(WindowNames.main);
if (mainWindow?.isVisible() === true) {
@@ -81,29 +106,29 @@ export async function handleAttachToMenuBar(windowConfig: BrowserWindowConstruct
}
});
// https://github.com/maxogden/menubar/issues/120
- menuBar.on('after-hide', () => {
+ tidgiMiniWindow.on('after-hide', () => {
if (isMac) {
- menuBar.app.hide();
+ tidgiMiniWindow.app.hide();
}
});
// manually save window state https://github.com/mawie81/electron-window-state/issues/64
const debouncedSaveWindowState = debounce(
() => {
- if (menuBar.window !== undefined) {
- windowWithBrowserViewState?.saveState(menuBar.window);
+ if (tidgiMiniWindow.window !== undefined) {
+ windowWithBrowserViewState?.saveState(tidgiMiniWindow.window);
}
},
500,
);
- // menubar is hide, not close, so not managed by windowStateKeeper, need to save manually
- menuBar.window?.on('resize', debouncedSaveWindowState);
- menuBar.window?.on('move', debouncedSaveWindowState);
+ // tidgi mini window is hide, not close, so not managed by windowStateKeeper, need to save manually
+ tidgiMiniWindow.window?.on('resize', debouncedSaveWindowState);
+ tidgiMiniWindow.window?.on('move', debouncedSaveWindowState);
return await new Promise((resolve) => {
- menuBar.on('ready', async () => {
+ tidgiMiniWindow.on('ready', async () => {
// right on tray icon
- menuBar.tray.on('right-click', () => {
+ tidgiMiniWindow.tray.on('right-click', () => {
const contextMenu = Menu.buildFromTemplate([
{
label: i18n.t('ContextMenu.OpenTidGi'),
@@ -112,9 +137,9 @@ export async function handleAttachToMenuBar(windowConfig: BrowserWindowConstruct
},
},
{
- label: i18n.t('ContextMenu.OpenTidGiMenuBar'),
+ label: i18n.t('ContextMenu.OpenTidGiMiniWindow'),
click: async () => {
- await menuBar.showWindow();
+ await tidgiMiniWindow.showWindow();
},
},
{
@@ -143,23 +168,23 @@ export async function handleAttachToMenuBar(windowConfig: BrowserWindowConstruct
{
label: i18n.t('ContextMenu.Quit'),
click: () => {
- menuBar.app.quit();
+ tidgiMiniWindow.app.quit();
},
},
]);
- menuBar.tray.popUpContextMenu(contextMenu);
+ tidgiMiniWindow.tray.popUpContextMenu(contextMenu);
});
// right click on window content
- if (menuBar.window?.webContents !== undefined) {
- const unregisterContextMenu = await menuService.initContextMenuForWindowWebContents(menuBar.window.webContents);
- menuBar.on('after-close', () => {
+ if (tidgiMiniWindow.window?.webContents !== undefined) {
+ const unregisterContextMenu = await menuService.initContextMenuForWindowWebContents(tidgiMiniWindow.window.webContents);
+ tidgiMiniWindow.on('after-close', () => {
unregisterContextMenu();
});
}
- resolve(menuBar);
+ resolve(tidgiMiniWindow);
});
});
}
diff --git a/src/services/windows/index.ts b/src/services/windows/index.ts
index a73b59de..f3850733 100644
--- a/src/services/windows/index.ts
+++ b/src/services/windows/index.ts
@@ -8,7 +8,9 @@ import { windowDimension, WindowMeta, WindowNames } from '@services/windows/Wind
import { Channels, MetaDataChannel, ViewChannel, WindowChannel } from '@/constants/channels';
import type { IPreferenceService } from '@services/preferences/interface';
+import type { IViewService } from '@services/view/interface';
import type { IWorkspaceService } from '@services/workspaces/interface';
+import type { IWorkspaceViewService } from '@services/workspacesView/interface';
import { SETTINGS_FOLDER } from '@/constants/appPaths';
import { isTest } from '@/constants/environment';
@@ -19,8 +21,8 @@ import { container } from '@services/container';
import getViewBounds from '@services/libs/getViewBounds';
import { logger } from '@services/libs/log';
import type { IThemeService } from '@services/theme/interface';
-import type { IViewService } from '@services/view/interface';
-import { handleAttachToMenuBar } from './handleAttachToMenuBar';
+import { getTidgiMiniWindowTargetWorkspace } from '@services/workspacesView/utilities';
+import { handleAttachToTidgiMiniWindow } from './handleAttachToTidgiMiniWindow';
import { handleCreateBasicWindow } from './handleCreateBasicWindow';
import type { IWindowOpenConfig, IWindowService } from './interface';
import { registerBrowserViewWindowListeners } from './registerBrowserViewWindowListeners';
@@ -31,8 +33,8 @@ import { getPreloadPath } from './viteEntry';
export class Window implements IWindowService {
private readonly windows = new Map();
private windowMeta = {} as Partial;
- /** menubar version of main window, if user set openInMenubar to true in preferences */
- private mainWindowMenuBar?: Menubar;
+ /** tidgi mini window version of main window, if user set attachToTidgiMiniWindow to true in preferences */
+ private tidgiMiniWindowMenubar?: Menubar;
constructor(
@inject(serviceIdentifier.Preference) private readonly preferenceService: IPreferenceService,
@@ -76,8 +78,8 @@ export class Window implements IWindowService {
}
public get(windowName: WindowNames = WindowNames.main): BrowserWindow | undefined {
- if (windowName === WindowNames.menuBar) {
- return this.mainWindowMenuBar?.window;
+ if (windowName === WindowNames.tidgiMiniWindow) {
+ return this.tidgiMiniWindowMenubar?.window;
}
return this.windows.get(windowName);
}
@@ -124,8 +126,8 @@ export class Window implements IWindowService {
this.windows.clear();
}
- public async isMenubarOpen(): Promise {
- return this.mainWindowMenuBar?.window?.isFocused() ?? false;
+ public async isTidgiMiniWindowOpen(): Promise {
+ return this.tidgiMiniWindowMenubar?.window?.isVisible() ?? false;
}
public async open(windowName: N, meta?: WindowMeta[N], config?: IWindowOpenConfig): Promise;
@@ -164,11 +166,11 @@ export class Window implements IWindowService {
// create new window
const preferenceService = container.get(serviceIdentifier.Preference);
- const { hideMenuBar: autoHideMenuBar, titleBar: showTitleBar, menuBarAlwaysOnTop, alwaysOnTop } = preferenceService.getPreferences();
+ const { hideMenuBar: autoHideMenuBar, titleBar: showTitleBar, tidgiMiniWindowAlwaysOnTop, alwaysOnTop } = preferenceService.getPreferences();
let windowWithBrowserViewConfig: Partial = {};
let windowWithBrowserViewState: windowStateKeeperState | undefined;
- const WindowToKeepPositionState = [WindowNames.main, WindowNames.menuBar];
- const WindowWithBrowserView = [WindowNames.main, WindowNames.menuBar, WindowNames.secondary];
+ const WindowToKeepPositionState = [WindowNames.main, WindowNames.tidgiMiniWindow];
+ const WindowWithBrowserView = [WindowNames.main, WindowNames.tidgiMiniWindow, WindowNames.secondary];
const isWindowWithBrowserView = WindowWithBrowserView.includes(windowName);
if (WindowToKeepPositionState.includes(windowName)) {
windowWithBrowserViewState = windowStateKeeper({
@@ -185,7 +187,7 @@ export class Window implements IWindowService {
};
}
// hide titleBar should not take effect on setting window
- const hideTitleBar = [WindowNames.main, WindowNames.menuBar].includes(windowName) && !showTitleBar;
+ const hideTitleBar = [WindowNames.main, WindowNames.tidgiMiniWindow].includes(windowName) && !showTitleBar;
const windowConfig: BrowserWindowConstructorOptions = {
...windowDimension[windowName],
...windowWithBrowserViewConfig,
@@ -197,7 +199,7 @@ export class Window implements IWindowService {
titleBarStyle: hideTitleBar ? 'hidden' : 'default',
// https://www.electronjs.org/docs/latest/tutorial/custom-title-bar#add-native-window-controls-windows-linux
...(hideTitleBar && process.platform !== 'darwin' ? { titleBarOverlay: true } : {}),
- alwaysOnTop: windowName === WindowNames.menuBar ? menuBarAlwaysOnTop : alwaysOnTop,
+ alwaysOnTop: windowName === WindowNames.tidgiMiniWindow ? tidgiMiniWindowAlwaysOnTop : alwaysOnTop,
webPreferences: {
devTools: !isTest,
nodeIntegration: false,
@@ -214,12 +216,12 @@ export class Window implements IWindowService {
parent: isWindowWithBrowserView ? undefined : this.get(WindowNames.main),
};
let newWindow: BrowserWindow;
- if (windowName === WindowNames.menuBar) {
- this.mainWindowMenuBar = await handleAttachToMenuBar(windowConfig, windowWithBrowserViewState);
- if (this.mainWindowMenuBar.window === undefined) {
- throw new Error('MenuBar failed to create window.');
+ if (windowName === WindowNames.tidgiMiniWindow) {
+ this.tidgiMiniWindowMenubar = await handleAttachToTidgiMiniWindow(windowConfig, windowWithBrowserViewState);
+ if (this.tidgiMiniWindowMenubar.window === undefined) {
+ throw new Error('TidgiMiniWindow failed to create window.');
}
- newWindow = this.mainWindowMenuBar.window;
+ newWindow = this.tidgiMiniWindowMenubar.window;
} else {
newWindow = await handleCreateBasicWindow(windowName, windowConfig, meta, config);
if (isWindowWithBrowserView) {
@@ -337,4 +339,243 @@ export class Window implements IWindowService {
}
}
}
+
+ public async toggleTidgiMiniWindow(): Promise {
+ logger.info('toggleTidgiMiniWindow called', { function: 'toggleTidgiMiniWindow' });
+ try {
+ const preferenceService = container.get(serviceIdentifier.Preference);
+
+ const isOpen = await this.isTidgiMiniWindowOpen();
+ logger.debug('TidgiMiniWindow open status checked', { function: 'toggleTidgiMiniWindow', isOpen });
+ if (isOpen) {
+ logger.info('Closing tidgi mini window', { function: 'toggleTidgiMiniWindow' });
+ await this.closeTidgiMiniWindow();
+ } else {
+ const tidgiMiniWindow = await preferenceService.get('tidgiMiniWindow');
+ logger.debug('tidgiMiniWindow preference checked', { function: 'toggleTidgiMiniWindow', tidgiMiniWindow });
+ if (tidgiMiniWindow) {
+ logger.info('Opening tidgi mini window', { function: 'toggleTidgiMiniWindow' });
+ await this.openTidgiMiniWindow(true, true); // Explicitly show window when toggling
+ } else {
+ logger.warn('Cannot open tidgi mini window: tidgiMiniWindow preference is disabled', { function: 'toggleTidgiMiniWindow' });
+ }
+ }
+ } catch (error) {
+ logger.error('Failed to open/hide tidgi mini window', { error });
+ }
+ }
+
+ public async openTidgiMiniWindow(enableIt = true, showWindow = true): Promise {
+ try {
+ // Check if tidgi mini window is already enabled
+ if (this.tidgiMiniWindowMenubar?.window !== undefined) {
+ logger.debug('TidGi mini window is already enabled, bring it to front', { function: 'openTidgiMiniWindow' });
+ if (showWindow) {
+ // Before showing, get the target workspace
+ const { shouldSync, targetWorkspaceId } = await getTidgiMiniWindowTargetWorkspace();
+
+ logger.info('openTidgiMiniWindow: preparing to show window', {
+ function: 'openTidgiMiniWindow',
+ shouldSync,
+ targetWorkspaceId,
+ });
+
+ // Ensure view exists for the target workspace before realigning
+ if (targetWorkspaceId) {
+ const targetWorkspace = await container.get(serviceIdentifier.Workspace).get(targetWorkspaceId);
+ if (targetWorkspace && !targetWorkspace.pageType) {
+ // This is a wiki workspace - ensure it has a view for tidgi mini window
+ const viewService = container.get(serviceIdentifier.View);
+ const existingView = viewService.getView(targetWorkspace.id, WindowNames.tidgiMiniWindow);
+ if (!existingView) {
+ logger.info('openTidgiMiniWindow: creating missing tidgi mini window view', {
+ function: 'openTidgiMiniWindow',
+ workspaceId: targetWorkspace.id,
+ });
+ await viewService.addView(targetWorkspace, WindowNames.tidgiMiniWindow);
+ }
+ }
+
+ logger.info('openTidgiMiniWindow: calling realignActiveWorkspace', {
+ function: 'openTidgiMiniWindow',
+ targetWorkspaceId,
+ });
+ await container.get(serviceIdentifier.WorkspaceView).realignActiveWorkspace(targetWorkspaceId);
+ logger.info('openTidgiMiniWindow: realignActiveWorkspace completed', {
+ function: 'openTidgiMiniWindow',
+ targetWorkspaceId,
+ });
+ }
+
+ // Use menuBar.showWindow() instead of direct window.show() for proper tidgi mini window behavior
+ void this.tidgiMiniWindowMenubar.showWindow();
+ }
+ return;
+ }
+
+ // Create tidgi mini window (create and open when enableIt is true)
+ await this.open(WindowNames.tidgiMiniWindow);
+ if (enableIt) {
+ logger.debug('TidGi mini window enabled', { function: 'openTidgiMiniWindow' });
+ // After creating the tidgi mini window, show it if requested
+ if (showWindow && this.tidgiMiniWindowMenubar) {
+ logger.debug('Showing newly created tidgi mini window', { function: 'openTidgiMiniWindow' });
+ void this.tidgiMiniWindowMenubar.showWindow();
+ }
+ }
+ } catch (error) {
+ logger.error('Failed to open tidgi mini window', { error, function: 'openTidgiMiniWindow' });
+ throw error;
+ }
+ }
+
+ public async closeTidgiMiniWindow(disableIt = false): Promise {
+ try {
+ // Check if tidgi mini window exists
+ if (this.tidgiMiniWindowMenubar === undefined) {
+ logger.debug('TidGi mini window is already disabled', { function: 'closeTidgiMiniWindow' });
+ return;
+ }
+ const menuBar = this.tidgiMiniWindowMenubar;
+ if (disableIt) {
+ // Fully destroy tidgi mini window: destroy window and tray, then clear reference
+ if (menuBar.window) {
+ // remove custom close listener so destroy will actually close
+ menuBar.window.removeAllListeners('close');
+ menuBar.window.destroy();
+ }
+ // hide app on mac if needed
+ menuBar.app?.hide?.();
+ if (menuBar.tray && !menuBar.tray.isDestroyed()) {
+ menuBar.tray.destroy();
+ }
+ this.tidgiMiniWindowMenubar = undefined;
+ logger.debug('TidGi mini window disabled successfully without restart', { function: 'closeTidgiMiniWindow' });
+ } else {
+ // Only hide the tidgi mini window (keep tray and instance for re-open)
+ // Use menuBar.hideWindow() for proper tidgi mini window behavior
+ menuBar.hideWindow();
+ logger.debug('TidGi mini window closed (kept enabled)', { function: 'closeTidgiMiniWindow' });
+ }
+ } catch (error) {
+ logger.error('Failed to close tidgi mini window', { error });
+ throw error;
+ }
+ }
+
+ public async updateWindowProperties(windowName: WindowNames, properties: { alwaysOnTop?: boolean }): Promise {
+ try {
+ const window = this.get(windowName);
+ if (window === undefined) {
+ logger.warn(`Window ${windowName} not found for property update`);
+ return;
+ }
+
+ if (properties.alwaysOnTop !== undefined) {
+ window.setAlwaysOnTop(properties.alwaysOnTop);
+ logger.info(`Updated ${windowName} alwaysOnTop to ${properties.alwaysOnTop}`);
+ }
+
+ // Handle tidgi mini window specific properties
+ if (windowName === WindowNames.tidgiMiniWindow && this.tidgiMiniWindowMenubar?.window) {
+ if (properties.alwaysOnTop !== undefined) {
+ this.tidgiMiniWindowMenubar.window.setAlwaysOnTop(properties.alwaysOnTop);
+ }
+ }
+ } catch (error) {
+ logger.error(`Failed to update window properties for ${windowName}`, { error, properties });
+ throw error;
+ }
+ }
+
+ public async reactWhenPreferencesChanged(key: string, value: unknown): Promise {
+ switch (key) {
+ case 'tidgiMiniWindow': {
+ if (value) {
+ // Enable tidgi mini window without showing the window; visibility controlled by toggle/shortcut
+ await this.openTidgiMiniWindow(true, false);
+
+ // After enabling tidgi mini window, create view for the current active workspace (if it's a wiki workspace)
+ const workspaceService = container.get(serviceIdentifier.Workspace);
+ const viewService = container.get(serviceIdentifier.View);
+ const activeWorkspace = await workspaceService.getActiveWorkspace();
+
+ if (activeWorkspace && !activeWorkspace.pageType) {
+ // This is a wiki workspace - ensure it has a view for tidgi mini window
+ const existingView = viewService.getView(activeWorkspace.id, WindowNames.tidgiMiniWindow);
+ if (!existingView) {
+ await viewService.addView(activeWorkspace, WindowNames.tidgiMiniWindow);
+ }
+ }
+ } else {
+ await this.closeTidgiMiniWindow(true);
+ }
+ return;
+ }
+ case 'tidgiMiniWindowSyncWorkspaceWithMainWindow':
+ case 'tidgiMiniWindowFixedWorkspaceId': {
+ logger.info('Preference changed', { function: 'reactWhenPreferencesChanged', key, value: JSON.stringify(value) });
+
+ // When switching to sync with main window, hide the sidebar
+ if (key === 'tidgiMiniWindowSyncWorkspaceWithMainWindow' && value === true) {
+ await this.preferenceService.set('tidgiMiniWindowShowSidebar', false);
+ }
+
+ // When tidgi mini window workspace settings change, hide all views and let the next window show trigger realignment
+ const tidgiMiniWindow = this.get(WindowNames.tidgiMiniWindow);
+ if (tidgiMiniWindow) {
+ const workspaceService = container.get(serviceIdentifier.Workspace);
+ const viewService = container.get(serviceIdentifier.View);
+ const allWorkspaces = await workspaceService.getWorkspacesAsList();
+ logger.debug(`Hiding all tidgi mini window views (${allWorkspaces.length} workspaces)`, { function: 'reactWhenPreferencesChanged', key });
+ // Hide all views - the correct view will be shown when window is next opened
+ await Promise.all(
+ allWorkspaces.map(async (workspace) => {
+ const view = viewService.getView(workspace.id, WindowNames.tidgiMiniWindow);
+ if (view) {
+ await viewService.hideView(tidgiMiniWindow, WindowNames.tidgiMiniWindow, workspace.id);
+ }
+ }),
+ );
+ // View creation is handled by openTidgiMiniWindow when the window is shown
+ } else {
+ logger.warn('tidgiMiniWindow not found, skipping view management', { function: 'reactWhenPreferencesChanged', key });
+ }
+ return;
+ }
+ case 'tidgiMiniWindowAlwaysOnTop': {
+ await this.updateWindowProperties(WindowNames.tidgiMiniWindow, { alwaysOnTop: value as boolean });
+ return;
+ }
+ case 'tidgiMiniWindowShowTitleBar': {
+ // Title bar style requires recreating the window
+ // We need to fully destroy and recreate the tidgi mini window with new titleBar settings
+ logger.info('tidgiMiniWindowShowTitleBar changed, recreating tidgi mini window', {
+ function: 'reactWhenPreferencesChanged',
+ newValue: value,
+ });
+
+ const wasVisible = await this.isTidgiMiniWindowOpen();
+ logger.debug('Current tidgi mini window visibility', {
+ function: 'reactWhenPreferencesChanged',
+ wasVisible,
+ });
+
+ // Fully destroy current tidgi mini window (disableIt = true)
+ await this.closeTidgiMiniWindow(true);
+ logger.debug('Tidgi mini window destroyed', { function: 'reactWhenPreferencesChanged' });
+
+ // Reopen tidgi mini window with new titleBar setting from updated preferences
+ // enableIt = true to recreate, showWindow = wasVisible to restore visibility
+ await this.openTidgiMiniWindow(true, wasVisible);
+ logger.info('Tidgi mini window recreated with new titleBar setting', {
+ function: 'reactWhenPreferencesChanged',
+ showWindow: wasVisible,
+ });
+ return;
+ }
+ default:
+ break;
+ }
+ }
}
diff --git a/src/services/windows/interface.ts b/src/services/windows/interface.ts
index e36ec796..2ccbba9a 100644
--- a/src/services/windows/interface.ts
+++ b/src/services/windows/interface.ts
@@ -37,7 +37,7 @@ export interface IWindowService {
*/
hide(windowName: WindowNames): Promise;
isFullScreen(windowName?: WindowNames): Promise;
- isMenubarOpen(): Promise;
+ isTidgiMiniWindowOpen(): Promise;
loadURL(windowName: WindowNames, newUrl?: string): Promise;
maximize(): Promise;
/**
@@ -55,7 +55,16 @@ export interface IWindowService {
set(windowName: WindowNames, win: BrowserWindow | undefined): void;
setWindowMeta(windowName: N, meta?: WindowMeta[N]): Promise;
stopFindInPage(close?: boolean, windowName?: WindowNames): Promise;
+ toggleTidgiMiniWindow(): Promise;
updateWindowMeta(windowName: N, meta?: WindowMeta[N]): Promise;
+ /** Open tidgi mini window without restart - hot reload. enableIt=true means fully enable and open. */
+ openTidgiMiniWindow(enableIt?: boolean, showWindow?: boolean): Promise;
+ /** Close tidgi mini window. disableIt=true means fully disable and cleanup tray. */
+ closeTidgiMiniWindow(disableIt?: boolean): Promise;
+ /** Update window properties without restart - hot reload */
+ updateWindowProperties(windowName: WindowNames, properties: { alwaysOnTop?: boolean }): Promise;
+ /** React to preference changes related to windows (tidgi mini window etc.) */
+ reactWhenPreferencesChanged(key: string, value: unknown): Promise;
}
export const WindowServiceIPCDescriptor = {
channel: WindowChannel.name,
@@ -69,7 +78,7 @@ export const WindowServiceIPCDescriptor = {
goForward: ProxyPropertyType.Function,
goHome: ProxyPropertyType.Function,
isFullScreen: ProxyPropertyType.Function,
- isMenubarOpen: ProxyPropertyType.Function,
+ isTidgiMiniWindowOpen: ProxyPropertyType.Function,
loadURL: ProxyPropertyType.Function,
maximize: ProxyPropertyType.Function,
open: ProxyPropertyType.Function,
@@ -78,6 +87,10 @@ export const WindowServiceIPCDescriptor = {
sendToAllWindows: ProxyPropertyType.Function,
setWindowMeta: ProxyPropertyType.Function,
stopFindInPage: ProxyPropertyType.Function,
+ toggleTidgiMiniWindow: ProxyPropertyType.Function,
updateWindowMeta: ProxyPropertyType.Function,
+ openTidgiMiniWindow: ProxyPropertyType.Function,
+ closeTidgiMiniWindow: ProxyPropertyType.Function,
+ updateWindowProperties: ProxyPropertyType.Function,
},
};
diff --git a/src/services/windows/registerMenu.ts b/src/services/windows/registerMenu.ts
index 7d601300..76c9f6f7 100644
--- a/src/services/windows/registerMenu.ts
+++ b/src/services/windows/registerMenu.ts
@@ -123,10 +123,6 @@ export async function registerMenu(): Promise {
// if back is called in popup window
// navigate in the popup window instead
if (browserWindow !== undefined) {
- // TODO: test if we really can get this isPopup value, and it works for help page popup and menubar window
- // const { isPopup = false } = await getFromRenderer(MetaDataChannel.getViewMetaData, browserWindow);
- // const windowName = isPopup ? WindowNames.menuBar : WindowNames.main
-
await windowService.goForward();
}
ipcMain.emit('request-go-forward');
diff --git a/src/services/workspacesView/index.ts b/src/services/workspacesView/index.ts
index 2dcac7e1..efc68e87 100644
--- a/src/services/workspacesView/index.ts
+++ b/src/services/workspacesView/index.ts
@@ -24,6 +24,7 @@ import { DELAY_MENU_REGISTER } from '@/constants/parameters';
import type { ISyncService } from '@services/sync/interface';
import type { IInitializeWorkspaceOptions, IWorkspaceViewService } from './interface';
import { registerMenu } from './registerMenu';
+import { getTidgiMiniWindowTargetWorkspace } from './utilities';
@injectable()
export class WorkspaceView implements IWorkspaceViewService {
@@ -153,11 +154,9 @@ export class WorkspaceView implements IWorkspaceViewService {
}
}
} catch (error) {
- const error_ = error instanceof Error ? error : new Error(String(error));
logger.error('wikiStartup sync failed', {
function: 'initializeAllWorkspaceView',
- error: error_.message,
- stack: error_.stack ?? 'no stack',
+ error,
});
}
};
@@ -173,7 +172,7 @@ export class WorkspaceView implements IWorkspaceViewService {
logger.debug('Skip because alreadyHaveView');
return;
}
- // Create browserView, and if user want a menubar, we also create a new window for that
+ // Create browserView, and if user want a tidgi mini window, we also create a new window for that
await this.addViewForAllBrowserViews(workspace);
if (isNew && options.from === WikiCreationMethod.Create) {
const view = container.get(serviceIdentifier.View).getView(workspace.id, WindowNames.main);
@@ -205,12 +204,28 @@ export class WorkspaceView implements IWorkspaceViewService {
}
public async addViewForAllBrowserViews(workspace: IWorkspace): Promise {
- await Promise.all([
- container.get(serviceIdentifier.View).addView(workspace, WindowNames.main),
- this.preferenceService.get('attachToMenubar').then(async (attachToMenubar) => {
- return await (attachToMenubar && container.get(serviceIdentifier.View).addView(workspace, WindowNames.menuBar));
- }),
- ]);
+ const mainTask = container.get(serviceIdentifier.View).addView(workspace, WindowNames.main);
+
+ // For tidgi mini window, decide which workspace to show based on preferences
+ const tidgiMiniWindowTask = (async () => {
+ const tidgiMiniWindow = await this.preferenceService.get('tidgiMiniWindow');
+ if (!tidgiMiniWindow) {
+ return;
+ }
+ const { shouldSync, targetWorkspaceId } = await getTidgiMiniWindowTargetWorkspace(workspace.id);
+ // If syncing with main window, use the current workspace
+ if (shouldSync) {
+ await container.get(serviceIdentifier.View).addView(workspace, WindowNames.tidgiMiniWindow);
+ return;
+ }
+ // If not syncing and a fixed workspace is set, only add view if this IS the fixed workspace
+ if (targetWorkspaceId && workspace.id === targetWorkspaceId) {
+ await container.get(serviceIdentifier.View).addView(workspace, WindowNames.tidgiMiniWindow);
+ }
+ // If not syncing and no fixed workspace is set, don't add any view (user needs to select one)
+ })();
+
+ await Promise.all([mainTask, tidgiMiniWindowTask]);
}
public async openWorkspaceWindowWithView(workspace: IWorkspace, configs?: { uri?: string }): Promise {
@@ -355,11 +370,9 @@ export class WorkspaceView implements IWorkspaceViewService {
await container.get(serviceIdentifier.View).setActiveViewForAllBrowserViews(nextWorkspaceID);
await this.realignActiveWorkspace(nextWorkspaceID);
} catch (error) {
- const error_ = error instanceof Error ? error : new Error(String(error));
logger.error('setActiveWorkspaceView error', {
function: 'setActiveWorkspaceView',
- error: error_.message,
- errorObj: error_,
+ error,
});
throw error;
}
@@ -388,11 +401,9 @@ export class WorkspaceView implements IWorkspaceViewService {
try {
await this.hideWorkspaceView(activeWorkspace.id);
} catch (error) {
- const error_ = error instanceof Error ? error : new Error(String(error));
logger.error('setActiveWorkspaceView error', {
function: 'clearActiveWorkspaceView',
- error: error_.message,
- errorObj: error_,
+ error,
});
throw error;
}
@@ -461,7 +472,7 @@ export class WorkspaceView implements IWorkspaceViewService {
await Promise.all(
workspaces.map(async (workspace) => {
await Promise.all(
- [WindowNames.main, WindowNames.menuBar].map(async (windowName) => {
+ [WindowNames.main, WindowNames.tidgiMiniWindow].map(async (windowName) => {
const view = container.get(serviceIdentifier.View).getView(workspace.id, windowName);
if (view !== undefined) {
await container.get(serviceIdentifier.View).loadUrlForView(workspace, view);
@@ -529,11 +540,9 @@ export class WorkspaceView implements IWorkspaceViewService {
try {
await container.get(serviceIdentifier.MenuService).buildMenu();
} catch (error) {
- const error_ = error instanceof Error ? error : new Error(String(error));
logger.error('realignActiveWorkspace buildMenu error', {
function: 'realignActiveWorkspace',
- error: error_.message,
- errorObj: error_,
+ error,
});
throw error;
}
@@ -556,7 +565,7 @@ export class WorkspaceView implements IWorkspaceViewService {
return;
}
const mainWindow = container.get(serviceIdentifier.Window).get(WindowNames.main);
- const menuBarWindow = container.get(serviceIdentifier.Window).get(WindowNames.menuBar);
+ const tidgiMiniWindow = container.get(serviceIdentifier.Window).get(WindowNames.tidgiMiniWindow);
logger.info(
`realignActiveWorkspaceView: id ${workspaceToRealign?.id ?? 'undefined'}`,
@@ -565,7 +574,7 @@ export class WorkspaceView implements IWorkspaceViewService {
logger.warn('realignActiveWorkspaceView: no active workspace');
return;
}
- if (mainWindow === undefined && menuBarWindow === undefined) {
+ if (mainWindow === undefined && tidgiMiniWindow === undefined) {
logger.warn('realignActiveWorkspaceView: no active window');
return;
}
@@ -576,18 +585,20 @@ export class WorkspaceView implements IWorkspaceViewService {
tasks.push(container.get(serviceIdentifier.View).realignActiveView(mainWindow, workspaceToRealign.id, WindowNames.main));
logger.debug(`realignActiveWorkspaceView: realign main window for ${workspaceToRealign.id}.`);
}
- if (menuBarWindow === undefined) {
- logger.info(`realignActiveWorkspaceView: no menuBarBrowserViewWebContent, skip menu bar window for ${workspaceToRealign.id}.`);
+ if (tidgiMiniWindow === undefined) {
+ logger.info(`realignActiveWorkspaceView: no tidgiMiniWindowBrowserViewWebContent, skip tidgi mini window for ${workspaceToRealign.id}.`);
} else {
- logger.debug(`realignActiveWorkspaceView: realign menu bar window for ${workspaceToRealign.id}.`);
- tasks.push(container.get(serviceIdentifier.View).realignActiveView(menuBarWindow, workspaceToRealign.id, WindowNames.menuBar));
+ // For tidgi mini window, decide which workspace to show based on preferences
+ const { targetWorkspaceId } = await getTidgiMiniWindowTargetWorkspace(workspaceToRealign.id);
+ const tidgiMiniWindowWorkspaceId = targetWorkspaceId || workspaceToRealign.id;
+ tasks.push(container.get(serviceIdentifier.View).realignActiveView(tidgiMiniWindow, tidgiMiniWindowWorkspaceId, WindowNames.tidgiMiniWindow));
}
await Promise.all(tasks);
}
private async hideWorkspaceView(idToDeactivate: string): Promise {
const mainWindow = container.get(serviceIdentifier.Window).get(WindowNames.main);
- const menuBarWindow = container.get(serviceIdentifier.Window).get(WindowNames.menuBar);
+ const tidgiMiniWindow = container.get(serviceIdentifier.Window).get(WindowNames.tidgiMiniWindow);
const tasks = [];
if (mainWindow === undefined) {
logger.warn(`hideWorkspaceView: no mainBrowserWindow, skip main window browserView.`);
@@ -595,11 +606,20 @@ export class WorkspaceView implements IWorkspaceViewService {
logger.info(`hideWorkspaceView: hide main window browserView.`);
tasks.push(container.get(serviceIdentifier.View).hideView(mainWindow, WindowNames.main, idToDeactivate));
}
- if (menuBarWindow === undefined) {
- logger.debug(`hideWorkspaceView: no menuBarBrowserWindow, skip menu bar window browserView.`);
+ if (tidgiMiniWindow === undefined) {
+ logger.debug(`hideWorkspaceView: no tidgiMiniWindowBrowserWindow, skip tidgi mini window browserView.`);
} else {
- logger.info(`hideWorkspaceView: hide menu bar window browserView.`);
- tasks.push(container.get(serviceIdentifier.View).hideView(menuBarWindow, WindowNames.menuBar, idToDeactivate));
+ // For tidgi mini window, only hide if syncing with main window OR if this is the fixed workspace being deactivated
+ const { shouldSync, targetWorkspaceId } = await getTidgiMiniWindowTargetWorkspace(idToDeactivate);
+ // Only hide tidgi mini window view if:
+ // 1. Syncing with main window (should hide when main window hides)
+ // 2. OR the workspace being hidden is the fixed workspace (rare case, but should be handled)
+ if (shouldSync || idToDeactivate === targetWorkspaceId) {
+ logger.info(`hideWorkspaceView: hide tidgi mini window browserView.`);
+ tasks.push(container.get(serviceIdentifier.View).hideView(tidgiMiniWindow, WindowNames.tidgiMiniWindow, idToDeactivate));
+ } else {
+ logger.debug(`hideWorkspaceView: skip hiding tidgi mini window browserView (fixed workspace: ${targetWorkspaceId || 'none'}).`);
+ }
}
await Promise.all(tasks);
logger.info(`hideWorkspaceView: done.`);
diff --git a/src/services/workspacesView/registerMenu.ts b/src/services/workspacesView/registerMenu.ts
index e84b1bf9..42bcefa8 100644
--- a/src/services/workspacesView/registerMenu.ts
+++ b/src/services/workspacesView/registerMenu.ts
@@ -65,8 +65,7 @@ export async function registerMenu(): Promise {
if (error instanceof DownloadCancelError) {
logger.debug('cancelled', { function: 'registerMenu.printPage' });
} else {
- const error_ = error instanceof Error ? error : new Error(String(error));
- logger.error('print page error', { function: 'registerMenu.printPage', error: error_.message, errorObj: error_ });
+ logger.error('print page error', { function: 'registerMenu.printPage', error });
}
}
},
diff --git a/src/services/workspacesView/utilities.ts b/src/services/workspacesView/utilities.ts
new file mode 100644
index 00000000..2c60222b
--- /dev/null
+++ b/src/services/workspacesView/utilities.ts
@@ -0,0 +1,34 @@
+import { container } from '@services/container';
+import type { IPreferenceService } from '@services/preferences/interface';
+import serviceIdentifier from '@services/serviceIdentifier';
+import type { IWorkspaceService } from '@services/workspaces/interface';
+
+/**
+ * Helper function to determine the target workspace for tidgi mini window based on preferences
+ * @param fallbackWorkspaceId - The workspace ID to use as fallback (usually the active/current workspace)
+ * @returns Object containing shouldSync flag and targetWorkspaceId
+ */
+export async function getTidgiMiniWindowTargetWorkspace(fallbackWorkspaceId?: string): Promise<{
+ shouldSync: boolean;
+ targetWorkspaceId: string | undefined;
+}> {
+ const preferenceService = container.get(serviceIdentifier.Preference);
+ const [tidgiMiniWindowSyncWorkspaceWithMainWindow, tidgiMiniWindowFixedWorkspaceId] = await Promise.all([
+ preferenceService.get('tidgiMiniWindowSyncWorkspaceWithMainWindow'),
+ preferenceService.get('tidgiMiniWindowFixedWorkspaceId'),
+ ]);
+
+ // Default to sync (undefined means default to true, or explicitly true)
+ const shouldSync = tidgiMiniWindowSyncWorkspaceWithMainWindow === undefined || tidgiMiniWindowSyncWorkspaceWithMainWindow;
+
+ let targetWorkspaceId: string | undefined;
+ if (shouldSync) {
+ // Sync with main window - use fallback or active workspace
+ targetWorkspaceId = fallbackWorkspaceId ?? (await container.get(serviceIdentifier.Workspace).getActiveWorkspace())?.id;
+ } else {
+ // Use fixed workspace
+ targetWorkspaceId = tidgiMiniWindowFixedWorkspaceId;
+ }
+
+ return { shouldSync, targetWorkspaceId };
+}
diff --git a/src/windows/Preferences/index.tsx b/src/windows/Preferences/index.tsx
index 26f0379d..83a5d602 100644
--- a/src/windows/Preferences/index.tsx
+++ b/src/windows/Preferences/index.tsx
@@ -23,6 +23,7 @@ import { Search } from './sections/Search';
import { Sync } from './sections/Sync';
import { System } from './sections/System';
import { TiddlyWiki } from './sections/TiddlyWiki';
+import { TidGiMiniWindow } from './sections/TidGiMiniWindow';
import { Updates } from './sections/Updates';
import { SectionSideBar } from './SectionsSideBar';
import { usePreferenceSections } from './useSections';
@@ -66,6 +67,7 @@ export default function Preferences(): React.JSX.Element {
+
diff --git a/src/windows/Preferences/sections/AIAgent.tsx b/src/windows/Preferences/sections/AIAgent.tsx
index 11e0bc29..8bc7a24c 100644
--- a/src/windows/Preferences/sections/AIAgent.tsx
+++ b/src/windows/Preferences/sections/AIAgent.tsx
@@ -24,7 +24,7 @@ export function AIAgent(props: ISectionProps): React.JSX.Element {
'AIAgent: fetch agent database info failed',
{
function: 'AIAgent.fetchInfo',
- error: String(error),
+ error,
},
);
}
@@ -54,7 +54,7 @@ export function AIAgent(props: ISectionProps): React.JSX.Element {
'AIAgent: open database folder failed',
{
function: 'AIAgent.openDatabaseFolder',
- error: String(error),
+ error,
path: agentInfo.path,
},
);
@@ -118,7 +118,7 @@ export function AIAgent(props: ISectionProps): React.JSX.Element {
'AIAgent: delete agent database failed',
{
function: 'AIAgent.handleDelete',
- error: String(error),
+ error,
},
);
}
diff --git a/src/windows/Preferences/sections/DeveloperTools.tsx b/src/windows/Preferences/sections/DeveloperTools.tsx
index 0a0e4f40..96635ba9 100644
--- a/src/windows/Preferences/sections/DeveloperTools.tsx
+++ b/src/windows/Preferences/sections/DeveloperTools.tsx
@@ -33,7 +33,7 @@ export function DeveloperTools(props: ISectionProps): React.JSX.Element {
'DeveloperTools: fetch externalApi database info failed',
{
function: 'DeveloperTools.fetchInfo',
- error: String(error),
+ error,
},
);
}
@@ -79,7 +79,7 @@ export function DeveloperTools(props: ISectionProps): React.JSX.Element {
'DeveloperTools: open V8 cache folder failed',
{
function: 'DeveloperTools.openV8CacheFolder',
- error: String(error),
+ error: error as Error,
},
);
}
@@ -133,7 +133,7 @@ export function DeveloperTools(props: ISectionProps): React.JSX.Element {
'DeveloperTools: open externalApi database folder failed',
{
function: 'DeveloperTools.openExternalApiDatabaseFolder',
- error: String(error),
+ error,
path: externalApiInfo.path,
},
);
@@ -202,7 +202,7 @@ export function DeveloperTools(props: ISectionProps): React.JSX.Element {
'DeveloperTools: delete externalApi database failed',
{
function: 'DeveloperTools.handleDelete',
- error: String(error),
+ error,
},
);
}
diff --git a/src/windows/Preferences/sections/Downloads.tsx b/src/windows/Preferences/sections/Downloads.tsx
index 0c4a36aa..1325c577 100644
--- a/src/windows/Preferences/sections/Downloads.tsx
+++ b/src/windows/Preferences/sections/Downloads.tsx
@@ -29,7 +29,7 @@ export function Downloads(props: Required): React.JSX.Element {
}
})
.catch((error: unknown) => {
- void window.service.native.log('error', 'Preferences: pickDirectory failed', { function: 'Downloads.pickDirectory', error: String(error) });
+ void window.service.native.log('error', 'Preferences: pickDirectory failed', { function: 'Downloads.pickDirectory', error });
});
}}
>
diff --git a/src/windows/Preferences/sections/ExternalAPI/components/AIModelParametersDialog.tsx b/src/windows/Preferences/sections/ExternalAPI/components/AIModelParametersDialog.tsx
index 99f10c5a..e2007a32 100644
--- a/src/windows/Preferences/sections/ExternalAPI/components/AIModelParametersDialog.tsx
+++ b/src/windows/Preferences/sections/ExternalAPI/components/AIModelParametersDialog.tsx
@@ -51,7 +51,7 @@ export function AIModelParametersDialog({ open, onClose, config, onSave }: AIMod
await onSave(newConfig);
onClose();
} catch (error) {
- void window.service.native.log('error', 'Failed to save model parameters', { function: 'AIModelParametersDialog.handleSave', error: String(error) });
+ void window.service.native.log('error', 'Failed to save model parameters', { function: 'AIModelParametersDialog.handleSave', error });
}
};
diff --git a/src/windows/Preferences/sections/ExternalAPI/components/ProviderConfig.tsx b/src/windows/Preferences/sections/ExternalAPI/components/ProviderConfig.tsx
index 18fda80b..c1ff1123 100644
--- a/src/windows/Preferences/sections/ExternalAPI/components/ProviderConfig.tsx
+++ b/src/windows/Preferences/sections/ExternalAPI/components/ProviderConfig.tsx
@@ -108,7 +108,7 @@ async function autoFillDefaultModels(
} catch (error) {
void window.service.native.log('error', 'Failed to auto-fill default models', {
function: 'autoFillDefaultModels',
- error: String(error),
+ error,
});
}
}
@@ -208,7 +208,7 @@ export function ProviderConfig({
await window.service.externalAPI.updateProvider(providerName, { [field]: value });
showMessage(t('Preference.SettingsSaved'), 'success');
} catch (error) {
- void window.service.native.log('error', 'Failed to update provider', { function: 'ProviderConfig.handleFormChange', error: String(error) });
+ void window.service.native.log('error', 'Failed to update provider', { function: 'ProviderConfig.handleFormChange', error });
showMessage(t('Preference.FailedToSaveSettings'), 'error');
}
};
@@ -222,7 +222,7 @@ export function ProviderConfig({
showMessage(enabled ? t('Preference.ProviderEnabled') : t('Preference.ProviderDisabled'), 'success');
} catch (error) {
- void window.service.native.log('error', 'Failed to update provider status', { function: 'ProviderConfig.handleProviderEnabledChange', error: String(error) });
+ void window.service.native.log('error', 'Failed to update provider status', { function: 'ProviderConfig.handleProviderEnabledChange', error });
showMessage(t('Preference.FailedToUpdateProviderStatus'), 'error');
}
};
@@ -429,10 +429,10 @@ export function ProviderConfig({
showMessage(editingModelName ? t('Preference.ModelUpdatedSuccessfully') : t('Preference.ModelAddedSuccessfully'), 'success');
closeModelDialog();
}
- } catch (error_) {
+ } catch (error) {
void window.service.native.log('error', editingModelName ? 'Failed to update model' : 'Failed to add model', {
function: 'ProviderConfig.handleAddModel',
- error: String(error_),
+ error,
});
showMessage(editingModelName ? t('Preference.FailedToUpdateModel') : t('Preference.FailedToAddModel'), 'error');
}
@@ -468,8 +468,8 @@ export function ProviderConfig({
setProviders(previous => previous.map(p => p.provider === providerName ? { ...p, models: updatedModels } : p));
showMessage(t('Preference.ModelRemovedSuccessfully'), 'success');
- } catch (error_) {
- void window.service.native.log('error', 'Failed to remove model', { function: 'ProviderConfig.removeModel', error: String(error_) });
+ } catch (error) {
+ void window.service.native.log('error', 'Failed to remove model', { function: 'ProviderConfig.removeModel', error });
showMessage(t('Preference.FailedToRemoveModel'), 'error');
}
};
@@ -623,8 +623,8 @@ export function ProviderConfig({
setShowAddProviderForm(false);
showMessage(t('Preference.ProviderAddedSuccessfully'), 'success');
- } catch (error_) {
- void window.service.native.log('error', 'Failed to add provider', { function: 'ProviderConfig.handleAddProvider', error: String(error_) });
+ } catch (error) {
+ void window.service.native.log('error', 'Failed to add provider', { function: 'ProviderConfig.handleAddProvider', error });
showMessage(t('Preference.FailedToAddProvider'), 'error');
}
};
@@ -655,10 +655,10 @@ export function ProviderConfig({
}
showMessage(t('Preference.ProviderDeleted', { providerName }), 'success');
- } catch (error_) {
+ } catch (error) {
void window.service.native.log('error', 'Failed to delete provider', {
function: 'ProviderConfig.handleDeleteProvider',
- error: String(error_),
+ error,
});
showMessage(t('Preference.FailedToDeleteProvider', { providerName }), 'error');
}
diff --git a/src/windows/Preferences/sections/ExternalAPI/useAIConfigManagement.ts b/src/windows/Preferences/sections/ExternalAPI/useAIConfigManagement.ts
index c5bd59a1..1bb1f174 100644
--- a/src/windows/Preferences/sections/ExternalAPI/useAIConfigManagement.ts
+++ b/src/windows/Preferences/sections/ExternalAPI/useAIConfigManagement.ts
@@ -68,7 +68,7 @@ export const useAIConfigManagement = ({ agentDefId, agentId }: UseAIConfigManage
setLoading(false);
} catch (error) {
- void window.service.native.log('error', 'Failed to load AI configuration', { function: 'useAIConfigManagement.fetchConfig', error: String(error) });
+ void window.service.native.log('error', 'Failed to load AI configuration', { function: 'useAIConfigManagement.fetchConfig', error });
setLoading(false);
}
};
@@ -107,7 +107,7 @@ export const useAIConfigManagement = ({ agentDefId, agentId }: UseAIConfigManage
setConfig(updatedConfig);
await updateConfig(updatedConfig);
} catch (error) {
- void window.service.native.log('error', 'Failed to update model configuration', { function: 'useAIConfigManagement.handleModelChange', error: String(error) });
+ void window.service.native.log('error', 'Failed to update model configuration', { function: 'useAIConfigManagement.handleModelChange', error });
}
}, [config, updateConfig]);
@@ -127,7 +127,7 @@ export const useAIConfigManagement = ({ agentDefId, agentId }: UseAIConfigManage
} catch (error) {
void window.service.native.log('error', 'Failed to update embedding model configuration', {
function: 'useAIConfigManagement.handleEmbeddingModelChange',
- error: String(error),
+ error,
});
}
}, [config, updateConfig]);
@@ -148,7 +148,7 @@ export const useAIConfigManagement = ({ agentDefId, agentId }: UseAIConfigManage
} catch (error) {
void window.service.native.log('error', 'Failed to update speech model configuration', {
function: 'useAIConfigManagement.handleSpeechModelChange',
- error: String(error),
+ error,
});
}
}, [config, updateConfig]);
@@ -169,7 +169,7 @@ export const useAIConfigManagement = ({ agentDefId, agentId }: UseAIConfigManage
} catch (error) {
void window.service.native.log('error', 'Failed to update image generation model configuration', {
function: 'useAIConfigManagement.handleImageGenerationModelChange',
- error: String(error),
+ error,
});
}
}, [config, updateConfig]);
@@ -190,7 +190,7 @@ export const useAIConfigManagement = ({ agentDefId, agentId }: UseAIConfigManage
} catch (error) {
void window.service.native.log('error', 'Failed to update transcriptions model configuration', {
function: 'useAIConfigManagement.handleTranscriptionsModelChange',
- error: String(error),
+ error,
});
}
}, [config, updateConfig]);
@@ -200,7 +200,7 @@ export const useAIConfigManagement = ({ agentDefId, agentId }: UseAIConfigManage
setConfig(newConfig);
await updateConfig(newConfig);
} catch (error) {
- void window.service.native.log('error', 'Failed to update configuration', { function: 'useAIConfigManagement.handleConfigChange', error: String(error) });
+ void window.service.native.log('error', 'Failed to update configuration', { function: 'useAIConfigManagement.handleConfigChange', error });
}
}, [updateConfig]);
diff --git a/src/windows/Preferences/sections/ExternalAPI/useHandlerConfigManagement.ts b/src/windows/Preferences/sections/ExternalAPI/useHandlerConfigManagement.ts
index 4dd6113d..9a89d6ce 100644
--- a/src/windows/Preferences/sections/ExternalAPI/useHandlerConfigManagement.ts
+++ b/src/windows/Preferences/sections/ExternalAPI/useHandlerConfigManagement.ts
@@ -52,14 +52,14 @@ export const useHandlerConfigManagement = ({ agentDefId, agentId }: UseHandlerCo
const handlerSchema = await window.service.agentInstance.getHandlerConfigSchema(handlerID);
setSchema(handlerSchema);
} catch (error) {
- void window.service.native.log('error', 'Failed to load handler schema', { function: 'useHandlerConfigManagement.fetchConfig', error: String(error) });
+ void window.service.native.log('error', 'Failed to load handler schema', { function: 'useHandlerConfigManagement.fetchConfig', error });
}
}
setConfig(finalConfig);
setLoading(false);
} catch (error) {
- void window.service.native.log('error', 'Failed to load handler configuration', { function: 'useHandlerConfigManagement.fetchConfig', error: String(error) });
+ void window.service.native.log('error', 'Failed to load handler configuration', { function: 'useHandlerConfigManagement.fetchConfig', error });
setLoading(false);
}
};
@@ -87,7 +87,7 @@ export const useHandlerConfigManagement = ({ agentDefId, agentId }: UseHandlerCo
void window.service.native.log('error', 'No agent ID or definition ID provided for updating handler config', { function: 'useHandlerConfigManagement.handleConfigChange' });
}
} catch (error) {
- void window.service.native.log('error', 'Failed to update handler configuration', { function: 'useHandlerConfigManagement.handleConfigChange', error: String(error) });
+ void window.service.native.log('error', 'Failed to update handler configuration', { function: 'useHandlerConfigManagement.handleConfigChange', error });
}
}, [agentId, agentDefId]);
diff --git a/src/windows/Preferences/sections/General.tsx b/src/windows/Preferences/sections/General.tsx
index ebafc148..4ebbb049 100644
--- a/src/windows/Preferences/sections/General.tsx
+++ b/src/windows/Preferences/sections/General.tsx
@@ -192,42 +192,6 @@ export function General(props: Required): React.JSX.Element {
- {
- await window.service.preference.set('attachToMenubar', event.target.checked);
- props.requestRestartCountDown();
- }}
- />
- }
- >
-
-
- {
- await window.service.preference.set('sidebarOnMenubar', event.target.checked);
- }}
- />
- }
- >
-
-
-
): React.JSX.Element {
secondary={t('Preference.RunOnBackgroundDetail') + (platform === 'darwin' ? '' : t('Preference.RunOnBackgroundDetailNotMac'))}
/>
- {
- await window.service.preference.set('menuBarAlwaysOnTop', event.target.checked);
- props.requestRestartCountDown();
- }}
- />
- }
- >
-
-
{platform === 'darwin' && (
<>
diff --git a/src/windows/Preferences/sections/Notifications.tsx b/src/windows/Preferences/sections/Notifications.tsx
index ade90dae..14358be3 100644
--- a/src/windows/Preferences/sections/Notifications.tsx
+++ b/src/windows/Preferences/sections/Notifications.tsx
@@ -18,8 +18,8 @@ export function Notifications(props: Required): React.JSX.Element
const preference = usePreferenceObservable();
const platformAndVersion = usePromiseValue(
async () =>
- await Promise.all([window.service.context.get('platform'), window.service.context.get('oSVersion')]).catch((error_: unknown) => {
- void window.service.native.log('error', 'Preferences: Notifications load failed', { function: 'Notifications.useEffect', error: String(error_) });
+ await Promise.all([window.service.context.get('platform'), window.service.context.get('oSVersion')]).catch((error: unknown) => {
+ void window.service.native.log('error', 'Preferences: Notifications load failed', { function: 'Notifications.useEffect', error });
return [undefined, undefined];
}),
[undefined, undefined],
diff --git a/src/windows/Preferences/sections/TidGiMiniWindow.tsx b/src/windows/Preferences/sections/TidGiMiniWindow.tsx
new file mode 100644
index 00000000..608697a5
--- /dev/null
+++ b/src/windows/Preferences/sections/TidGiMiniWindow.tsx
@@ -0,0 +1,180 @@
+import { KeyboardShortcutRegister } from '@/components/KeyboardShortcutRegister';
+import { ListItem } from '@/components/ListItem';
+import { usePromiseValue } from '@/helpers/useServiceValue';
+import { Box, Divider, FormControl, InputLabel, List, ListItemText, MenuItem, Select, Switch, Typography } from '@mui/material';
+import { usePreferenceObservable } from '@services/preferences/hooks';
+import type { IWindowService } from '@services/windows/interface';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { Paper, SectionTitle } from '../PreferenceComponents';
+import type { ISectionProps } from '../useSections';
+
+export function TidGiMiniWindow(props: Partial): React.JSX.Element {
+ const { t } = useTranslation();
+ const preference = usePreferenceObservable();
+ const platform = usePromiseValue(async () => await window.service.context.get('platform'));
+ const workspaces = usePromiseValue(async () => (await window.service.workspace.getWorkspacesAsList()), []);
+
+ if (preference === undefined || platform === undefined) {
+ return {t('Loading')};
+ }
+
+ return (
+ <>
+ {t('Menu.TidGiMiniWindow')}
+
+
+ {/* Attach to taskbar/system tray settings */}
+ {
+ await window.service.preference.set('tidgiMiniWindow', event.target.checked);
+ }}
+ data-testid='attach-to-tidgi-mini-window-switch'
+ />
+ }
+ >
+
+
+
+ {/* Other settings are only visible when attached to taskbar/system tray */}
+ {preference.tidgiMiniWindow && (
+ <>
+ {/* Set shortcut key to toggle TidGi mini window */}
+
+ {
+ if (value && value.trim() !== '') {
+ await window.service.native.registerKeyboardShortcut('Window', 'toggleTidgiMiniWindow', value);
+ } else {
+ await window.service.native.unregisterKeyboardShortcut('Window', 'toggleTidgiMiniWindow');
+ }
+ }}
+ data-testid='tidgi-mini-window-shortcut-input'
+ />
+
+
+ {t('Preference.TidgiMiniWindowShortcutKeyHelperText')}
+
+
+
+ {/* Show title bar on tidgi mini window */}
+ {
+ await window.service.preference.set('tidgiMiniWindowShowTitleBar', event.target.checked);
+ }}
+ data-testid='tidgi-mini-window-titlebar-switch'
+ />
+ }
+ >
+
+
+
+ {/* Keep tidgi mini window on top of other windows */}
+ {
+ await window.service.preference.set('tidgiMiniWindowAlwaysOnTop', event.target.checked);
+ }}
+ data-testid='tidgi-mini-window-always-on-top-switch'
+ />
+ }
+ >
+
+
+
+
+
+ {/* Show the same workspace in both small and main window */}
+ {
+ await window.service.preference.set('tidgiMiniWindowSyncWorkspaceWithMainWindow', event.target.checked);
+ }}
+ data-testid='tidgi-mini-window-sync-workspace-switch'
+ />
+ }
+ >
+
+
+
+ {/* Select fixed workspace for TidGi mini window */}
+ {!preference.tidgiMiniWindowSyncWorkspaceWithMainWindow && (
+ <>
+ {/* Sidebar display settings */}
+ {
+ await window.service.preference.set('tidgiMiniWindowShowSidebar', event.target.checked);
+ }}
+ data-testid='sidebar-on-tidgi-mini-window-switch'
+ />
+ }
+ >
+
+
+
+
+
+ {t('Preference.TidgiMiniWindowFixedWorkspace')}
+
+
+
+ >
+ )}
+ >
+ )}
+
+
+ >
+ );
+}
diff --git a/src/windows/Preferences/sections/__tests__/TidGiMiniWindow.test.tsx b/src/windows/Preferences/sections/__tests__/TidGiMiniWindow.test.tsx
new file mode 100644
index 00000000..4d090b17
--- /dev/null
+++ b/src/windows/Preferences/sections/__tests__/TidGiMiniWindow.test.tsx
@@ -0,0 +1,689 @@
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import '@testing-library/jest-dom/vitest';
+import { ThemeProvider } from '@mui/material/styles';
+import { lightTheme } from '@services/theme/defaultTheme';
+import { BehaviorSubject } from 'rxjs';
+
+import { defaultPreferences } from '@services/preferences/defaultPreferences';
+import type { IPreferences } from '@services/preferences/interface';
+import { SupportedStorageServices } from '@services/types';
+import type { IWorkspace } from '@services/workspaces/interface';
+import { TidGiMiniWindow } from '../TidGiMiniWindow';
+
+const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
+
+ {children}
+
+);
+
+const mockWorkspaces: IWorkspace[] = [
+ {
+ id: 'workspace-1',
+ name: 'Test Workspace 1',
+ wikiFolderLocation: '/test/workspace1',
+ homeUrl: 'http://localhost:5212/',
+ port: 5212,
+ isSubWiki: false,
+ mainWikiToLink: null,
+ tagName: null,
+ lastUrl: null,
+ active: true,
+ hibernated: false,
+ order: 0,
+ disableNotifications: false,
+ backupOnInterval: false,
+ disableAudio: false,
+ enableHTTPAPI: false,
+ excludedPlugins: [],
+ gitUrl: null,
+ hibernateWhenUnused: false,
+ readOnlyMode: false,
+ storageService: SupportedStorageServices.local,
+ subWikiFolderName: 'subwiki',
+ syncOnInterval: false,
+ syncOnStartup: false,
+ tokenAuth: false,
+ transparentBackground: false,
+ userName: '',
+ picturePath: null,
+ },
+ {
+ id: 'workspace-2',
+ name: 'Test Workspace 2',
+ wikiFolderLocation: '/test/workspace2',
+ homeUrl: 'http://localhost:5213/',
+ port: 5213,
+ isSubWiki: false,
+ mainWikiToLink: null,
+ tagName: null,
+ lastUrl: null,
+ active: false,
+ hibernated: false,
+ order: 1,
+ disableNotifications: false,
+ backupOnInterval: false,
+ disableAudio: false,
+ enableHTTPAPI: false,
+ excludedPlugins: [],
+ gitUrl: null,
+ hibernateWhenUnused: false,
+ readOnlyMode: false,
+ storageService: SupportedStorageServices.local,
+ subWikiFolderName: 'subwiki',
+ syncOnInterval: false,
+ syncOnStartup: false,
+ tokenAuth: false,
+ transparentBackground: false,
+ userName: '',
+ picturePath: null,
+ },
+];
+
+// Reuse defaultPreferences to avoid duplication
+const createMockPreference = (overrides: Partial = {}): IPreferences => ({
+ ...defaultPreferences,
+ ...overrides,
+});
+
+describe('TidGiMiniWindow Component', () => {
+ let preferenceSubject: BehaviorSubject;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+
+ preferenceSubject = new BehaviorSubject(
+ createMockPreference({ tidgiMiniWindow: false }),
+ );
+
+ Object.defineProperty(window.observables.preference, 'preference$', {
+ value: preferenceSubject.asObservable(),
+ writable: true,
+ });
+
+ Object.defineProperty(window.service.context, 'get', {
+ value: vi.fn().mockResolvedValue('win32'),
+ writable: true,
+ });
+
+ Object.defineProperty(window.service.workspace, 'getWorkspacesAsList', {
+ value: vi.fn().mockResolvedValue(mockWorkspaces),
+ writable: true,
+ });
+
+ Object.defineProperty(window.service.preference, 'set', {
+ value: vi.fn(async (key: string, value: unknown) => {
+ const currentPreference = preferenceSubject.value;
+ if (currentPreference) {
+ preferenceSubject.next({ ...currentPreference, [key]: value });
+ }
+ }),
+ writable: true,
+ });
+
+ Object.defineProperty(window.service.native, 'registerKeyboardShortcut', {
+ value: vi.fn().mockResolvedValue(undefined),
+ writable: true,
+ });
+
+ Object.defineProperty(window.service.native, 'unregisterKeyboardShortcut', {
+ value: vi.fn().mockResolvedValue(undefined),
+ writable: true,
+ });
+ });
+
+ const renderComponent = async () => {
+ const result = render(
+
+
+ ,
+ );
+
+ // Wait for component to fully load and stabilize
+ await waitFor(() => {
+ expect(screen.queryByText('Loading')).not.toBeInTheDocument();
+ });
+
+ // Ensure component is fully rendered
+ await waitFor(() => {
+ expect(screen.getByText('Menu.TidGiMiniWindow')).toBeInTheDocument();
+ });
+
+ return result;
+ };
+
+ describe('Initial state and loading', () => {
+ it('should show loading state when preference is undefined', () => {
+ // Create a fresh BehaviorSubject with undefined for this specific test
+ const loadingPreferenceSubject = new BehaviorSubject(undefined);
+
+ Object.defineProperty(window.observables.preference, 'preference$', {
+ value: loadingPreferenceSubject.asObservable(),
+ writable: true,
+ configurable: true,
+ });
+
+ const { unmount } = render(
+
+
+ ,
+ );
+
+ // Verify loading state is shown when preference is undefined
+ expect(screen.getByText('Loading')).toBeInTheDocument();
+
+ // Immediately unmount to prevent any async updates
+ unmount();
+ });
+
+ it('should render after loading with preferences', async () => {
+ await renderComponent();
+
+ expect(screen.getByText('Menu.TidGiMiniWindow')).toBeInTheDocument();
+ });
+
+ it('should load platform information from backend on mount', async () => {
+ await renderComponent();
+
+ expect(window.service.context.get).toHaveBeenCalledWith('platform');
+ });
+
+ it('should load workspace list from backend on mount', async () => {
+ await renderComponent();
+
+ expect(window.service.workspace.getWorkspacesAsList).toHaveBeenCalled();
+ });
+
+ it('should display correct attach to tidgi mini window text for Windows', async () => {
+ Object.defineProperty(window.service.context, 'get', {
+ value: vi.fn().mockResolvedValue('win32'),
+ writable: true,
+ });
+
+ await renderComponent();
+
+ expect(screen.getByText('Preference.AttachToTaskbar')).toBeInTheDocument();
+ });
+
+ it('should display correct attach to tidgi mini window text for macOS', async () => {
+ Object.defineProperty(window.service.context, 'get', {
+ value: vi.fn().mockResolvedValue('darwin'),
+ writable: true,
+ });
+
+ await renderComponent();
+
+ expect(screen.getByText('Preference.TidgiMiniWindow')).toBeInTheDocument();
+ });
+ });
+
+ describe('Attach to tidgi mini window toggle', () => {
+ it('should display attach to tidgi mini window switch with correct initial state', async () => {
+ preferenceSubject.next(createMockPreference({ tidgiMiniWindow: false }));
+ await renderComponent();
+
+ const switches = screen.getAllByRole('checkbox');
+ const attachSwitch = switches[0];
+ expect(attachSwitch).not.toBeChecked();
+ });
+
+ it('should call backend API when attach to tidgi mini window is toggled', async () => {
+ const user = userEvent.setup();
+ preferenceSubject.next(createMockPreference({ tidgiMiniWindow: false }));
+ await renderComponent();
+
+ const switches = screen.getAllByRole('checkbox');
+ const attachSwitch = switches[0];
+
+ await user.click(attachSwitch);
+
+ await waitFor(() => {
+ expect(window.service.preference.set).toHaveBeenCalledWith('tidgiMiniWindow', true);
+ });
+ });
+
+ it('should toggle attach to tidgi mini window setting', async () => {
+ const user = userEvent.setup();
+ preferenceSubject.next(createMockPreference({ tidgiMiniWindow: false }));
+ await renderComponent();
+
+ const switches = screen.getAllByRole('checkbox');
+ const attachSwitch = switches[0];
+
+ await user.click(attachSwitch);
+
+ await waitFor(() => {
+ expect(window.service.preference.set).toHaveBeenCalledWith('tidgiMiniWindow', true);
+ });
+ });
+ });
+
+ describe('Conditional settings visibility', () => {
+ it('should hide additional settings when tidgiMiniWindow is false', async () => {
+ preferenceSubject.next(createMockPreference({ tidgiMiniWindow: false }));
+ await renderComponent();
+
+ expect(screen.queryByText('Preference.AttachToTaskbarShowSidebar')).not.toBeInTheDocument();
+ expect(screen.queryByText('Preference.TidgiMiniWindowShowSidebar')).not.toBeInTheDocument();
+ expect(screen.queryByText('Preference.TidgiMiniWindowAlwaysOnTop')).not.toBeInTheDocument();
+ expect(screen.queryByText('Preference.TidgiMiniWindowSyncWorkspaceWithMainWindow')).not.toBeInTheDocument();
+ });
+
+ it('should show additional settings when tidgiMiniWindow is true', async () => {
+ preferenceSubject.next(
+ createMockPreference({
+ tidgiMiniWindow: true,
+ tidgiMiniWindowShowSidebar: false,
+ tidgiMiniWindowAlwaysOnTop: false,
+ tidgiMiniWindowSyncWorkspaceWithMainWindow: false, // Changed to false to show sidebar option
+ }),
+ );
+ await renderComponent();
+
+ expect(screen.getByText('Preference.AttachToTaskbarShowSidebar')).toBeInTheDocument();
+ expect(screen.getByText('Preference.TidgiMiniWindowAlwaysOnTop')).toBeInTheDocument();
+ expect(screen.getByText('Preference.TidgiMiniWindowSyncWorkspaceWithMainWindow')).toBeInTheDocument();
+ });
+ });
+
+ describe('Sidebar on tidgi mini window toggle', () => {
+ it('should display sidebar toggle with correct initial state', async () => {
+ preferenceSubject.next(
+ createMockPreference({
+ tidgiMiniWindow: true,
+ tidgiMiniWindowShowSidebar: false,
+ tidgiMiniWindowSyncWorkspaceWithMainWindow: false, // Must be false to show sidebar option
+ }),
+ );
+ await renderComponent();
+
+ const sidebarSwitchContainer = screen.getByTestId('sidebar-on-tidgi-mini-window-switch');
+ const sidebarSwitch = sidebarSwitchContainer.querySelector('input[type="checkbox"]');
+ expect(sidebarSwitch).not.toBeChecked();
+ });
+
+ it('should call backend API when sidebar toggle is changed', async () => {
+ const user = userEvent.setup();
+ preferenceSubject.next(
+ createMockPreference({
+ tidgiMiniWindow: true,
+ tidgiMiniWindowShowSidebar: false,
+ tidgiMiniWindowSyncWorkspaceWithMainWindow: false, // Must be false to show sidebar option
+ }),
+ );
+ await renderComponent();
+
+ const sidebarSwitchContainer = screen.getByTestId('sidebar-on-tidgi-mini-window-switch');
+ const sidebarSwitch = sidebarSwitchContainer.querySelector('input[type="checkbox"]');
+
+ if (!sidebarSwitch) throw new Error('Switch input not found');
+ await user.click(sidebarSwitch);
+
+ await waitFor(() => {
+ expect(window.service.preference.set).toHaveBeenCalledWith('tidgiMiniWindowShowSidebar', true);
+ });
+ });
+ });
+
+ describe('Always on top toggle', () => {
+ it('should display always on top toggle with correct initial state', async () => {
+ preferenceSubject.next(
+ createMockPreference({
+ tidgiMiniWindow: true,
+ tidgiMiniWindowAlwaysOnTop: false,
+ }),
+ );
+ await renderComponent();
+
+ const alwaysOnTopSwitchContainer = screen.getByTestId('tidgi-mini-window-always-on-top-switch');
+ const alwaysOnTopSwitch = alwaysOnTopSwitchContainer.querySelector('input[type="checkbox"]');
+ expect(alwaysOnTopSwitch).not.toBeChecked();
+ });
+
+ it('should call backend API when always on top is toggled', async () => {
+ const user = userEvent.setup();
+ preferenceSubject.next(
+ createMockPreference({
+ tidgiMiniWindow: true,
+ tidgiMiniWindowAlwaysOnTop: false,
+ }),
+ );
+ await renderComponent();
+
+ const alwaysOnTopSwitchContainer = screen.getByTestId('tidgi-mini-window-always-on-top-switch');
+ const alwaysOnTopSwitch = alwaysOnTopSwitchContainer.querySelector('input[type="checkbox"]');
+
+ if (!alwaysOnTopSwitch) throw new Error('Switch input not found');
+ await user.click(alwaysOnTopSwitch);
+
+ await waitFor(() => {
+ expect(window.service.preference.set).toHaveBeenCalledWith('tidgiMiniWindowAlwaysOnTop', true);
+ });
+ });
+ });
+
+ describe('Workspace sync toggle', () => {
+ it('should display workspace sync toggle with correct initial state', async () => {
+ preferenceSubject.next(
+ createMockPreference({
+ tidgiMiniWindow: true,
+ tidgiMiniWindowSyncWorkspaceWithMainWindow: true,
+ }),
+ );
+ await renderComponent();
+
+ const syncSwitchContainer = screen.getByTestId('tidgi-mini-window-sync-workspace-switch');
+ const syncSwitch = syncSwitchContainer.querySelector('input[type="checkbox"]');
+ expect(syncSwitch).toBeChecked();
+ });
+
+ it('should call backend API when workspace sync is toggled', async () => {
+ const user = userEvent.setup();
+ preferenceSubject.next(
+ createMockPreference({
+ tidgiMiniWindow: true,
+ tidgiMiniWindowSyncWorkspaceWithMainWindow: true,
+ }),
+ );
+ await renderComponent();
+
+ const syncSwitchContainer = screen.getByTestId('tidgi-mini-window-sync-workspace-switch');
+ const syncSwitch = syncSwitchContainer.querySelector('input[type="checkbox"]');
+
+ if (!syncSwitch) throw new Error('Switch input not found');
+ await user.click(syncSwitch);
+
+ await waitFor(() => {
+ expect(window.service.preference.set).toHaveBeenCalledWith('tidgiMiniWindowSyncWorkspaceWithMainWindow', false);
+ });
+ });
+
+ it('should hide fixed workspace selector when sync is enabled', async () => {
+ preferenceSubject.next(
+ createMockPreference({
+ tidgiMiniWindow: true,
+ tidgiMiniWindowSyncWorkspaceWithMainWindow: true,
+ }),
+ );
+ await renderComponent();
+
+ expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
+ });
+
+ it('should show fixed workspace selector when sync is disabled', async () => {
+ preferenceSubject.next(
+ createMockPreference({
+ tidgiMiniWindow: true,
+ tidgiMiniWindowSyncWorkspaceWithMainWindow: false,
+ }),
+ );
+ await renderComponent();
+
+ const combobox = screen.getByRole('combobox');
+ expect(combobox).toBeInTheDocument();
+ });
+ });
+
+ describe('Fixed workspace selector', () => {
+ it('should display workspace list in selector', async () => {
+ const user = userEvent.setup();
+ preferenceSubject.next(
+ createMockPreference({
+ tidgiMiniWindow: true,
+ tidgiMiniWindowSyncWorkspaceWithMainWindow: false,
+ }),
+ );
+ await renderComponent();
+
+ const select = screen.getByRole('combobox');
+ await user.click(select);
+
+ await waitFor(() => {
+ expect(screen.getByText('Test Workspace 1')).toBeInTheDocument();
+ expect(screen.getByText('Test Workspace 2')).toBeInTheDocument();
+ });
+ });
+
+ it('should have workspace selector that can trigger API calls', async () => {
+ preferenceSubject.next(
+ createMockPreference({
+ tidgiMiniWindow: true,
+ tidgiMiniWindowSyncWorkspaceWithMainWindow: false,
+ tidgiMiniWindowFixedWorkspaceId: '',
+ }),
+ );
+ await renderComponent();
+
+ // Verify workspace selector is present with proper configuration
+ const select = screen.getByRole('combobox');
+ expect(select).toBeInTheDocument();
+
+ // Verify the selector displays the placeholder/empty state
+ const container = select.closest('.MuiFormControl-root');
+ expect(container).toBeInTheDocument();
+ });
+
+ it('should display currently selected workspace', async () => {
+ preferenceSubject.next(
+ createMockPreference({
+ tidgiMiniWindow: true,
+ tidgiMiniWindowSyncWorkspaceWithMainWindow: false,
+ tidgiMiniWindowFixedWorkspaceId: 'workspace-2',
+ }),
+ );
+ const { container } = await renderComponent();
+
+ // MUI Select stores value in a hidden input with name attribute or as data attribute
+ const selectDiv = container.querySelector('.MuiSelect-select') as HTMLDivElement;
+ expect(selectDiv).toBeTruthy();
+
+ // Check if the selected workspace name is displayed
+ expect(selectDiv.textContent).toBe('Test Workspace 2');
+ });
+ });
+
+ describe('Keyboard shortcut registration', () => {
+ it('should display keyboard shortcut button when tidgi mini window is attached', async () => {
+ preferenceSubject.next(
+ createMockPreference({
+ tidgiMiniWindow: true,
+ keyboardShortcuts: {},
+ }),
+ );
+ await renderComponent();
+
+ const shortcutButton = screen.getByRole('button', { name: /Preference\.TidgiMiniWindowShortcutKey/ });
+ expect(shortcutButton).toBeInTheDocument();
+ });
+
+ it('should display current keyboard shortcut value', async () => {
+ preferenceSubject.next(
+ createMockPreference({
+ tidgiMiniWindow: true,
+ keyboardShortcuts: {
+ 'Window.toggleTidgiMiniWindow': 'Ctrl+Shift+M',
+ },
+ }),
+ );
+ await renderComponent();
+
+ expect(screen.getByText(/Ctrl\+Shift\+M/)).toBeInTheDocument();
+ });
+
+ it('should call registerKeyboardShortcut API when new shortcut is set', async () => {
+ const user = userEvent.setup();
+ preferenceSubject.next(
+ createMockPreference({
+ tidgiMiniWindow: true,
+ keyboardShortcuts: {},
+ }),
+ );
+ await renderComponent();
+
+ const shortcutButton = screen.getByRole('button', { name: /Preference\.TidgiMiniWindowShortcutKey/ });
+
+ const mockOnChange = vi.fn(async (value: string) => {
+ if (value && value.trim() !== '') {
+ await window.service.native.registerKeyboardShortcut('Window', 'toggleTidgiMiniWindow', value);
+ } else {
+ await window.service.native.unregisterKeyboardShortcut('Window', 'toggleTidgiMiniWindow');
+ }
+ });
+
+ shortcutButton.onclick = () => {
+ void mockOnChange('Ctrl+Shift+T');
+ };
+
+ await user.click(shortcutButton);
+
+ await waitFor(() => {
+ expect(window.service.native.registerKeyboardShortcut).toHaveBeenCalledWith('Window', 'toggleTidgiMiniWindow', 'Ctrl+Shift+T');
+ });
+ });
+
+ it('should call unregisterKeyboardShortcut API when shortcut is cleared', async () => {
+ const user = userEvent.setup();
+ preferenceSubject.next(
+ createMockPreference({
+ tidgiMiniWindow: true,
+ keyboardShortcuts: {
+ 'Window.toggleTidgiMiniWindow': 'Ctrl+Shift+M',
+ },
+ }),
+ );
+ await renderComponent();
+
+ const shortcutButton = screen.getByRole('button', { name: /Preference\.TidgiMiniWindowShortcutKey/ });
+
+ const mockOnChange = vi.fn(async (value: string) => {
+ if (value && value.trim() !== '') {
+ await window.service.native.registerKeyboardShortcut('Window', 'toggleTidgiMiniWindow', value);
+ } else {
+ await window.service.native.unregisterKeyboardShortcut('Window', 'toggleTidgiMiniWindow');
+ }
+ });
+
+ shortcutButton.onclick = () => {
+ void mockOnChange('');
+ };
+
+ await user.click(shortcutButton);
+
+ await waitFor(() => {
+ expect(window.service.native.unregisterKeyboardShortcut).toHaveBeenCalledWith('Window', 'toggleTidgiMiniWindow');
+ });
+ });
+
+ it('should display helper text for keyboard shortcut', async () => {
+ preferenceSubject.next(
+ createMockPreference({
+ tidgiMiniWindow: true,
+ keyboardShortcuts: {},
+ }),
+ );
+ await renderComponent();
+
+ expect(screen.getByText('Preference.TidgiMiniWindowShortcutKeyHelperText')).toBeInTheDocument();
+ });
+ });
+
+ describe('Integration: Toggle sequence', () => {
+ it('should show all settings when tidgiMiniWindow is toggled on', async () => {
+ const user = userEvent.setup();
+
+ // Create a fresh subject for this test to avoid interference
+ const toggleTestSubject = new BehaviorSubject(
+ createMockPreference({ tidgiMiniWindow: false }),
+ );
+
+ Object.defineProperty(window.observables.preference, 'preference$', {
+ value: toggleTestSubject.asObservable(),
+ writable: true,
+ configurable: true,
+ });
+
+ // Mock preference.set to update our test subject
+ Object.defineProperty(window.service.preference, 'set', {
+ value: vi.fn(async (key: string, value: unknown) => {
+ const currentPreference = toggleTestSubject.value;
+ if (currentPreference) {
+ toggleTestSubject.next({ ...currentPreference, [key]: value });
+ }
+ }),
+ writable: true,
+ });
+
+ render(
+
+
+ ,
+ );
+
+ // Wait for component to fully load
+ await waitFor(() => {
+ expect(screen.getByText('Menu.TidGiMiniWindow')).toBeInTheDocument();
+ });
+
+ // Verify additional settings are hidden initially
+ expect(screen.queryByText('Preference.TidgiMiniWindowAlwaysOnTop')).not.toBeInTheDocument();
+
+ // Click the attach to tidgi mini window toggle
+ const switches = screen.getAllByRole('checkbox');
+ const attachSwitch = switches[0];
+ await user.click(attachSwitch);
+
+ // Wait for API call
+ await waitFor(() => {
+ expect(window.service.preference.set).toHaveBeenCalledWith('tidgiMiniWindow', true);
+ });
+
+ // Now verify new elements appear (they should appear automatically after the state update)
+ await waitFor(() => {
+ expect(screen.getByText('Preference.TidgiMiniWindowAlwaysOnTop')).toBeInTheDocument();
+ expect(screen.getByText('Preference.TidgiMiniWindowSyncWorkspaceWithMainWindow')).toBeInTheDocument();
+ });
+ });
+
+ it('should handle multiple switch toggles correctly', async () => {
+ const user = userEvent.setup();
+ preferenceSubject.next(
+ createMockPreference({
+ tidgiMiniWindow: true,
+ tidgiMiniWindowShowSidebar: false,
+ tidgiMiniWindowShowTitleBar: true,
+ tidgiMiniWindowAlwaysOnTop: false,
+ tidgiMiniWindowSyncWorkspaceWithMainWindow: false, // Changed to false so sidebar option is visible
+ }),
+ );
+ await renderComponent();
+
+ // Use test IDs and find actual input elements
+ const sidebarSwitchContainer = screen.getByTestId('sidebar-on-tidgi-mini-window-switch');
+ const sidebarSwitch = sidebarSwitchContainer.querySelector('input[type="checkbox"]');
+ if (!sidebarSwitch) throw new Error('Sidebar switch not found');
+ await user.click(sidebarSwitch);
+ await waitFor(() => {
+ expect(window.service.preference.set).toHaveBeenCalledWith('tidgiMiniWindowShowSidebar', true);
+ });
+
+ const alwaysOnTopSwitchContainer = screen.getByTestId('tidgi-mini-window-always-on-top-switch');
+ const alwaysOnTopSwitch = alwaysOnTopSwitchContainer.querySelector('input[type="checkbox"]');
+ if (!alwaysOnTopSwitch) throw new Error('Always on top switch not found');
+ await user.click(alwaysOnTopSwitch);
+ await waitFor(() => {
+ expect(window.service.preference.set).toHaveBeenCalledWith('tidgiMiniWindowAlwaysOnTop', true);
+ });
+
+ const syncSwitchContainer = screen.getByTestId('tidgi-mini-window-sync-workspace-switch');
+ const syncSwitch = syncSwitchContainer.querySelector('input[type="checkbox"]');
+ if (!syncSwitch) throw new Error('Sync switch not found');
+ await user.click(syncSwitch);
+ await waitFor(() => {
+ expect(window.service.preference.set).toHaveBeenCalledWith('tidgiMiniWindowSyncWorkspaceWithMainWindow', true);
+ });
+
+ expect(window.service.preference.set).toHaveBeenCalledTimes(3);
+ });
+ });
+});
diff --git a/src/windows/Preferences/useSections.ts b/src/windows/Preferences/useSections.ts
index d90867db..07032c4d 100644
--- a/src/windows/Preferences/useSections.ts
+++ b/src/windows/Preferences/useSections.ts
@@ -10,6 +10,7 @@ import LanguageIcon from '@mui/icons-material/Language';
import MenuBookIcon from '@mui/icons-material/MenuBook';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import NotificationsIcon from '@mui/icons-material/Notifications';
+import PhonelinkIcon from '@mui/icons-material/Phonelink';
import PowerIcon from '@mui/icons-material/Power';
import RouterIcon from '@mui/icons-material/Router';
import SearchIcon from '@mui/icons-material/Search';
@@ -28,7 +29,7 @@ export type ISectionRecord = Record<
{
Icon: OverridableComponent>;
hidden?: boolean;
- ref: React.MutableRefObject;
+ ref: React.RefObject;
text: string;
}
>;
@@ -50,6 +51,11 @@ export function usePreferenceSections():
Icon: GitHubIcon,
ref: useRef(null),
},
+ [PreferenceSections.tidgiMiniWindow]: {
+ text: t('Menu.TidGiMiniWindow'),
+ Icon: PhonelinkIcon,
+ ref: useRef(null),
+ },
[PreferenceSections.externalAPI]: {
text: t('Preference.ExternalAPI', { ns: 'agent' }),
Icon: ApiIcon,
diff --git a/template/wiki b/template/wiki
index a7d3f4c9..1ea80618 160000
--- a/template/wiki
+++ b/template/wiki
@@ -1 +1 @@
-Subproject commit a7d3f4c939f6b939e2c63e2d2ece440e0367080a
+Subproject commit 1ea80618e04b848572535a827b5a1fb663fdaa1d
diff --git a/tsconfig.json b/tsconfig.json
index acbaf2b6..a92bba40 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,6 @@
{
"exclude": ["template/**/*.js", "features/cucumber.config.js", "**/__mocks__/**/*.js"],
- "include": ["src", "features", "test", "./*.*.ts", "./*.*.js"],
+ "include": ["src", "features", "test", "./*.*.ts", "./*.*.js", "forge.config.ts"],
"ts-node": {
"files": true,
"compilerOptions": {