mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2025-12-05 18:20:39 -08:00
Feat/mini window (#642)
* feat: new config for tidgi mini window * chore: upgrade electron-forge * fix: use 汉语 和 漢語 * feat: shortcut to open mini window * test: TidGiMenubarWindow * feat: allow updateWindowProperties on the fly * fix: wrong icon path * fix: log not showing error message and stack * refactor: directly log error when using logger.error * feat: shortcut to open window * fix: menubar not closed * test: e2e for menubar * test: keyboard shortcut * test: wiki web content, and refactor to files * test: update command * Update Testing.md * test: menubar settings about menubarSyncWorkspaceWithMainWindow, menubarFixedWorkspaceId * test: simplify menubar test and cleanup test config * fix: view missing when execute several test all together * refactor: use hook to cleanup menubar setting * refactor: I clear test ai settings to before hook * Add option to show title bar on menubar window Introduces a new preference 'showMenubarWindowTitleBar' allowing users to toggle the title bar visibility on the menubar window. Updates related services, interfaces, and UI components to support this feature, and adds corresponding localization strings for English and Chinese. * refactor: tidgiminiwindow * refactor: preference keys to right order * Refactor window dimension checks to use constants Replaces hardcoded window dimensions with values from windowDimension and WindowNames constants for improved maintainability and consistency in window identification and checks. * I cleanup test wiki * Update defaultPreferences.ts * test: mini window workspace switch * fix: image broken by ai, and lint * fix: can't switch to mini window * refactor: useless todo * Update index.ts * refactor: reuse serialize-error * Update index.ts * Update testKeyboardShortcuts.ts * refactor: dup logic * Update ui.ts * fix: electron-ipc-cat
This commit is contained in:
parent
fa9751e5ea
commit
19ef74a4a6
111 changed files with 4588 additions and 884 deletions
4
.github/instructions/testing.instructions.md
vendored
Normal file
4
.github/instructions/testing.instructions.md
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
applyTo: '**/*.feature,features/**'
|
||||||
|
---
|
||||||
|
Read docs/Testing.md for commands you can use. Don't guess shell commands!
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
|
@ -11,14 +11,14 @@ pnpm test
|
||||||
# Run unit tests only
|
# Run unit tests only
|
||||||
pnpm test:unit
|
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
|
pnpm run test:prepare-e2e
|
||||||
# (When only modify tests in ./features folder, and you have packaged app before, only need to run this.)
|
# (When only modify tests in ./features folder, and you have packaged app before, only need to run this.)
|
||||||
pnpm test:e2e
|
pnpm test:e2e
|
||||||
# Or run a specific e2e test by using same `@xxx` as in the `.feature` file.
|
# Or run a specific e2e test by using same `@xxx` as in the `.feature` file.
|
||||||
pnpm test:e2e --tags="@smoke"
|
pnpm test:e2e --tags="@smoke"
|
||||||
# Or run a single e2e
|
# Or run a single e2e test by `--name`
|
||||||
pnpm test:e2e --name "Wiki-search tool usage"
|
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
|
# Run with coverage
|
||||||
pnpm test:unit -- --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
|
## 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)
|
### Query Priority (use in this order)
|
||||||
|
|
||||||
1. Accessible queries - `getByRole`, `getByLabelText`, `getByPlaceholderText`
|
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
|
- Components with `useEffect` that fetch data on mount
|
||||||
- Async API calls that update component state
|
- Async API calls that update component state
|
||||||
- Timers or intervals that trigger state changes
|
- Timers or intervals that trigger state changes
|
||||||
|
- **RxJS Observable subscriptions** that trigger state updates
|
||||||
|
|
||||||
**Solution**: Wait for async operations to complete using helper functions:
|
**Solution**: Wait for async operations to complete using helper functions:
|
||||||
|
|
||||||
|
|
@ -342,12 +351,62 @@ it('should test feature', async () => {
|
||||||
await renderAsyncComponent();
|
await renderAsyncComponent();
|
||||||
// Now safe to interact without warnings
|
// 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(<Component />);
|
||||||
|
|
||||||
|
// 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.
|
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(<Component />);
|
||||||
|
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(<Component />);
|
||||||
|
expect(screen.getByText('Content')).toBeInTheDocument();
|
||||||
|
// Optional: unmount() if testing transient state
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
### E2E test open production app
|
### E2E test open production app
|
||||||
|
|
||||||
See User profile section above, we need to set `NODE_ENV` as `test` to open with correct profile.
|
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.
|
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`
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ Add your language, make it looks like:
|
||||||
"ja": "日本語",
|
"ja": "日本語",
|
||||||
"ru": "русский",
|
"ru": "русский",
|
||||||
"vi": "Tiếng Việt",
|
"vi": "Tiếng Việt",
|
||||||
"zh-Hans": "汉字"
|
"zh-Hans": "汉语"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,21 @@ Feature: TidGi Default Wiki
|
||||||
I want app auto create a default wiki workspace for me
|
I want app auto create a default wiki workspace for me
|
||||||
So that I can start using wiki immediately
|
So that I can start using wiki immediately
|
||||||
|
|
||||||
@wiki
|
Background:
|
||||||
Scenario: Application has default wiki workspace
|
|
||||||
# Note: tests expect the test wiki parent folder to exist. Run the preparation step before E2E:
|
# 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
|
# 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
|
When I launch the TidGi application
|
||||||
And I wait for the page to load completely
|
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:
|
Then I should see "page body and wiki workspace" elements with selectors:
|
||||||
| body |
|
| body |
|
||||||
| div[data-testid^='workspace-']:has-text('wiki') |
|
| div[data-testid^='workspace-']:has-text('wiki') |
|
||||||
And the window title should contain "太记"
|
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
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ Feature: TidGi Preference
|
||||||
So that I can customize its behavior and improve my experience
|
So that I can customize its behavior and improve my experience
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
Given I clear test ai settings
|
|
||||||
Given I launch the TidGi application
|
Given I launch the TidGi application
|
||||||
And I wait for the page to load completely
|
And I wait for the page to load completely
|
||||||
And I should see a "page body" element with selector "body"
|
And I should see a "page body" element with selector "body"
|
||||||
|
|
|
||||||
|
|
@ -291,9 +291,11 @@ Given('I add test ai settings', function() {
|
||||||
fs.writeJsonSync(settingsPath, { ...existing, aiSettings: newAi } as ISettingFile, { spaces: 2 });
|
fs.writeJsonSync(settingsPath, { ...existing, aiSettings: newAi } as ISettingFile, { spaces: 2 });
|
||||||
});
|
});
|
||||||
|
|
||||||
Given('I clear test ai settings', function() {
|
function clearAISettings() {
|
||||||
if (!fs.existsSync(settingsPath)) return;
|
if (!fs.existsSync(settingsPath)) return;
|
||||||
const parsed = fs.readJsonSync(settingsPath) as ISettingFile;
|
const parsed = fs.readJsonSync(settingsPath) as ISettingFile;
|
||||||
const cleaned = omit(parsed, ['aiSettings']);
|
const cleaned = omit(parsed, ['aiSettings']);
|
||||||
fs.writeJsonSync(settingsPath, cleaned, { spaces: 2 });
|
fs.writeJsonSync(settingsPath, cleaned, { spaces: 2 });
|
||||||
});
|
}
|
||||||
|
|
||||||
|
export { clearAISettings };
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,30 @@ import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { _electron as electron } from 'playwright';
|
import { _electron as electron } from 'playwright';
|
||||||
import type { ElectronApplication, Page } 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 { MockOpenAIServer } from '../supports/mockOpenAI';
|
||||||
import { logsDirectory, makeSlugPath, screenshotsDirectory } from '../supports/paths';
|
import { logsDirectory, makeSlugPath, screenshotsDirectory } from '../supports/paths';
|
||||||
import { getPackedAppPath } 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<string, WindowNames>)[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 {
|
export class ApplicationWorld {
|
||||||
app: ElectronApplication | undefined;
|
app: ElectronApplication | undefined;
|
||||||
|
|
@ -14,43 +34,141 @@ export class ApplicationWorld {
|
||||||
currentWindow: Page | undefined; // New state-managed current window
|
currentWindow: Page | undefined; // New state-managed current window
|
||||||
mockOpenAIServer: MockOpenAIServer | undefined;
|
mockOpenAIServer: MockOpenAIServer | undefined;
|
||||||
|
|
||||||
|
// Helper method to check if window is visible
|
||||||
|
async isWindowVisible(page: Page): Promise<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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<Page | undefined> {
|
||||||
|
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<Page | undefined> {
|
async getWindow(windowType: string = 'main'): Promise<Page | undefined> {
|
||||||
if (!this.app) return undefined;
|
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++) {
|
for (let attempt = 0; attempt < 3; attempt++) {
|
||||||
const pages = this.app.windows();
|
try {
|
||||||
|
const window = await this.findWindowByType(windowType);
|
||||||
const extractFragment = (url: string) => {
|
if (window) return window;
|
||||||
if (!url) return '';
|
} catch (error) {
|
||||||
const afterHash = url.includes('#') ? url.split('#').slice(1).join('#') : '';
|
// If it's an invalid window type error, throw immediately
|
||||||
// remove leading slashes or colons like '/preferences' or ':Index'
|
if (error instanceof Error && error.message.includes('is not a valid WindowNames')) {
|
||||||
return afterHash.replace(/^[:/]+/, '').split(/[/?#]/)[0] || '';
|
throw error;
|
||||||
};
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
if (attempt < 2) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
}
|
}
|
||||||
|
|
@ -64,7 +182,7 @@ setWorldConstructor(ApplicationWorld);
|
||||||
|
|
||||||
// setDefaultTimeout(50000);
|
// setDefaultTimeout(50000);
|
||||||
|
|
||||||
Before(function(this: ApplicationWorld) {
|
Before(function(this: ApplicationWorld, { pickle }) {
|
||||||
// Create necessary directories under userData-test/logs to match appPaths in dev/test
|
// Create necessary directories under userData-test/logs to match appPaths in dev/test
|
||||||
if (!fs.existsSync(logsDirectory)) {
|
if (!fs.existsSync(logsDirectory)) {
|
||||||
fs.mkdirSync(logsDirectory, { recursive: true });
|
fs.mkdirSync(logsDirectory, { recursive: true });
|
||||||
|
|
@ -74,11 +192,29 @@ Before(function(this: ApplicationWorld) {
|
||||||
if (!fs.existsSync(screenshotsDirectory)) {
|
if (!fs.existsSync(screenshotsDirectory)) {
|
||||||
fs.mkdirSync(screenshotsDirectory, { recursive: true });
|
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) {
|
if (this.app) {
|
||||||
try {
|
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();
|
await this.app.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during cleanup:', error);
|
console.error('Error during cleanup:', error);
|
||||||
|
|
@ -87,6 +223,12 @@ After(async function(this: ApplicationWorld) {
|
||||||
this.mainWindow = undefined;
|
this.mainWindow = undefined;
|
||||||
this.currentWindow = 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 }) {
|
AfterStep(async function(this: ApplicationWorld, { pickle, pickleStep, result }) {
|
||||||
|
|
@ -125,7 +267,7 @@ AfterStep(async function(this: ApplicationWorld, { pickle, pickleStep, result })
|
||||||
/**
|
/**
|
||||||
* Typical steps like:
|
* Typical steps like:
|
||||||
* - I add test ai settings
|
* - 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
|
* - I clear test ai settings
|
||||||
*/
|
*/
|
||||||
if (!pageToUse || pageToUse.isClosed()) {
|
if (!pageToUse || pageToUse.isClosed()) {
|
||||||
|
|
@ -208,7 +350,7 @@ When('I launch the TidGi application', async function(this: ApplicationWorld) {
|
||||||
this.currentWindow = this.mainWindow;
|
this.currentWindow = this.mainWindow;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new 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.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
103
features/stepDefinitions/browserView.ts
Normal file
103
features/stepDefinitions/browserView.ts
Normal file
|
|
@ -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`);
|
||||||
|
});
|
||||||
74
features/stepDefinitions/tidgiMiniWindow.ts
Normal file
74
features/stepDefinitions/tidgiMiniWindow.ts
Normal file
|
|
@ -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 };
|
||||||
|
|
@ -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) {
|
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 });
|
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) {
|
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 {
|
try {
|
||||||
await currentWindow?.waitForSelector(selector, { timeout: 10000 });
|
await currentWindow?.waitForSelector(selector, { timeout: 10000 });
|
||||||
const isVisible = await currentWindow?.isVisible(selector);
|
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) {
|
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) {
|
if (!currentWindow) {
|
||||||
throw new Error('No current window is available');
|
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) {
|
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) {
|
if (!currentWindow) {
|
||||||
throw new Error('No current window is available');
|
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) {
|
if (count > 0) {
|
||||||
const isVisible = await element.isVisible();
|
const isVisible = await element.isVisible();
|
||||||
if (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
|
// 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) {
|
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');
|
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) {
|
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');
|
if (!win) throw new Error('No active window available to click elements');
|
||||||
|
|
||||||
const locator = win.locator(selector);
|
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) {
|
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) {
|
if (!currentWindow) {
|
||||||
throw new Error('No current window is available');
|
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) {
|
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) {
|
if (!currentWindow) {
|
||||||
throw new Error('No current window is available');
|
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) {
|
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) {
|
if (!currentWindow) {
|
||||||
throw new Error('No current window is available');
|
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`);
|
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)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import fs from 'fs-extra';
|
||||||
import type { IWorkspace } from '../../src/services/workspaces/interface';
|
import type { IWorkspace } from '../../src/services/workspaces/interface';
|
||||||
import { settingsPath, wikiTestWikiPath } from '../supports/paths';
|
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);
|
if (fs.existsSync(wikiTestWikiPath)) fs.removeSync(wikiTestWikiPath);
|
||||||
|
|
||||||
type SettingsFile = { workspaces?: Record<string, IWorkspace> } & Record<string, unknown>;
|
type SettingsFile = { workspaces?: Record<string, IWorkspace> } & Record<string, unknown>;
|
||||||
|
|
|
||||||
207
features/stepDefinitions/window.ts
Normal file
207
features/stepDefinitions/window.ts
Normal file
|
|
@ -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}}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -40,7 +40,7 @@ export function getPackedAppPath(): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(
|
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.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
151
features/supports/webContentsViewHelper.ts
Normal file
151
features/supports/webContentsViewHelper.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
import type { ElectronApplication } from 'playwright';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get text content from WebContentsView
|
||||||
|
* @param app Electron application instance
|
||||||
|
* @returns Promise<string | null> Returns text content or null
|
||||||
|
*/
|
||||||
|
export async function getTextContent(app: ElectronApplication): Promise<string | null> {
|
||||||
|
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<string> } };
|
||||||
|
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<string | null> Returns DOM content or null
|
||||||
|
*/
|
||||||
|
export async function getDOMContent(app: ElectronApplication): Promise<string | null> {
|
||||||
|
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<string> } };
|
||||||
|
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<boolean> Returns whether it exists and is loaded
|
||||||
|
*/
|
||||||
|
export async function isLoaded(app: ElectronApplication): Promise<boolean> {
|
||||||
|
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<boolean> Returns whether text was found
|
||||||
|
*/
|
||||||
|
export async function containsText(
|
||||||
|
app: ElectronApplication,
|
||||||
|
expectedText: string,
|
||||||
|
contentType: 'text' | 'dom' = 'text',
|
||||||
|
): Promise<boolean> {
|
||||||
|
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<string> Returns content summary
|
||||||
|
*/
|
||||||
|
export async function getContentSummary(
|
||||||
|
app: ElectronApplication,
|
||||||
|
contentType: 'text' | 'dom' = 'text',
|
||||||
|
maxLength: number = 200,
|
||||||
|
): Promise<string> {
|
||||||
|
const content = contentType === 'text'
|
||||||
|
? await getTextContent(app)
|
||||||
|
: await getDOMContent(app);
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
return 'null';
|
||||||
|
}
|
||||||
|
|
||||||
|
return content.length > maxLength
|
||||||
|
? content.substring(0, maxLength) + '...'
|
||||||
|
: content;
|
||||||
|
}
|
||||||
38
features/tidgiMiniWindow.feature
Normal file
38
features/tidgiMiniWindow.feature
Normal file
|
|
@ -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
|
||||||
|
|
||||||
130
features/tidgiMiniWindowWorkspace.feature
Normal file
130
features/tidgiMiniWindowWorkspace.feature
Normal file
|
|
@ -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'] |
|
||||||
|
|
@ -32,7 +32,7 @@ const config: ForgeConfig = {
|
||||||
// Unpack worker files, native modules path, and ALL .node binaries (including better-sqlite3)
|
// Unpack worker files, native modules path, and ALL .node binaries (including better-sqlite3)
|
||||||
unpack: '{**/.webpack/main/*.worker.*,**/.webpack/main/native_modules/path.txt,**/{.**,**}/**/*.node}',
|
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
|
// @ts-expect-error - mac config is valid
|
||||||
mac: {
|
mac: {
|
||||||
category: 'productivity',
|
category: 'productivity',
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
"OpenCommandPalette": "Open CommandPalette",
|
"OpenCommandPalette": "Open CommandPalette",
|
||||||
"OpenLinkInBrowser": "Open Link in Browser",
|
"OpenLinkInBrowser": "Open Link in Browser",
|
||||||
"OpenTidGi": "Open TidGi",
|
"OpenTidGi": "Open TidGi",
|
||||||
"OpenTidGiMenuBar": "Open TidGi MenuBar",
|
"OpenTidGiMiniWindow": "Open TidGi Mini Window",
|
||||||
"OpenWorkspaceInNewWindow": "Open Workspace in New Window",
|
"OpenWorkspaceInNewWindow": "Open Workspace in New Window",
|
||||||
"Paste": "Paste",
|
"Paste": "Paste",
|
||||||
"Preferences": "Preferences...",
|
"Preferences": "Preferences...",
|
||||||
|
|
@ -309,7 +309,7 @@
|
||||||
"SelectNextWorkspace": "Select Next Workspace",
|
"SelectNextWorkspace": "Select Next Workspace",
|
||||||
"SelectPreviousWorkspace": "Select Previous Workspace",
|
"SelectPreviousWorkspace": "Select Previous Workspace",
|
||||||
"TidGi": "TidGi",
|
"TidGi": "TidGi",
|
||||||
"TidGiMenuBar": "TidGi MenuBar",
|
"TidGiMiniWindow": "TidGi Mini Window",
|
||||||
"View": "View",
|
"View": "View",
|
||||||
"Wiki": "Wiki",
|
"Wiki": "Wiki",
|
||||||
"Window": "Window",
|
"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",
|
"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.",
|
"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",
|
"AskDownloadLocation": "Ask where to save each file before downloading",
|
||||||
"AttachToMenuBar": "Attach to menu bar",
|
"TidgiMiniWindow": "Attach to TidGi mini window",
|
||||||
"AttachToMenuBarShowSidebar": "Attach To Menu Bar Show Sidebar",
|
"TidgiMiniWindowShowSidebar": "Attach To TidGi Mini Window 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.",
|
"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.",
|
||||||
"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.",
|
"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",
|
"AttachToTaskbar": "Attach to taskbar",
|
||||||
"AttachToTaskbarShowSidebar": "Attach To Taskbar Show Sidebar",
|
"AttachToTaskbarShowSidebar": "Attach To Taskbar Show Sidebar",
|
||||||
"ChooseLanguage": "Choose Language 选择语言",
|
"ChooseLanguage": "Choose Language 选择语言",
|
||||||
|
|
@ -362,8 +362,16 @@
|
||||||
"ItIsWorking": "It is working!",
|
"ItIsWorking": "It is working!",
|
||||||
"Languages": "Lang/语言",
|
"Languages": "Lang/语言",
|
||||||
"LightTheme": "Light Theme",
|
"LightTheme": "Light Theme",
|
||||||
"MenubarAlwaysOnTop": "Menubar Always on top",
|
"TidgiMiniWindowAlwaysOnTop": "TidGi Mini Window Always on top",
|
||||||
"MenubarAlwaysOnTopDetail": "Keep TidGi’s Menubar always on top of other windows, and will not be covered by other windows",
|
"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",
|
"Miscellaneous": "Miscellaneous",
|
||||||
"MoreWorkspaceSyncSettings": "More Workspace Sync Settings",
|
"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.",
|
"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 → TidGi</1>.</0>",
|
"TestNotificationDescription": "<0>If notifications dont show up, make sure you enable notifications in<1>macOS Preferences → Notifications → TidGi</1>.</0>",
|
||||||
"Theme": "Theme",
|
"Theme": "Theme",
|
||||||
"TiddlyWiki": "TiddlyWiki",
|
"TiddlyWiki": "TiddlyWiki",
|
||||||
"ToggleMenuBar": "Toggle Menu Bar",
|
"ToggleTidgiMiniWindow": "Toggle TidGi Mini Window",
|
||||||
"Token": "Git credentials",
|
"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.",
|
"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",
|
"Translatium": "Translatium",
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
"OpenCommandPalette": "Ouvrir la palette de commandes",
|
"OpenCommandPalette": "Ouvrir la palette de commandes",
|
||||||
"OpenLinkInBrowser": "Ouvrir le lien dans le navigateur",
|
"OpenLinkInBrowser": "Ouvrir le lien dans le navigateur",
|
||||||
"OpenTidGi": "Ouvrir TidGi",
|
"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",
|
"OpenWorkspaceInNewWindow": "Ouvrir l'espace de travail dans une nouvelle fenêtre",
|
||||||
"Paste": "Coller",
|
"Paste": "Coller",
|
||||||
"Preferences": "Préférences...",
|
"Preferences": "Préférences...",
|
||||||
|
|
@ -309,7 +309,7 @@
|
||||||
"SelectNextWorkspace": "Sélectionner l'espace de travail suivant",
|
"SelectNextWorkspace": "Sélectionner l'espace de travail suivant",
|
||||||
"SelectPreviousWorkspace": "Sélectionner l'espace de travail précédent",
|
"SelectPreviousWorkspace": "Sélectionner l'espace de travail précédent",
|
||||||
"TidGi": "TidGi",
|
"TidGi": "TidGi",
|
||||||
"TidGiMenuBar": "Barre de menu TidGi",
|
"TidGiMiniWindow": "Mini-fenêtre TidGi",
|
||||||
"View": "Vue",
|
"View": "Vue",
|
||||||
"Wiki": "Wiki",
|
"Wiki": "Wiki",
|
||||||
"Window": "Fenêtre",
|
"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",
|
"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.",
|
"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",
|
"AskDownloadLocation": "Demander où enregistrer chaque fichier avant de télécharger",
|
||||||
"AttachToMenuBar": "Attacher à la barre de menu",
|
"TidgiMiniWindow": "Attacher à la mini-fenêtre TidGi",
|
||||||
"AttachToMenuBarShowSidebar": "Attacher à la barre de menu Afficher la barre latérale",
|
"TidgiMiniWindowShowSidebar": "Attacher à la mini-fenêtre TidGi 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.",
|
"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.",
|
||||||
"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.",
|
"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",
|
"AttachToTaskbar": "Attacher à la barre des tâches",
|
||||||
"AttachToTaskbarShowSidebar": "Attacher à la barre des tâches Afficher la barre latérale",
|
"AttachToTaskbarShowSidebar": "Attacher à la barre des tâches Afficher la barre latérale",
|
||||||
"ChooseLanguage": "Choisir la langue 选择语言",
|
"ChooseLanguage": "Choisir la langue 选择语言",
|
||||||
|
|
@ -349,8 +349,8 @@
|
||||||
"General": "UI & Interact",
|
"General": "UI & Interact",
|
||||||
"HibernateAllUnusedWorkspaces": "Mettre en veille les espaces de travail inutilisés au lancement de l'application",
|
"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.",
|
"HibernateAllUnusedWorkspacesDescription": "Mettre en veille tous les espaces de travail au lancement, sauf le dernier espace de travail actif.",
|
||||||
"HideMenuBar": "Masquer la barre de menu",
|
"HideTidgiMiniWindow": "Masquer la mini-fenêtre TidGi",
|
||||||
"HideMenuBarDetail": "Masquer la barre de menu sauf si Alt+M est pressé.",
|
"HideTidgiMiniWindowDetail": "Masquer la mini-fenêtre TidGi sauf si Alt+M est pressé.",
|
||||||
"HideSideBar": "Masquer la barre latérale",
|
"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",
|
"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",
|
"HideTitleBar": "Masquer la barre de titre",
|
||||||
|
|
@ -360,8 +360,8 @@
|
||||||
"ItIsWorking": "Ça fonctionne !",
|
"ItIsWorking": "Ça fonctionne !",
|
||||||
"Languages": "Lang/语言",
|
"Languages": "Lang/语言",
|
||||||
"LightTheme": "Thème clair",
|
"LightTheme": "Thème clair",
|
||||||
"MenubarAlwaysOnTop": "Barre de menu toujours au-dessus",
|
"TidgiMiniWindowAlwaysOnTop": "TidGi Mini Window 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",
|
"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",
|
"Miscellaneous": "Divers",
|
||||||
"MoreWorkspaceSyncSettings": "Plus de paramètres de synchronisation de l'espace de travail",
|
"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.",
|
"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 → TidGi</1>.</0>",
|
"TestNotificationDescription": "<0>Si les notifications ne s'affichent pas, assurez-vous d'activer les notifications dans<1>Préférences macOS → Notifications → TidGi</1>.</0>",
|
||||||
"Theme": "Thème",
|
"Theme": "Thème",
|
||||||
"TiddlyWiki": "TiddlyWiki",
|
"TiddlyWiki": "TiddlyWiki",
|
||||||
"ToggleMenuBar": "Basculer la barre de menu",
|
"ToggleTidgiMiniWindow": "Basculer la mini-fenêtre TidGi",
|
||||||
"Token": "Informations d'identification Git",
|
"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.",
|
"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",
|
"Translatium": "Translatium",
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
"OpenCommandPalette": "コマンドパレットを開く",
|
"OpenCommandPalette": "コマンドパレットを開く",
|
||||||
"OpenLinkInBrowser": "ブラウザでリンクを開く",
|
"OpenLinkInBrowser": "ブラウザでリンクを開く",
|
||||||
"OpenTidGi": "TidGiを開く",
|
"OpenTidGi": "TidGiを開く",
|
||||||
"OpenTidGiMenuBar": "TidGiメニューバーを開く",
|
"OpenTidGiMiniWindow": "TidGiミニウィンドウを開く",
|
||||||
"OpenWorkspaceInNewWindow": "ワークスペースを新しいウィンドウで開く",
|
"OpenWorkspaceInNewWindow": "ワークスペースを新しいウィンドウで開く",
|
||||||
"Paste": "貼り付け",
|
"Paste": "貼り付け",
|
||||||
"Preferences": "設定...",
|
"Preferences": "設定...",
|
||||||
|
|
@ -359,8 +359,8 @@
|
||||||
"ItIsWorking": "使いやすい!",
|
"ItIsWorking": "使いやすい!",
|
||||||
"Languages": "言語/ランゲージ",
|
"Languages": "言語/ランゲージ",
|
||||||
"LightTheme": "明るい色のテーマ",
|
"LightTheme": "明るい色のテーマ",
|
||||||
"MenubarAlwaysOnTop": "メニューバーの小ウィンドウを他のウィンドウの上に保持する",
|
"TidgiMiniWindowAlwaysOnTop": "太記小ウィンドウを他のウィンドウの上に保持する",
|
||||||
"MenubarAlwaysOnTopDetail": "太記のメニューバーウィンドウを常に他のウィンドウの上に表示させ、他のウィンドウで覆われないようにします。",
|
"TidgiMiniWindowAlwaysOnTopDetail": "太記の小ウィンドウを常に他のウィンドウの上に表示させ、他のウィンドウで覆われないようにします。",
|
||||||
"Miscellaneous": "その他の設定",
|
"Miscellaneous": "その他の設定",
|
||||||
"MoreWorkspaceSyncSettings": "さらに多くのワークスペース同期設定",
|
"MoreWorkspaceSyncSettings": "さらに多くのワークスペース同期設定",
|
||||||
"MoreWorkspaceSyncSettingsDescription": "ワークスペースアイコンを右クリックし、右クリックメニューから「ワークスペースの編集」を選択して、ワークスペース設定を開いてください。そこで各ワークスペースの同期設定を行います。",
|
"MoreWorkspaceSyncSettingsDescription": "ワークスペースアイコンを右クリックし、右クリックメニューから「ワークスペースの編集」を選択して、ワークスペース設定を開いてください。そこで各ワークスペースの同期設定を行います。",
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
"OpenCommandPalette": "Открыть палитру команд",
|
"OpenCommandPalette": "Открыть палитру команд",
|
||||||
"OpenLinkInBrowser": "Открыть ссылку в браузере",
|
"OpenLinkInBrowser": "Открыть ссылку в браузере",
|
||||||
"OpenTidGi": "Открыть TidGi",
|
"OpenTidGi": "Открыть TidGi",
|
||||||
"OpenTidGiMenuBar": "Открыть меню TidGi",
|
"OpenTidGiMiniWindow": "Открыть мини-окно TidGi",
|
||||||
"OpenWorkspaceInNewWindow": "Открыть рабочее пространство в новом окне",
|
"OpenWorkspaceInNewWindow": "Открыть рабочее пространство в новом окне",
|
||||||
"Paste": "Вставить",
|
"Paste": "Вставить",
|
||||||
"Preferences": "Настройки...",
|
"Preferences": "Настройки...",
|
||||||
|
|
@ -309,7 +309,7 @@
|
||||||
"SelectNextWorkspace": "Выбрать следующее рабочее пространство",
|
"SelectNextWorkspace": "Выбрать следующее рабочее пространство",
|
||||||
"SelectPreviousWorkspace": "Выбрать предыдущее рабочее пространство",
|
"SelectPreviousWorkspace": "Выбрать предыдущее рабочее пространство",
|
||||||
"TidGi": "TidGi",
|
"TidGi": "TidGi",
|
||||||
"TidGiMenuBar": "Слишком помню маленькое окно.",
|
"TidGiMiniWindow": "Мини-окно TidGi",
|
||||||
"View": "Просмотр",
|
"View": "Просмотр",
|
||||||
"Wiki": "Wiki",
|
"Wiki": "Wiki",
|
||||||
"Window": "Окно",
|
"Window": "Окно",
|
||||||
|
|
@ -359,8 +359,8 @@
|
||||||
"ItIsWorking": "Работает!",
|
"ItIsWorking": "Работает!",
|
||||||
"Languages": "Языки",
|
"Languages": "Языки",
|
||||||
"LightTheme": "Светлая тема",
|
"LightTheme": "Светлая тема",
|
||||||
"MenubarAlwaysOnTop": "Меню всегда сверху",
|
"TidgiMiniWindowAlwaysOnTop": "TidGi мини-окно всегда сверху",
|
||||||
"MenubarAlwaysOnTopDetail": "Детали меню всегда сверху",
|
"TidgiMiniWindowAlwaysOnTopDetail": "Держать мини-окно TidGi всегда поверх других окон",
|
||||||
"Miscellaneous": "Разное",
|
"Miscellaneous": "Разное",
|
||||||
"MoreWorkspaceSyncSettings": "Дополнительные настройки синхронизации рабочего пространства",
|
"MoreWorkspaceSyncSettings": "Дополнительные настройки синхронизации рабочего пространства",
|
||||||
"MoreWorkspaceSyncSettingsDescription": "Описание дополнительных настроек синхронизации рабочего пространства",
|
"MoreWorkspaceSyncSettingsDescription": "Описание дополнительных настроек синхронизации рабочего пространства",
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
"OpenCommandPalette": "打开搜索与命令面板",
|
"OpenCommandPalette": "打开搜索与命令面板",
|
||||||
"OpenLinkInBrowser": "在浏览器中打开链接",
|
"OpenLinkInBrowser": "在浏览器中打开链接",
|
||||||
"OpenTidGi": "打开太记",
|
"OpenTidGi": "打开太记",
|
||||||
"OpenTidGiMenuBar": "打开太记小窗口",
|
"OpenTidGiMiniWindow": "打开太记小窗口",
|
||||||
"OpenWorkspaceInNewWindow": "在新窗口中打开工作区",
|
"OpenWorkspaceInNewWindow": "在新窗口中打开工作区",
|
||||||
"Paste": "粘贴",
|
"Paste": "粘贴",
|
||||||
"Preferences": "设置...",
|
"Preferences": "设置...",
|
||||||
|
|
@ -177,6 +177,7 @@
|
||||||
"SyncOnIntervalDescription": "开启后会根据全局设置里的时间间隔自动同步,并且依然会在启动时自动同步,点击按钮也可以手动同步。同步云端前会自动先把数据备份到本地Git。如果关闭,则只有在应用程序打开时会有一次自动同步,还有当用户通过点击知识库中的同步按钮手动触发同步。",
|
"SyncOnIntervalDescription": "开启后会根据全局设置里的时间间隔自动同步,并且依然会在启动时自动同步,点击按钮也可以手动同步。同步云端前会自动先把数据备份到本地Git。如果关闭,则只有在应用程序打开时会有一次自动同步,还有当用户通过点击知识库中的同步按钮手动触发同步。",
|
||||||
"SyncOnStartup": "启动时自动同步",
|
"SyncOnStartup": "启动时自动同步",
|
||||||
"SyncOnStartupDescription": "在应用冷启动时自动同步一次。",
|
"SyncOnStartupDescription": "在应用冷启动时自动同步一次。",
|
||||||
|
"TiddlyWiki": "太微",
|
||||||
"TokenAuth": "凭证鉴权",
|
"TokenAuth": "凭证鉴权",
|
||||||
"TokenAuthAutoFillUserNameDescription": "此功能需要在全局设置或工作区设置里填写用户名,不然不会生效。若你未填,将自动在工作区设置里填一个默认值,你可自行修改。",
|
"TokenAuthAutoFillUserNameDescription": "此功能需要在全局设置或工作区设置里填写用户名,不然不会生效。若你未填,将自动在工作区设置里填一个默认值,你可自行修改。",
|
||||||
"TokenAuthCurrentHeader": "凭证鉴权当前请求头",
|
"TokenAuthCurrentHeader": "凭证鉴权当前请求头",
|
||||||
|
|
@ -188,7 +189,6 @@
|
||||||
"UploadOrSelectPathDescription": "点击上传按钮将文件提交给太记保管,也可以点击选择路径按钮从你保管的位置选取文件。",
|
"UploadOrSelectPathDescription": "点击上传按钮将文件提交给太记保管,也可以点击选择路径按钮从你保管的位置选取文件。",
|
||||||
"WikiRootTiddler": "知识库根条目",
|
"WikiRootTiddler": "知识库根条目",
|
||||||
"WikiRootTiddlerDescription": "知识库的根条目(root-tiddler)决定了系统的核心行为,修改前请阅读官方文档来了解",
|
"WikiRootTiddlerDescription": "知识库的根条目(root-tiddler)决定了系统的核心行为,修改前请阅读官方文档来了解",
|
||||||
"TiddlyWiki": "太微",
|
|
||||||
"WikiRootTiddlerItems": {
|
"WikiRootTiddlerItems": {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -234,6 +234,14 @@
|
||||||
"Tags": {
|
"Tags": {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"KeyboardShortcut": {
|
||||||
|
"Clear": "清除",
|
||||||
|
"HelpText": "按下任意键组合(如 Ctrl+Shift+A)。单独的修饰键将被忽略。",
|
||||||
|
"None": "无",
|
||||||
|
"PressKeys": "请按键...",
|
||||||
|
"PressKeysPrompt": "请为{{feature}}按下快捷键组合",
|
||||||
|
"RegisterShortcut": "注册快捷键"
|
||||||
|
},
|
||||||
"LOG": {
|
"LOG": {
|
||||||
"CommitBackupMessage": "使用太记桌面版备份",
|
"CommitBackupMessage": "使用太记桌面版备份",
|
||||||
"CommitMessage": "使用太记桌面版同步"
|
"CommitMessage": "使用太记桌面版同步"
|
||||||
|
|
@ -309,7 +317,7 @@
|
||||||
"SelectNextWorkspace": "选择下一个工作区",
|
"SelectNextWorkspace": "选择下一个工作区",
|
||||||
"SelectPreviousWorkspace": "选择前一个工作区",
|
"SelectPreviousWorkspace": "选择前一个工作区",
|
||||||
"TidGi": "太记",
|
"TidGi": "太记",
|
||||||
"TidGiMenuBar": "太记小窗",
|
"TidGiMiniWindow": "太记小窗",
|
||||||
"View": "查看",
|
"View": "查看",
|
||||||
"Wiki": "知识库",
|
"Wiki": "知识库",
|
||||||
"Window": "窗口",
|
"Window": "窗口",
|
||||||
|
|
@ -324,10 +332,10 @@
|
||||||
"AlwaysOnTopDetail": "让太记的主窗口永远保持在其它窗口上方,不会被其他窗口覆盖",
|
"AlwaysOnTopDetail": "让太记的主窗口永远保持在其它窗口上方,不会被其他窗口覆盖",
|
||||||
"AntiAntiLeech": "有的网站做了防盗链,会阻止某些图片在你的知识库上显示,我们通过模拟访问该网站的请求头来绕过这种限制。",
|
"AntiAntiLeech": "有的网站做了防盗链,会阻止某些图片在你的知识库上显示,我们通过模拟访问该网站的请求头来绕过这种限制。",
|
||||||
"AskDownloadLocation": "下载前询问每个文件的保存位置",
|
"AskDownloadLocation": "下载前询问每个文件的保存位置",
|
||||||
"AttachToMenuBar": "附加到菜单栏",
|
"TidgiMiniWindow": "附加到太记小窗",
|
||||||
"AttachToMenuBarShowSidebar": "附加到菜单栏的窗口包含侧边栏",
|
"TidgiMiniWindowShowSidebar": "太记小窗包含侧边栏",
|
||||||
"AttachToMenuBarShowSidebarTip": "一般太记小窗仅用于快速查看当前工作区,所以默认与主窗口工作区同步,不需要侧边栏,默认隐藏侧边栏。",
|
"TidgiMiniWindowShowSidebarTip": "一般太记小窗仅用于快速查看当前工作区,所以默认与主窗口工作区同步,不需要侧边栏,默认隐藏侧边栏。",
|
||||||
"AttachToMenuBarTip": "创建一个点击菜单栏/任务栏图标会弹出的小太记窗口。提示:右键单击小图标以访问上下文菜单。",
|
"TidgiMiniWindowTip": "创建一个点击系统托盘图标会弹出的小太记窗口。提示:右键单击小图标以访问上下文菜单。",
|
||||||
"AttachToTaskbar": "附加到任务栏",
|
"AttachToTaskbar": "附加到任务栏",
|
||||||
"AttachToTaskbarShowSidebar": "附加到任务栏的窗口包含侧边栏",
|
"AttachToTaskbarShowSidebar": "附加到任务栏的窗口包含侧边栏",
|
||||||
"ChooseLanguage": "选择语言 Choose Language",
|
"ChooseLanguage": "选择语言 Choose Language",
|
||||||
|
|
@ -363,8 +371,16 @@
|
||||||
"ItIsWorking": "好使的!",
|
"ItIsWorking": "好使的!",
|
||||||
"Languages": "语言/Lang",
|
"Languages": "语言/Lang",
|
||||||
"LightTheme": "亮色主题",
|
"LightTheme": "亮色主题",
|
||||||
"MenubarAlwaysOnTop": "保持菜单栏小窗口在其他窗口上方",
|
"TidgiMiniWindowAlwaysOnTop": "保持太记小窗口在其他窗口上方",
|
||||||
"MenubarAlwaysOnTopDetail": "让太记的菜单栏小窗口永远保持在其它窗口上方,不会被其他窗口覆盖",
|
"TidgiMiniWindowAlwaysOnTopDetail": "让太记的小窗口永远保持在其它窗口上方,不会被其他窗口覆盖",
|
||||||
|
"TidgiMiniWindowFixedWorkspace": "为固定的太记小窗口选择工作区",
|
||||||
|
"TidgiMiniWindowShortcutKey": "设置快捷键来切换太记小窗口",
|
||||||
|
"TidgiMiniWindowShortcutKeyHelperText": "设置一个快捷键来快速打开或关闭太记小窗口",
|
||||||
|
"TidgiMiniWindowShortcutKeyPlaceholder": "例如:Ctrl+Shift+D",
|
||||||
|
"TidgiMiniWindowSyncWorkspaceWithMainWindow": "小窗和主窗口展示同样的工作区",
|
||||||
|
"TidgiMiniWindowSyncWorkspaceWithMainWindowDetail": "勾选后,小窗将与主窗口同步显示相同的工作区内容",
|
||||||
|
"TidgiMiniWindowShowTitleBar": "小窗显示标题栏",
|
||||||
|
"TidgiMiniWindowShowTitleBarDetail": "在太记小窗口上显示可拖动的标题栏",
|
||||||
"Miscellaneous": "其他设置",
|
"Miscellaneous": "其他设置",
|
||||||
"MoreWorkspaceSyncSettings": "更多工作区同步设置",
|
"MoreWorkspaceSyncSettings": "更多工作区同步设置",
|
||||||
"MoreWorkspaceSyncSettingsDescription": "请右键工作区图标,点右键菜单里的「编辑工作区」来打开工作区设置,在里面配各个工作区的同步设置。",
|
"MoreWorkspaceSyncSettingsDescription": "请右键工作区图标,点右键菜单里的「编辑工作区」来打开工作区设置,在里面配各个工作区的同步设置。",
|
||||||
|
|
@ -406,6 +422,7 @@
|
||||||
"SearchEmbeddingStatusIdle": "未生成嵌入",
|
"SearchEmbeddingStatusIdle": "未生成嵌入",
|
||||||
"SearchEmbeddingUpdate": "更新嵌入",
|
"SearchEmbeddingUpdate": "更新嵌入",
|
||||||
"SearchNoWorkspaces": "未找到工作区",
|
"SearchNoWorkspaces": "未找到工作区",
|
||||||
|
"SelectWorkspace": "选择工作区",
|
||||||
"ShareBrowsingData": "在工作区之间共享浏览器数据(cookies、缓存等),关闭后可以每个工作区登不同的第三方服务账号。",
|
"ShareBrowsingData": "在工作区之间共享浏览器数据(cookies、缓存等),关闭后可以每个工作区登不同的第三方服务账号。",
|
||||||
"ShowSideBar": "显示侧边栏",
|
"ShowSideBar": "显示侧边栏",
|
||||||
"ShowSideBarDetail": "侧边栏让你可以在工作区之间快速切换",
|
"ShowSideBarDetail": "侧边栏让你可以在工作区之间快速切换",
|
||||||
|
|
@ -431,7 +448,7 @@
|
||||||
"TestNotificationDescription": "<0>如果通知未显示,请确保在<1>macOS首选项 → 通知 → TidGi中启用通知</1></0>",
|
"TestNotificationDescription": "<0>如果通知未显示,请确保在<1>macOS首选项 → 通知 → TidGi中启用通知</1></0>",
|
||||||
"Theme": "主题色",
|
"Theme": "主题色",
|
||||||
"TiddlyWiki": "太微",
|
"TiddlyWiki": "太微",
|
||||||
"ToggleMenuBar": "切换显隐菜单栏",
|
"ToggleTidgiMiniWindow": "切换太记小窗",
|
||||||
"Token": "Git身份凭证",
|
"Token": "Git身份凭证",
|
||||||
"TokenDescription": "用于向Git服务器验证身份并同步内容的凭证,可通过登录在线存储服务(如Github)来取得,也可以手动获取「Personal Access Token」后填到这里。",
|
"TokenDescription": "用于向Git服务器验证身份并同步内容的凭证,可通过登录在线存储服务(如Github)来取得,也可以手动获取「Personal Access Token」后填到这里。",
|
||||||
"Translatium": "翻译素APP",
|
"Translatium": "翻译素APP",
|
||||||
|
|
@ -465,6 +482,7 @@
|
||||||
"Add": "添加",
|
"Add": "添加",
|
||||||
"Agent": "智能体",
|
"Agent": "智能体",
|
||||||
"AreYouSure": "你确定要移除这个工作区吗?移除工作区会删除本应用中的工作区,但不会删除硬盘上的文件夹。如果你选择一并删除知识库文件夹,则所有内容都会被删除。",
|
"AreYouSure": "你确定要移除这个工作区吗?移除工作区会删除本应用中的工作区,但不会删除硬盘上的文件夹。如果你选择一并删除知识库文件夹,则所有内容都会被删除。",
|
||||||
|
"DedicatedWorkspace": "专用工作区",
|
||||||
"DefaultTiddlers": "默认条目",
|
"DefaultTiddlers": "默认条目",
|
||||||
"EditCurrentWorkspace": "配置当前工作区",
|
"EditCurrentWorkspace": "配置当前工作区",
|
||||||
"EditWorkspace": "配置工作区",
|
"EditWorkspace": "配置工作区",
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
"OpenCommandPalette": "打開搜索與命令面板",
|
"OpenCommandPalette": "打開搜索與命令面板",
|
||||||
"OpenLinkInBrowser": "在瀏覽器中打開連結",
|
"OpenLinkInBrowser": "在瀏覽器中打開連結",
|
||||||
"OpenTidGi": "打開太記",
|
"OpenTidGi": "打開太記",
|
||||||
"OpenTidGiMenuBar": "打開太記小窗口",
|
"OpenTidGiMiniWindow": "打開太記小窗口",
|
||||||
"OpenWorkspaceInNewWindow": "在新窗口中打開工作區",
|
"OpenWorkspaceInNewWindow": "在新窗口中打開工作區",
|
||||||
"Paste": "黏貼",
|
"Paste": "黏貼",
|
||||||
"Preferences": "設置...",
|
"Preferences": "設置...",
|
||||||
|
|
@ -309,7 +309,7 @@
|
||||||
"SelectNextWorkspace": "選擇下一個工作區",
|
"SelectNextWorkspace": "選擇下一個工作區",
|
||||||
"SelectPreviousWorkspace": "選擇前一個工作區",
|
"SelectPreviousWorkspace": "選擇前一個工作區",
|
||||||
"TidGi": "太記",
|
"TidGi": "太記",
|
||||||
"TidGiMenuBar": "太記小窗",
|
"TidGiMiniWindow": "太記小窗",
|
||||||
"View": "查看",
|
"View": "查看",
|
||||||
"Wiki": "知識庫",
|
"Wiki": "知識庫",
|
||||||
"Window": "窗口",
|
"Window": "窗口",
|
||||||
|
|
@ -324,10 +324,10 @@
|
||||||
"AlwaysOnTopDetail": "讓太記的主窗口永遠保持在其它窗口上方,不會被其他窗口覆蓋",
|
"AlwaysOnTopDetail": "讓太記的主窗口永遠保持在其它窗口上方,不會被其他窗口覆蓋",
|
||||||
"AntiAntiLeech": "有的網站做了防盜鏈,會阻止某些圖片在你的知識庫上顯示,我們透過模擬訪問該網站的請求頭來繞過這種限制。",
|
"AntiAntiLeech": "有的網站做了防盜鏈,會阻止某些圖片在你的知識庫上顯示,我們透過模擬訪問該網站的請求頭來繞過這種限制。",
|
||||||
"AskDownloadLocation": "下載前詢問每個文件的保存位置",
|
"AskDownloadLocation": "下載前詢問每個文件的保存位置",
|
||||||
"AttachToMenuBar": "附加到選單欄",
|
"TidgiMiniWindow": "附加到太記小窗",
|
||||||
"AttachToMenuBarShowSidebar": "附加到選單欄的窗口包含側邊欄",
|
"TidgiMiniWindowShowSidebar": "太記小窗包含側邊欄",
|
||||||
"AttachToMenuBarShowSidebarTip": "一般太記小窗僅用於快速查看當前工作區,所以默認與主窗口工作區同步,不需要側邊欄,默認隱藏側邊欄。",
|
"TidgiMiniWindowShowSidebarTip": "一般太記小窗僅用於快速查看當前工作區,所以默認與主窗口工作區同步,不需要側邊欄,默認隱藏側邊欄。",
|
||||||
"AttachToMenuBarTip": "創建一個點擊選單欄/任務欄圖示會彈出的小太記窗口。提示:右鍵單擊小圖示以訪問上下文菜單。",
|
"TidgiMiniWindowTip": "創建一個點擊系統托盤圖示會彈出的小太記窗口。提示:右鍵單擊小圖示以訪問上下文菜單。",
|
||||||
"AttachToTaskbar": "附加到任務欄",
|
"AttachToTaskbar": "附加到任務欄",
|
||||||
"AttachToTaskbarShowSidebar": "附加到任務欄的窗口包含側邊欄",
|
"AttachToTaskbarShowSidebar": "附加到任務欄的窗口包含側邊欄",
|
||||||
"ChooseLanguage": "選擇語言 Choose Language",
|
"ChooseLanguage": "選擇語言 Choose Language",
|
||||||
|
|
@ -363,8 +363,8 @@
|
||||||
"ItIsWorking": "好使的!",
|
"ItIsWorking": "好使的!",
|
||||||
"Languages": "語言/Lang",
|
"Languages": "語言/Lang",
|
||||||
"LightTheme": "亮色主題",
|
"LightTheme": "亮色主題",
|
||||||
"MenubarAlwaysOnTop": "保持選單欄小窗口在其他窗口上方",
|
"TidgiMiniWindowAlwaysOnTop": "保持太記小窗口在其他窗口上方",
|
||||||
"MenubarAlwaysOnTopDetail": "讓太記的選單欄小窗口永遠保持在其它窗口上方,不會被其他窗口覆蓋",
|
"TidgiMiniWindowAlwaysOnTopDetail": "讓太記的小窗口永遠保持在其它窗口上方,不會被其他窗口覆蓋",
|
||||||
"Miscellaneous": "其他設置",
|
"Miscellaneous": "其他設置",
|
||||||
"MoreWorkspaceSyncSettings": "更多工作區同步設定",
|
"MoreWorkspaceSyncSettings": "更多工作區同步設定",
|
||||||
"MoreWorkspaceSyncSettingsDescription": "請右鍵工作區圖示,點右鍵菜單裡的「編輯工作區」來打開工作區設置,在裡面配各個工作區的同步設定。",
|
"MoreWorkspaceSyncSettingsDescription": "請右鍵工作區圖示,點右鍵菜單裡的「編輯工作區」來打開工作區設置,在裡面配各個工作區的同步設定。",
|
||||||
|
|
@ -431,7 +431,7 @@
|
||||||
"TestNotificationDescription": "<0>如果通知未顯示,請確保在<1>macOS首選項 → 通知 → TidGi中啟用通知</1></0>",
|
"TestNotificationDescription": "<0>如果通知未顯示,請確保在<1>macOS首選項 → 通知 → TidGi中啟用通知</1></0>",
|
||||||
"Theme": "主題色",
|
"Theme": "主題色",
|
||||||
"TiddlyWiki": "太微",
|
"TiddlyWiki": "太微",
|
||||||
"ToggleMenuBar": "切換顯隱選單欄",
|
"ToggleTidgiMiniWindow": "切換太記小窗",
|
||||||
"Token": "Git身份憑證",
|
"Token": "Git身份憑證",
|
||||||
"TokenDescription": "用於向Git伺服器驗證身份並同步內容的憑證,可透過登錄在線儲存服務(如Github)來取得,也可以手動獲取「Personal Access Token」後填到這裡。",
|
"TokenDescription": "用於向Git伺服器驗證身份並同步內容的憑證,可透過登錄在線儲存服務(如Github)來取得,也可以手動獲取「Personal Access Token」後填到這裡。",
|
||||||
"Translatium": "翻譯素APP",
|
"Translatium": "翻譯素APP",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"zh-Hans": "汉字",
|
"zh-Hans": "汉语",
|
||||||
"zh-Hant": "漢字",
|
"zh-Hant": "漢語",
|
||||||
"ja": "日本語",
|
"ja": "日本語",
|
||||||
"fr": "Français",
|
"fr": "Français",
|
||||||
"ru": "Русский"
|
"ru": "Русский"
|
||||||
|
|
|
||||||
27
package.json
27
package.json
|
|
@ -4,7 +4,7 @@
|
||||||
"description": "Customizable personal knowledge-base with Github as unlimited storage and blogging platform.",
|
"description": "Customizable personal knowledge-base with Github as unlimited storage and blogging platform.",
|
||||||
"version": "0.12.4",
|
"version": "0.12.4",
|
||||||
"license": "MPL 2.0",
|
"license": "MPL 2.0",
|
||||||
"packageManager": "pnpm@10.13.1",
|
"packageManager": "pnpm@10.18.2",
|
||||||
"scripts": {
|
"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: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",
|
"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": "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": "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: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",
|
"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": "pnpm run build:plugin && cross-env NODE_ENV=production electron-forge make",
|
||||||
"make:analyze": "cross-env ANALYZE=true pnpm run make",
|
"make:analyze": "cross-env ANALYZE=true pnpm run make",
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
"default-gateway": "6.0.3",
|
"default-gateway": "6.0.3",
|
||||||
"dugite": "2.7.1",
|
"dugite": "2.7.1",
|
||||||
"electron-dl": "^4.0.0",
|
"electron-dl": "^4.0.0",
|
||||||
"electron-ipc-cat": "2.0.1",
|
"electron-ipc-cat": "2.1.1",
|
||||||
"electron-settings": "5.0.0",
|
"electron-settings": "5.0.0",
|
||||||
"electron-unhandled": "4.0.1",
|
"electron-unhandled": "4.0.1",
|
||||||
"electron-window-state": "5.0.3",
|
"electron-window-state": "5.0.3",
|
||||||
|
|
@ -102,6 +102,7 @@
|
||||||
"rotating-file-stream": "^3.2.5",
|
"rotating-file-stream": "^3.2.5",
|
||||||
"rxjs": "7.8.2",
|
"rxjs": "7.8.2",
|
||||||
"semver": "7.7.2",
|
"semver": "7.7.2",
|
||||||
|
"serialize-error": "^12.0.0",
|
||||||
"simplebar": "6.3.1",
|
"simplebar": "6.3.1",
|
||||||
"simplebar-react": "3.3.0",
|
"simplebar-react": "3.3.0",
|
||||||
"source-map-support": "0.5.21",
|
"source-map-support": "0.5.21",
|
||||||
|
|
@ -122,19 +123,19 @@
|
||||||
"zx": "8.5.5"
|
"zx": "8.5.5"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@electron-forge/maker-deb": "7.8.1",
|
"@electron-forge/maker-deb": "7.10.2",
|
||||||
"@electron-forge/maker-flatpak": "7.8.1",
|
"@electron-forge/maker-flatpak": "7.10.2",
|
||||||
"@electron-forge/maker-rpm": "7.8.1",
|
"@electron-forge/maker-rpm": "7.10.2",
|
||||||
"@electron-forge/maker-snap": "7.8.1",
|
"@electron-forge/maker-snap": "7.10.2",
|
||||||
"@electron-forge/maker-squirrel": "7.8.1",
|
"@electron-forge/maker-squirrel": "7.10.2",
|
||||||
"@electron-forge/maker-zip": "7.8.1",
|
"@electron-forge/maker-zip": "7.10.2",
|
||||||
"@reforged/maker-appimage": "^5.0.0"
|
"@reforged/maker-appimage": "5.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cucumber/cucumber": "^11.2.0",
|
"@cucumber/cucumber": "^11.2.0",
|
||||||
"@electron-forge/cli": "7.8.1",
|
"@electron-forge/cli": "7.10.2",
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "7.8.1",
|
"@electron-forge/plugin-auto-unpack-natives": "7.10.2",
|
||||||
"@electron-forge/plugin-vite": "^7.9.0",
|
"@electron-forge/plugin-vite": "7.10.2",
|
||||||
"@electron/rebuild": "^4.0.1",
|
"@electron/rebuild": "^4.0.1",
|
||||||
"@fetsorn/vite-node-worker": "^1.0.1",
|
"@fetsorn/vite-node-worker": "^1.0.1",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
|
|
||||||
738
pnpm-lock.yaml
generated
738
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
212
src/components/KeyboardShortcutRegister.tsx
Normal file
212
src/components/KeyboardShortcutRegister.tsx
Normal file
|
|
@ -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<KeyboardShortcutRegisterProps> = ({
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant={variant}
|
||||||
|
color={color}
|
||||||
|
onClick={handleOpenDialog}
|
||||||
|
disabled={disabled}
|
||||||
|
fullWidth
|
||||||
|
data-testid='shortcut-register-button'
|
||||||
|
>
|
||||||
|
{label}: {formatShortcutText(value)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={dialogOpen}
|
||||||
|
onClose={handleCloseDialog}
|
||||||
|
maxWidth='sm'
|
||||||
|
fullWidth
|
||||||
|
data-testid='shortcut-dialog'
|
||||||
|
>
|
||||||
|
<DialogTitle>{t('KeyboardShortcut.RegisterShortcut')}</DialogTitle>
|
||||||
|
<DialogContent data-testid='shortcut-dialog-content'>
|
||||||
|
<Typography variant='body1' gutterBottom>
|
||||||
|
{t('KeyboardShortcut.PressKeysPrompt', { feature: label })}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
data-testid='shortcut-display'
|
||||||
|
sx={{
|
||||||
|
padding: 2,
|
||||||
|
border: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
borderRadius: 1,
|
||||||
|
marginBottom: 2,
|
||||||
|
textAlign: 'center',
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant='h5' component='div'>
|
||||||
|
{currentKeyCombo || t('KeyboardShortcut.PressKeys')}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant='caption' color='textSecondary'>
|
||||||
|
{t('KeyboardShortcut.HelpText')}
|
||||||
|
</Typography>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleClear} color='inherit' data-testid='shortcut-clear-button'>
|
||||||
|
{t('KeyboardShortcut.Clear')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCloseDialog} data-testid='shortcut-cancel-button'>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleConfirm} variant='contained' color={color} data-testid='shortcut-confirm-button'>
|
||||||
|
{t('Confirm')}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -11,7 +11,7 @@ export function useAuth(storageService: SupportedStorageServices): [() => Promis
|
||||||
await window.service.auth.set(`${storageService}-token`, '');
|
await window.service.auth.set(`${storageService}-token`, '');
|
||||||
// await window.service.window.clearStorageData();
|
// await window.service.window.clearStorageData();
|
||||||
} catch (error) {
|
} 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]);
|
}, [storageService]);
|
||||||
|
|
||||||
|
|
@ -66,7 +66,7 @@ export function useGetGithubUserInfoOnLoad(): void {
|
||||||
await window.service.auth.setUserInfos(userInfo);
|
await window.service.auth.setUserInfos(userInfo);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
||||||
507
src/components/__tests__/KeyboardShortcutRegister.test.tsx
Normal file
507
src/components/__tests__/KeyboardShortcutRegister.test.tsx
Normal file
|
|
@ -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 }) => (
|
||||||
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('KeyboardShortcutRegister Component', () => {
|
||||||
|
let mockOnChange: ReturnType<typeof vi.fn>;
|
||||||
|
|
||||||
|
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(
|
||||||
|
<TestWrapper>
|
||||||
|
<KeyboardShortcutRegister {...defaultProps} />
|
||||||
|
</TestWrapper>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -24,14 +24,15 @@ export const sourcePath = isPackaged
|
||||||
? path.resolve(process.resourcesPath, '..') // Packaged: go up from resources/ to app root
|
? path.resolve(process.resourcesPath, '..') // Packaged: go up from resources/ to app root
|
||||||
: path.resolve(__dirname, '..', '..'); // Dev/Unit test: from src/constants to project root
|
: path.resolve(__dirname, '..', '..'); // Dev/Unit test: from src/constants to project root
|
||||||
// Build resources (only used in dev/test)
|
// 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');
|
export const developmentImageFolderPath = path.resolve(sourcePath, 'images');
|
||||||
|
|
||||||
// Menubar icon
|
// TidGi Mini Window icon
|
||||||
const menuBarIconFileName = isMac ? 'menubarTemplate@2x.png' : 'menubar@2x.png';
|
const tidgiMiniWindowIconFileName = isMac ? 'tidgiMiniWindowTemplate@2x.png' : 'tidgiMiniWindow@2x.png';
|
||||||
export const MENUBAR_ICON_PATH = isPackaged
|
export const TIDGI_MINI_WINDOW_ICON_PATH = isPackaged
|
||||||
? path.resolve(process.resourcesPath, menuBarIconFileName) // Packaged: resources/icon
|
? path.resolve(process.resourcesPath, tidgiMiniWindowIconFileName) // Packaged: resources/<icon>
|
||||||
: path.resolve(buildResourcePath, menuBarIconFileName); // Dev/Unit test: build-resources/icon
|
: path.resolve(buildResourcePath, tidgiMiniWindowIconFileName); // Dev/Unit test: <project-root>/build-resources/<icon>
|
||||||
|
|
||||||
// System paths
|
// System paths
|
||||||
export const CHROME_ERROR_PATH = 'chrome-error://chromewebdata/';
|
export const CHROME_ERROR_PATH = 'chrome-error://chromewebdata/';
|
||||||
|
|
|
||||||
121
src/helpers/testKeyboardShortcuts.ts
Normal file
121
src/helpers/testKeyboardShortcuts.ts
Normal file
|
|
@ -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<string, string> = {};
|
||||||
|
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<string, string> } | 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?.();
|
||||||
|
};
|
||||||
|
}
|
||||||
20
src/main.ts
20
src/main.ts
|
|
@ -24,6 +24,7 @@ import type { IDeepLinkService } from '@services/deepLink/interface';
|
||||||
import type { IExternalAPIService } from '@services/externalAPI/interface';
|
import type { IExternalAPIService } from '@services/externalAPI/interface';
|
||||||
import type { IGitService } from '@services/git/interface';
|
import type { IGitService } from '@services/git/interface';
|
||||||
import { initializeObservables } from '@services/libs/initializeObservables';
|
import { initializeObservables } from '@services/libs/initializeObservables';
|
||||||
|
import type { INativeService } from '@services/native/interface';
|
||||||
import { reportErrorToGithubWithTemplates } from '@services/native/reportError';
|
import { reportErrorToGithubWithTemplates } from '@services/native/reportError';
|
||||||
import type { IThemeService } from '@services/theme/interface';
|
import type { IThemeService } from '@services/theme/interface';
|
||||||
import type { IUpdaterService } from '@services/updater/interface';
|
import type { IUpdaterService } from '@services/updater/interface';
|
||||||
|
|
@ -77,6 +78,7 @@ const externalAPIService = container.get<IExternalAPIService>(serviceIdentifier.
|
||||||
const gitService = container.get<IGitService>(serviceIdentifier.Git);
|
const gitService = container.get<IGitService>(serviceIdentifier.Git);
|
||||||
const themeService = container.get<IThemeService>(serviceIdentifier.ThemeService);
|
const themeService = container.get<IThemeService>(serviceIdentifier.ThemeService);
|
||||||
const viewService = container.get<IViewService>(serviceIdentifier.View);
|
const viewService = container.get<IViewService>(serviceIdentifier.View);
|
||||||
|
const nativeService = container.get<INativeService>(serviceIdentifier.NativeService);
|
||||||
|
|
||||||
app.on('second-instance', async () => {
|
app.on('second-instance', async () => {
|
||||||
// see also src/helpers/singleInstance.ts
|
// see also src/helpers/singleInstance.ts
|
||||||
|
|
@ -116,21 +118,18 @@ const commonInit = async (): Promise<void> => {
|
||||||
externalAPIService.initialize(),
|
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
|
// handle workspace name + tiddler name in uri https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app
|
||||||
deepLinkService.initializeDeepLink('tidgi');
|
deepLinkService.initializeDeepLink('tidgi');
|
||||||
|
|
||||||
const attachToMenubar = await preferenceService.get('attachToMenubar');
|
await windowService.open(WindowNames.main);
|
||||||
await Promise.all([
|
|
||||||
windowService.open(WindowNames.main),
|
|
||||||
attachToMenubar ? windowService.open(WindowNames.menuBar) : Promise.resolve(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Initialize services that depend on windows being created
|
// Initialize services that depend on windows being created
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
gitService.initialize(),
|
gitService.initialize(),
|
||||||
themeService.initialize(),
|
themeService.initialize(),
|
||||||
viewService.initialize(),
|
viewService.initialize(),
|
||||||
|
nativeService.initialize(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
initializeObservables();
|
initializeObservables();
|
||||||
|
|
@ -140,6 +139,10 @@ const commonInit = async (): Promise<void> => {
|
||||||
await workspaceService.initializeDefaultPageWorkspaces();
|
await workspaceService.initializeDefaultPageWorkspaces();
|
||||||
// perform wiki startup and git sync for each workspace
|
// perform wiki startup and git sync for each workspace
|
||||||
await workspaceViewService.initializeAllWorkspaceView();
|
await workspaceViewService.initializeAllWorkspaceView();
|
||||||
|
const tidgiMiniWindow = await preferenceService.get('tidgiMiniWindow');
|
||||||
|
if (tidgiMiniWindow) {
|
||||||
|
await windowService.openTidgiMiniWindow(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
ipcMain.emit('request-update-pause-notifications-info');
|
ipcMain.emit('request-update-pause-notifications-info');
|
||||||
// Fix webview is not resized automatically
|
// Fix webview is not resized automatically
|
||||||
|
|
@ -195,8 +198,7 @@ app.on('ready', async () => {
|
||||||
}
|
}
|
||||||
await updaterService.checkForUpdates();
|
await updaterService.checkForUpdates();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const error_ = error instanceof Error ? error : new Error(String(error));
|
logger.error('Error during app ready handler', { function: "app.on('ready')", error });
|
||||||
logger.error('Error during app ready handler', { function: "app.on('ready')", error: error_.message, stack: error_.stack ?? '' });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
app.on(MainChannel.windowAllClosed, async () => {
|
app.on(MainChannel.windowAllClosed, async () => {
|
||||||
|
|
@ -222,7 +224,7 @@ app.on(
|
||||||
unhandled({
|
unhandled({
|
||||||
showDialog: !isDevelopmentOrTest,
|
showDialog: !isDevelopmentOrTest,
|
||||||
logger: (error: Error) => {
|
logger: (error: Error) => {
|
||||||
logger.error(error.message + (error.stack ?? ''));
|
logger.error('unhandled', { error });
|
||||||
},
|
},
|
||||||
reportButton: (error) => {
|
reportButton: (error) => {
|
||||||
reportErrorToGithubWithTemplates(error);
|
reportErrorToGithubWithTemplates(error);
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export function TabStoreInitializer() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Initialize the tab store when the component mounts
|
// Initialize the tab store when the component mounts
|
||||||
initialize().catch((error: unknown) => {
|
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]);
|
}, [initialize]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export const agentActions = (
|
||||||
`Failed to fetch agent definition for ${agentWithoutMessages.agentDefId}`,
|
`Failed to fetch agent definition for ${agentWithoutMessages.agentDefId}`,
|
||||||
{
|
{
|
||||||
function: 'agentActions.processAgentData',
|
function: 'agentActions.processAgentData',
|
||||||
error: String(error),
|
error,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -88,9 +88,9 @@ export const agentActions = (
|
||||||
error: null,
|
error: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
} catch (error_) {
|
} 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 load agent', { function: 'agentActions.loadAgent', error: String(error_) });
|
void window.service.native.log('error', 'Failed to load agent', { function: 'agentActions.loadAgent', error });
|
||||||
} finally {
|
} finally {
|
||||||
set({ loading: false });
|
set({ loading: false });
|
||||||
}
|
}
|
||||||
|
|
@ -114,9 +114,9 @@ export const agentActions = (
|
||||||
});
|
});
|
||||||
|
|
||||||
return processedData.agent;
|
return processedData.agent;
|
||||||
} catch (error_) {
|
} 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 create agent', { function: 'agentActions.createAgent', error: String(error_) });
|
void window.service.native.log('error', 'Failed to create agent', { function: 'agentActions.createAgent', error });
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
set({ loading: false });
|
set({ loading: false });
|
||||||
|
|
@ -147,9 +147,9 @@ export const agentActions = (
|
||||||
});
|
});
|
||||||
|
|
||||||
return processedData.agent;
|
return processedData.agent;
|
||||||
} catch (error_) {
|
} 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 update agent', { function: 'agentActions.updateAgent', error: String(error_) });
|
void window.service.native.log('error', 'Failed to update agent', { function: 'agentActions.updateAgent', error });
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
set({ loading: false });
|
set({ loading: false });
|
||||||
|
|
@ -179,7 +179,7 @@ export const agentActions = (
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const isInitialCall = !get().agent;
|
const isInitialCall = !get().agent;
|
||||||
set({
|
set({
|
||||||
error: error instanceof Error ? error : new Error(String(error)),
|
error: error as Error,
|
||||||
...(isInitialCall ? { loading: false } : {}),
|
...(isInitialCall ? { loading: false } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -246,7 +246,7 @@ export const agentActions = (
|
||||||
`Error in message subscription for ${message.id}`,
|
`Error in message subscription for ${message.id}`,
|
||||||
{
|
{
|
||||||
function: 'agentActions.subscribeToUpdates.messageSubscription',
|
function: 'agentActions.subscribeToUpdates.messageSubscription',
|
||||||
error: String(error_),
|
error: error_,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -281,10 +281,10 @@ export const agentActions = (
|
||||||
'Error in agent subscription',
|
'Error in agent subscription',
|
||||||
{
|
{
|
||||||
function: 'agentActions.subscribeToUpdates.agentSubscription',
|
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();
|
subscription.unsubscribe();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
} catch (error_) {
|
} catch (error) {
|
||||||
void window.service.native.log('error', 'Failed to subscribe to agent updates', { function: 'agentActions.subscribeToUpdates', error: String(error_) });
|
void window.service.native.log('error', 'Failed to subscribe to agent updates', { function: 'agentActions.subscribeToUpdates', error });
|
||||||
set({ error: error_ instanceof Error ? error_ : new Error(String(error_)) });
|
set({ error: error as Error });
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -347,8 +347,8 @@ export const agentActions = (
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await window.service.agentInstance.cancelAgent(storeAgent.id);
|
await window.service.agentInstance.cancelAgent(storeAgent.id);
|
||||||
} catch (error_) {
|
} catch (error) {
|
||||||
void window.service.native.log('error', 'Store: cancelAgent backend call failed', { function: 'agentActions.cancelAgent', agentId: storeAgent.id, error: String(error_) });
|
void window.service.native.log('error', 'Store: cancelAgent backend call failed', { function: 'agentActions.cancelAgent', agentId: storeAgent.id, error });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,11 @@ export const messageActions = (
|
||||||
set({ loading: true });
|
set({ loading: true });
|
||||||
await window.service.agentInstance.sendMsgToAgent(storeAgent.id, { text: content });
|
await window.service.agentInstance.sendMsgToAgent(storeAgent.id, { text: content });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({ error: error instanceof Error ? error : new Error(String(error)) });
|
set({ error: error as Error });
|
||||||
void window.service.native.log(
|
void window.service.native.log(
|
||||||
'error',
|
'error',
|
||||||
'Failed to send message',
|
'Failed to send message',
|
||||||
{ function: 'messageActions.sendMessage', error: String(error) },
|
{ function: 'messageActions.sendMessage', error },
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
set({ loading: false });
|
set({ loading: false });
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ export const previewActionsMiddleware: StateCreator<AgentChatStoreType, [], [],
|
||||||
previewCurrentStep: 'Error occurred',
|
previewCurrentStep: 'Error occurred',
|
||||||
previewCurrentPlugin: null,
|
previewCurrentPlugin: null,
|
||||||
});
|
});
|
||||||
reject(error instanceof Error ? error : new Error(String(error)));
|
reject(error as Error);
|
||||||
},
|
},
|
||||||
complete: () => {
|
complete: () => {
|
||||||
completed = true;
|
completed = true;
|
||||||
|
|
|
||||||
|
|
@ -265,7 +265,7 @@ export const basicActionsMiddleware: StateCreator<
|
||||||
activeTabId,
|
activeTabId,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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);
|
console.error('Failed to add tab:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ export const ChatTitle: React.FC<ChatTitleProps> = ({ title, agent, updateAgent
|
||||||
await updateAgent({ name: newTitle });
|
await updateAgent({ name: newTitle });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export function useLoadHelpPagesList(language = 'en-GB') {
|
||||||
const data = await fetch(source).then(async response => await (response.json() as Promise<typeof helpPages.default>));
|
const data = await fetch(source).then(async response => await (response.json() as Promise<typeof helpPages.default>));
|
||||||
return data.map(makeFallbackUrlsArray);
|
return data.map(makeFallbackUrlsArray);
|
||||||
} catch (error) {
|
} 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 [];
|
return [];
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
@ -28,7 +28,7 @@ export function useLoadHelpPagesList(language = 'en-GB') {
|
||||||
const newItems = responses.flat();
|
const newItems = responses.flat();
|
||||||
setItems(currentItems => uniqBy([...currentItems, ...newItems], 'url'));
|
setItems(currentItems => uniqBy([...currentItems, ...newItems], 'url'));
|
||||||
} catch (error) {
|
} 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 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,13 +71,13 @@ const IconButton = styled(IconButtonRaw)`
|
||||||
color: ${({ theme }) => theme.palette.action.active};
|
color: ${({ theme }) => theme.palette.action.active};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SidebarContainer = ({ children }: { children: React.ReactNode }): React.JSX.Element => {
|
const SidebarContainer = ({ children, ...props }: { children: React.ReactNode } & React.HTMLAttributes<HTMLDivElement>): React.JSX.Element => {
|
||||||
const platform = usePromiseValue(async () => await window.service.context.get('platform'));
|
const platform = usePromiseValue(async () => await window.service.context.get('platform'));
|
||||||
// use native scroll bar on macOS
|
// use native scroll bar on macOS
|
||||||
if (platform === 'darwin') {
|
if (platform === 'darwin') {
|
||||||
return <SidebarRoot>{children}</SidebarRoot>;
|
return <SidebarRoot {...props}>{children}</SidebarRoot>;
|
||||||
}
|
}
|
||||||
return <SidebarWithStyle>{children}</SidebarWithStyle>;
|
return <SidebarWithStyle {...props}>{children}</SidebarWithStyle>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SideBar(): React.JSX.Element {
|
export function SideBar(): React.JSX.Element {
|
||||||
|
|
@ -92,7 +92,7 @@ export function SideBar(): React.JSX.Element {
|
||||||
const { showSideBarText, showSideBarIcon } = preferences;
|
const { showSideBarText, showSideBarIcon } = preferences;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarContainer>
|
<SidebarContainer data-testid='main-sidebar'>
|
||||||
<SidebarTop $titleBar={titleBar}>
|
<SidebarTop $titleBar={titleBar}>
|
||||||
{workspacesList === undefined
|
{workspacesList === undefined
|
||||||
? <div>{t('Loading')}</div>
|
? <div>{t('Loading')}</div>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
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 { MouseEvent, useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { WorkspaceSelectorBase } from './WorkspaceSelectorBase';
|
import { useLocation } from 'wouter';
|
||||||
|
|
||||||
import { PageType } from '@/constants/pageTypes';
|
import { PageType } from '@/constants/pageTypes';
|
||||||
import { getBuildInPageIcon } from '@/pages/Main/WorkspaceIconAndSelector/getBuildInPageIcon';
|
import { getBuildInPageIcon } from '@/pages/Main/WorkspaceIconAndSelector/getBuildInPageIcon';
|
||||||
import { getBuildInPageName } from '@/pages/Main/WorkspaceIconAndSelector/getBuildInPageName';
|
import { getBuildInPageName } from '@/pages/Main/WorkspaceIconAndSelector/getBuildInPageName';
|
||||||
|
import { usePreferenceObservable } from '@services/preferences/hooks';
|
||||||
import { WindowNames } from '@services/windows/WindowProperties';
|
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 {
|
export interface ISortableItemProps {
|
||||||
index: number;
|
index: number;
|
||||||
|
|
@ -22,6 +23,7 @@ export interface ISortableItemProps {
|
||||||
export function SortableWorkspaceSelectorButton({ index, workspace, showSidebarTexts, showSideBarIcon }: ISortableItemProps): React.JSX.Element {
|
export function SortableWorkspaceSelectorButton({ index, workspace, showSidebarTexts, showSideBarIcon }: ISortableItemProps): React.JSX.Element {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { active, id, name, picturePath, pageType } = workspace;
|
const { active, id, name, picturePath, pageType } = workspace;
|
||||||
|
const preference = usePreferenceObservable();
|
||||||
|
|
||||||
const isWiki = isWikiWorkspace(workspace);
|
const isWiki = isWikiWorkspace(workspace);
|
||||||
const hibernated = isWiki ? workspace.hibernated : false;
|
const hibernated = isWiki ? workspace.hibernated : false;
|
||||||
|
|
@ -49,20 +51,41 @@ export function SortableWorkspaceSelectorButton({ index, workspace, showSidebarT
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [pageType]);
|
}, [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 () => {
|
const onWorkspaceClick = useCallback(async () => {
|
||||||
workspaceClickedLoadingSetter(true);
|
workspaceClickedLoadingSetter(true);
|
||||||
try {
|
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) {
|
if (workspace.pageType) {
|
||||||
// Handle special "add" workspace
|
// Page workspaces (dashboard, etc.)
|
||||||
if (workspace.pageType === PageType.add) {
|
setLocation(`/${workspace.pageType}`);
|
||||||
await window.service.window.open(WindowNames.addWorkspace);
|
await window.service.workspaceView.setActiveWorkspaceView(id);
|
||||||
} else {
|
|
||||||
// Handle other page workspaces - navigate to the page and set as active workspace
|
|
||||||
setLocation(`/${workspace.pageType}`);
|
|
||||||
await window.service.workspaceView.setActiveWorkspaceView(id);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Handle regular wiki workspace
|
// Regular wiki workspace
|
||||||
setLocation(`/${PageType.wiki}/${id}/`);
|
setLocation(`/${PageType.wiki}/${id}/`);
|
||||||
await window.service.workspace.openWorkspaceTiddler(workspace);
|
await window.service.workspace.openWorkspaceTiddler(workspace);
|
||||||
}
|
}
|
||||||
|
|
@ -73,7 +96,7 @@ export function SortableWorkspaceSelectorButton({ index, workspace, showSidebarT
|
||||||
} finally {
|
} finally {
|
||||||
workspaceClickedLoadingSetter(false);
|
workspaceClickedLoadingSetter(false);
|
||||||
}
|
}
|
||||||
}, [id, setLocation, workspace]);
|
}, [id, setLocation, workspace, isMiniWindow]);
|
||||||
const onWorkspaceContextMenu = useCallback(
|
const onWorkspaceContextMenu = useCallback(
|
||||||
async (event: MouseEvent<HTMLDivElement>) => {
|
async (event: MouseEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -90,7 +113,7 @@ export function SortableWorkspaceSelectorButton({ index, workspace, showSidebarT
|
||||||
restarting={workspace.metadata.isRestarting}
|
restarting={workspace.metadata.isRestarting}
|
||||||
showSideBarIcon={showSideBarIcon}
|
showSideBarIcon={showSideBarIcon}
|
||||||
onClick={onWorkspaceClick}
|
onClick={onWorkspaceClick}
|
||||||
active={active}
|
active={isActive}
|
||||||
id={id}
|
id={id}
|
||||||
key={id}
|
key={id}
|
||||||
pageType={pageType || undefined}
|
pageType={pageType || undefined}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import { DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
import { DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||||
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
|
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
|
||||||
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
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 { IWorkspace, IWorkspaceWithMetadata } from '@services/workspaces/interface';
|
||||||
import { SortableWorkspaceSelectorButton } from './SortableWorkspaceSelectorButton';
|
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 (
|
return (
|
||||||
<DndContext
|
<DndContext
|
||||||
|
|
@ -47,7 +61,7 @@ export function SortableWorkspaceSelectorList({ workspacesList, showSideBarText,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SortableContext items={workspaceIDs} strategy={verticalListSortingStrategy}>
|
<SortableContext items={workspaceIDs} strategy={verticalListSortingStrategy}>
|
||||||
{workspacesList
|
{filteredWorkspacesList
|
||||||
.sort((a, b) => a.order - b.order)
|
.sort((a, b) => a.order - b.order)
|
||||||
.map((workspace, index) => (
|
.map((workspace, index) => (
|
||||||
<SortableWorkspaceSelectorButton
|
<SortableWorkspaceSelectorButton
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,7 @@ export function WorkspaceSelectorBase({
|
||||||
$workspaceClickedLoading={workspaceClickedLoading}
|
$workspaceClickedLoading={workspaceClickedLoading}
|
||||||
onClick={workspaceClickedLoading ? () => {} : onClick}
|
onClick={workspaceClickedLoading ? () => {} : onClick}
|
||||||
data-testid={pageType ? `workspace-${pageType}` : `workspace-${id}`}
|
data-testid={pageType ? `workspace-${pageType}` : `workspace-${id}`}
|
||||||
|
data-active={active ? 'true' : 'false'}
|
||||||
>
|
>
|
||||||
<Badge color='secondary' badgeContent={badgeCount} max={99}>
|
<Badge color='secondary' badgeContent={badgeCount} max={99}>
|
||||||
{icon}
|
{icon}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import Main from '../index';
|
||||||
// Mock window.observables to provide realistic API behavior
|
// Mock window.observables to provide realistic API behavior
|
||||||
const preferencesSubject = new BehaviorSubject({
|
const preferencesSubject = new BehaviorSubject({
|
||||||
sidebar: true,
|
sidebar: true,
|
||||||
sidebarOnMenubar: true,
|
tidgiMiniWindowShowSidebar: true,
|
||||||
showSideBarText: true,
|
showSideBarText: true,
|
||||||
showSideBarIcon: true,
|
showSideBarIcon: true,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -57,19 +57,19 @@ const ContentRoot = styled('div')<{ $sidebar: boolean }>`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const windowName = window.meta().windowName;
|
|
||||||
|
|
||||||
export default function Main(): React.JSX.Element {
|
export default function Main(): React.JSX.Element {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
useInitialPage();
|
useInitialPage();
|
||||||
|
const windowName = window.meta().windowName;
|
||||||
const preferences = usePreferenceObservable();
|
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 (
|
return (
|
||||||
<OuterRoot>
|
<OuterRoot>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{t('Menu.TidGi')}</title>
|
<title>{t('Menu.TidGi')}{isTidgiMiniWindow ? ` - ${t('Menu.TidGiMiniWindow')}` : ''}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Root>
|
<Root data-windowName={windowName} data-showSidebar={showSidebar}>
|
||||||
{showSidebar && <SideBar />}
|
{showSidebar && <SideBar />}
|
||||||
<ContentRoot $sidebar={showSidebar}>
|
<ContentRoot $sidebar={showSidebar}>
|
||||||
<FindInPage />
|
<FindInPage />
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,92 @@
|
||||||
import { PageType } from '@/constants/pageTypes';
|
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 { useWorkspacesListObservable } from '@services/workspaces/hooks';
|
||||||
|
import type { IWorkspaceWithMetadata } from '@services/workspaces/interface';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useLocation } from 'wouter';
|
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() {
|
export function useInitialPage() {
|
||||||
const [location, setLocation] = useLocation();
|
const [location, setLocation] = useLocation();
|
||||||
const workspacesList = useWorkspacesListObservable();
|
const workspacesList = useWorkspacesListObservable();
|
||||||
|
const preferences = usePreferenceObservable();
|
||||||
const hasInitialized = useRef(false);
|
const hasInitialized = useRef(false);
|
||||||
|
const windowName = window.meta().windowName;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only initialize once and only when at root
|
// Only initialize once and only when at root
|
||||||
if (workspacesList && !hasInitialized.current && (location === '/' || location === '')) {
|
if (workspacesList && !hasInitialized.current && (location === '/' || location === '')) {
|
||||||
hasInitialized.current = true;
|
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.
|
// If there's no active workspace, navigate to root instead of guide.
|
||||||
// Root lets the UI stay neutral and prevents forcing the guide view.
|
// Root lets the UI stay neutral and prevents forcing the guide view.
|
||||||
setLocation(`/`);
|
setLocation(`/`);
|
||||||
} else if (activeWorkspace.pageType) {
|
} else if (targetWorkspace.pageType) {
|
||||||
// Don't navigate to add page, fallback to guide instead
|
// Don't navigate to add page, fallback to guide instead
|
||||||
if (activeWorkspace.pageType === PageType.add) {
|
if (targetWorkspace.pageType === PageType.add) {
|
||||||
setLocation(`/`);
|
setLocation(`/`);
|
||||||
} else {
|
} else {
|
||||||
setLocation(`/${activeWorkspace.pageType}`);
|
setLocation(`/${targetWorkspace.pageType}`);
|
||||||
}
|
}
|
||||||
} else {
|
} 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]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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).
|
* @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 => {
|
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;
|
const clickedButtonIndex = ipcRenderer.sendSync(NativeChannel.showElectronMessageBoxSync, options, WindowNames.main) as unknown;
|
||||||
if (typeof clickedButtonIndex === 'number') {
|
if (typeof clickedButtonIndex === 'number') {
|
||||||
return clickedButtonIndex;
|
return clickedButtonIndex;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { ThemeProvider } from '@mui/material/styles';
|
import { ThemeProvider } from '@mui/material/styles';
|
||||||
import i18next from 'i18next';
|
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 { createRoot } from 'react-dom/client';
|
||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import { Router } from 'wouter';
|
import { Router } from 'wouter';
|
||||||
|
|
@ -24,10 +24,12 @@ import { initRendererI18N } from './services/libs/i18n/renderer';
|
||||||
import 'electron-ipc-cat/fixContextIsolation';
|
import 'electron-ipc-cat/fixContextIsolation';
|
||||||
import { useHashLocation } from 'wouter/use-hash-location';
|
import { useHashLocation } from 'wouter/use-hash-location';
|
||||||
import { RootStyle } from './components/RootStyle';
|
import { RootStyle } from './components/RootStyle';
|
||||||
|
import { initTestKeyboardShortcutFallback } from './helpers/testKeyboardShortcuts';
|
||||||
import { Pages } from './windows';
|
import { Pages } from './windows';
|
||||||
|
|
||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
const theme = useThemeObservable();
|
const theme = useThemeObservable();
|
||||||
|
useEffect(() => initTestKeyboardShortcutFallback(), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,7 @@ export class AgentBrowserService implements IAgentBrowserService {
|
||||||
this.tabRepository = this.dataSource.getRepository(AgentBrowserTabEntity);
|
this.tabRepository = this.dataSource.getRepository(AgentBrowserTabEntity);
|
||||||
logger.debug('Agent browser repository initialized');
|
logger.debug('Agent browser repository initialized');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
logger.error('Failed to initialize agent browser service', { error });
|
||||||
logger.error(`Failed to initialize agent browser service: ${errorMessage}`);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -220,7 +219,7 @@ export class AgentBrowserService implements IAgentBrowserService {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to get tabs', {
|
logger.error('Failed to get tabs', {
|
||||||
function: 'getAllTabs',
|
function: 'getAllTabs',
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -242,7 +241,7 @@ export class AgentBrowserService implements IAgentBrowserService {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to get active tab', {
|
logger.error('Failed to get active tab', {
|
||||||
function: 'getActiveTabId',
|
function: 'getActiveTabId',
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -277,7 +276,7 @@ export class AgentBrowserService implements IAgentBrowserService {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to set active tab', {
|
logger.error('Failed to set active tab', {
|
||||||
function: 'setActiveTab',
|
function: 'setActiveTab',
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -321,7 +320,7 @@ export class AgentBrowserService implements IAgentBrowserService {
|
||||||
|
|
||||||
return savedTab;
|
return savedTab;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to add tab: ${error as Error}`);
|
logger.error('Failed to add tab', { error });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -392,7 +391,7 @@ export class AgentBrowserService implements IAgentBrowserService {
|
||||||
await this.tabRepository!.save(existingTab);
|
await this.tabRepository!.save(existingTab);
|
||||||
await this.updateTabsObservable();
|
await this.updateTabsObservable();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to update tab: ${error as Error}`);
|
logger.error('Failed to update tab', { error });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -458,7 +457,7 @@ export class AgentBrowserService implements IAgentBrowserService {
|
||||||
await this.reindexTabPositions();
|
await this.reindexTabPositions();
|
||||||
await this.updateTabsObservable();
|
await this.updateTabsObservable();
|
||||||
} catch (error) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -560,7 +559,7 @@ export class AgentBrowserService implements IAgentBrowserService {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to get closed tabs', {
|
logger.error('Failed to get closed tabs', {
|
||||||
function: 'getClosedTabs',
|
function: 'getClosedTabs',
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -606,7 +605,7 @@ export class AgentBrowserService implements IAgentBrowserService {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to restore closed tab', {
|
logger.error('Failed to restore closed tab', {
|
||||||
function: 'restoreClosedTab',
|
function: 'restoreClosedTab',
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -648,7 +647,7 @@ export class AgentBrowserService implements IAgentBrowserService {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to reindex tab positions', {
|
logger.error('Failed to reindex tab positions', {
|
||||||
function: 'reindexTabPositions',
|
function: 'reindexTabPositions',
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -241,13 +241,13 @@ export async function* basicPromptConcatHandler(context: AgentHandlerContext) {
|
||||||
(tm).metadata = { ...(tm).metadata, isPersisted: true };
|
(tm).metadata = { ...(tm).metadata, isPersisted: true };
|
||||||
} catch (error1) {
|
} catch (error1) {
|
||||||
logger.warn('Failed to persist pending tool result before error', {
|
logger.warn('Failed to persist pending tool result before error', {
|
||||||
error: error1 instanceof Error ? error1.message : String(error1),
|
error: error1,
|
||||||
messageId: tm.id,
|
messageId: tm.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error2) {
|
} 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
|
// Push an explicit error message into history for UI rendering
|
||||||
|
|
@ -269,7 +269,7 @@ export async function* basicPromptConcatHandler(context: AgentHandlerContext) {
|
||||||
await agentInstanceService.saveUserMessage(errorMessageForHistory);
|
await agentInstanceService.saveUserMessage(errorMessageForHistory);
|
||||||
} catch (persistError) {
|
} catch (persistError) {
|
||||||
logger.warn('Failed to persist error message to database', {
|
logger.warn('Failed to persist error message to database', {
|
||||||
error: persistError instanceof Error ? persistError.message : String(persistError),
|
error: persistError,
|
||||||
messageId: errorMessageForHistory.id,
|
messageId: errorMessageForHistory.id,
|
||||||
agentId: context.agent.id,
|
agentId: context.agent.id,
|
||||||
});
|
});
|
||||||
|
|
@ -286,11 +286,8 @@ export async function* basicPromptConcatHandler(context: AgentHandlerContext) {
|
||||||
});
|
});
|
||||||
currentRequestId = undefined;
|
currentRequestId = undefined;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
logger.error('Unexpected error during AI generation', { error });
|
||||||
logger.error('Unexpected error during AI generation', {
|
yield completed(`Unexpected error: ${(error as Error).message}`, context);
|
||||||
error: errorMessage,
|
|
||||||
});
|
|
||||||
yield completed(`Unexpected error: ${errorMessage}`, context);
|
|
||||||
} finally {
|
} finally {
|
||||||
if (context.isCancelled() && currentRequestId) {
|
if (context.isCancelled() && currentRequestId) {
|
||||||
logger.debug('Cancelling AI request in finally block', {
|
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
|
// Start processing with the initial user message
|
||||||
yield* processLLMCall();
|
yield* processLLMCall();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
logger.error('Error processing prompt', {
|
logger.error('Error processing prompt', {
|
||||||
method: 'basicPromptConcatHandler',
|
method: 'basicPromptConcatHandler',
|
||||||
agentId: context.agent.id,
|
agentId: context.agent.id,
|
||||||
error: errorMessage,
|
error,
|
||||||
});
|
});
|
||||||
yield completed(`Error processing prompt: ${errorMessage}`, context);
|
yield completed(`Error processing prompt: ${(error as Error).message}`, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,7 @@ export class AgentInstanceService implements IAgentInstanceService {
|
||||||
await this.initializeDatabase();
|
await this.initializeDatabase();
|
||||||
await this.initializeHandlers();
|
await this.initializeHandlers();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
logger.error('Failed to initialize agent instance service', { error });
|
||||||
logger.error(`Failed to initialize agent instance service: ${errorMessage}`);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -58,8 +57,7 @@ export class AgentInstanceService implements IAgentInstanceService {
|
||||||
this.agentMessageRepository = this.dataSource.getRepository(AgentInstanceMessageEntity);
|
this.agentMessageRepository = this.dataSource.getRepository(AgentInstanceMessageEntity);
|
||||||
logger.debug('AgentInstance repositories initialized');
|
logger.debug('AgentInstance repositories initialized');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
logger.error('Failed to initialize agent instance database', { error });
|
||||||
logger.error(`Failed to initialize agent instance database: ${errorMessage}`);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -74,8 +72,7 @@ export class AgentInstanceService implements IAgentInstanceService {
|
||||||
this.registerBuiltinHandlers();
|
this.registerBuiltinHandlers();
|
||||||
logger.debug('AgentInstance handlers registered');
|
logger.debug('AgentInstance handlers registered');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
logger.error('Failed to initialize agent instance handlers', { error });
|
||||||
logger.error(`Failed to initialize agent instance handlers: ${errorMessage}`);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -163,8 +160,7 @@ export class AgentInstanceService implements IAgentInstanceService {
|
||||||
modified: now,
|
modified: now,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
logger.error('Failed to create agent instance', { error });
|
||||||
logger.error(`Failed to create agent instance: ${errorMessage}`);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -195,8 +191,7 @@ export class AgentInstanceService implements IAgentInstanceService {
|
||||||
messages,
|
messages,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
logger.error('Failed to get agent instance', { error });
|
||||||
logger.error(`Failed to get agent instance: ${errorMessage}`);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -271,8 +266,7 @@ export class AgentInstanceService implements IAgentInstanceService {
|
||||||
|
|
||||||
return updatedAgent;
|
return updatedAgent;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
logger.error('Failed to update agent instance', { error });
|
||||||
logger.error(`Failed to update agent instance: ${errorMessage}`);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -292,8 +286,7 @@ export class AgentInstanceService implements IAgentInstanceService {
|
||||||
|
|
||||||
logger.info(`Deleted agent instance: ${agentId}`);
|
logger.info(`Deleted agent instance: ${agentId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
logger.error('Failed to delete agent instance', { error });
|
||||||
logger.error(`Failed to delete agent instance: ${errorMessage}`);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -337,8 +330,7 @@ export class AgentInstanceService implements IAgentInstanceService {
|
||||||
|
|
||||||
return instances.map(entity => pick(entity, AGENT_INSTANCE_FIELDS));
|
return instances.map(entity => pick(entity, AGENT_INSTANCE_FIELDS));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
logger.error('Failed to get agent instances', { error });
|
||||||
logger.error(`Failed to get agent instances: ${errorMessage}`);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -540,7 +532,7 @@ export class AgentInstanceService implements IAgentInstanceService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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
|
// Remove cancel token from map
|
||||||
|
|
@ -634,7 +626,7 @@ export class AgentInstanceService implements IAgentInstanceService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).catch((error: unknown) => {
|
}).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.getAgent(agentId).then(agent => {
|
||||||
this.agentInstanceSubjects.get(agentId)?.next(agent);
|
this.agentInstanceSubjects.get(agentId)?.next(agent);
|
||||||
}).catch((error: unknown) => {
|
}).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',
|
source: 'saveUserMessage',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
logger.error('Failed to save user message', {
|
||||||
logger.error(`Failed to save user message: ${errorMessage}`, {
|
error,
|
||||||
messageId: userMessage.id,
|
messageId: userMessage.id,
|
||||||
agentId: userMessage.agentId,
|
agentId: userMessage.agentId,
|
||||||
});
|
});
|
||||||
|
|
@ -840,8 +832,7 @@ export class AgentInstanceService implements IAgentInstanceService {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
logger.error('Failed to update/create message content', { error });
|
||||||
logger.error(`Failed to update/create message content: ${errorMessage}`);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
debounceMs,
|
debounceMs,
|
||||||
|
|
@ -891,7 +882,7 @@ export class AgentInstanceService implements IAgentInstanceService {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error in AgentInstanceService.concatPrompt', {
|
logger.error('Error in AgentInstanceService.concatPrompt', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
promptDescriptionId: (promptDescription as AgentPromptDescription).id,
|
promptDescriptionId: (promptDescription as AgentPromptDescription).id,
|
||||||
messagesCount: messages.length,
|
messagesCount: messages.length,
|
||||||
});
|
});
|
||||||
|
|
@ -915,7 +906,7 @@ export class AgentInstanceService implements IAgentInstanceService {
|
||||||
return { type: 'object', properties: {} };
|
return { type: 'object', properties: {} };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error in AgentInstanceService.getHandlerConfigSchema', {
|
logger.error('Error in AgentInstanceService.getHandlerConfigSchema', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
handlerId,
|
handlerId,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
|
||||||
callback();
|
callback();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Message management plugin error in userMessageReceived', {
|
logger.error('Message management plugin error in userMessageReceived', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
messageId: context.messageId,
|
messageId: context.messageId,
|
||||||
agentId: context.handlerContext.agent.id,
|
agentId: context.handlerContext.agent.id,
|
||||||
});
|
});
|
||||||
|
|
@ -79,7 +79,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
|
||||||
callback();
|
callback();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Message management plugin error in agentStatusChanged', {
|
logger.error('Message management plugin error in agentStatusChanged', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
agentId: context.handlerContext.agent.id,
|
agentId: context.handlerContext.agent.id,
|
||||||
status: context.status,
|
status: context.status,
|
||||||
});
|
});
|
||||||
|
|
@ -119,7 +119,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
|
||||||
aiMessage.metadata = { ...aiMessage.metadata, isPersisted: true };
|
aiMessage.metadata = { ...aiMessage.metadata, isPersisted: true };
|
||||||
} catch (persistError) {
|
} catch (persistError) {
|
||||||
logger.warn('Failed to persist initial streaming AI message', {
|
logger.warn('Failed to persist initial streaming AI message', {
|
||||||
error: persistError instanceof Error ? persistError.message : String(persistError),
|
error: persistError,
|
||||||
messageId: aiMessage.id,
|
messageId: aiMessage.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -135,14 +135,14 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
|
||||||
agentInstanceService.debounceUpdateMessage(aiMessage, handlerContext.agent.id);
|
agentInstanceService.debounceUpdateMessage(aiMessage, handlerContext.agent.id);
|
||||||
} catch (serviceError) {
|
} catch (serviceError) {
|
||||||
logger.warn('Failed to update UI for streaming message', {
|
logger.warn('Failed to update UI for streaming message', {
|
||||||
error: serviceError instanceof Error ? serviceError.message : String(serviceError),
|
error: serviceError,
|
||||||
messageId: aiMessage.id,
|
messageId: aiMessage.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Message management plugin error in responseUpdate', {
|
logger.error('Message management plugin error in responseUpdate', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
callback();
|
callback();
|
||||||
|
|
@ -194,7 +194,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
|
||||||
agentInstanceService.debounceUpdateMessage(aiMessage, handlerContext.agent.id);
|
agentInstanceService.debounceUpdateMessage(aiMessage, handlerContext.agent.id);
|
||||||
} catch (serviceError) {
|
} catch (serviceError) {
|
||||||
logger.warn('Failed to update UI for completed message', {
|
logger.warn('Failed to update UI for completed message', {
|
||||||
error: serviceError instanceof Error ? serviceError.message : String(serviceError),
|
error: serviceError,
|
||||||
messageId: aiMessage.id,
|
messageId: aiMessage.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -208,7 +208,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
|
||||||
callback();
|
callback();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Message management plugin error in responseComplete', {
|
logger.error('Message management plugin error in responseComplete', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
});
|
});
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
|
|
@ -245,7 +245,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
|
||||||
});
|
});
|
||||||
} catch (serviceError) {
|
} catch (serviceError) {
|
||||||
logger.error('Failed to persist tool result message', {
|
logger.error('Failed to persist tool result message', {
|
||||||
error: serviceError instanceof Error ? serviceError.message : String(serviceError),
|
error: serviceError,
|
||||||
messageId: message.id,
|
messageId: message.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -261,7 +261,7 @@ export const messageManagementPlugin: PromptConcatPlugin = (hooks) => {
|
||||||
callback();
|
callback();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Message management plugin error in toolExecuted', {
|
logger.error('Message management plugin error in toolExecuted', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
});
|
});
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
|
||||||
callback();
|
callback();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error in wiki operation tool list injection', {
|
logger.error('Error in wiki operation tool list injection', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
pluginId: pluginConfig.id,
|
pluginId: pluginConfig.id,
|
||||||
});
|
});
|
||||||
callback();
|
callback();
|
||||||
|
|
@ -340,7 +340,7 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
|
||||||
toolResultMessage.metadata = { ...toolResultMessage.metadata, isPersisted: true };
|
toolResultMessage.metadata = { ...toolResultMessage.metadata, isPersisted: true };
|
||||||
} catch (persistError) {
|
} catch (persistError) {
|
||||||
logger.warn('Failed to persist tool result immediately in wikiOperationPlugin', {
|
logger.warn('Failed to persist tool result immediately in wikiOperationPlugin', {
|
||||||
error: persistError instanceof Error ? persistError.message : String(persistError),
|
error: persistError,
|
||||||
messageId: toolResultMessage.id,
|
messageId: toolResultMessage.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -369,7 +369,7 @@ export const wikiOperationPlugin: PromptConcatPlugin = (hooks) => {
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Wiki operation tool execution failed', {
|
logger.error('Wiki operation tool execution failed', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
agentId: handlerContext.agent.id,
|
agentId: handlerContext.agent.id,
|
||||||
toolParameters: toolMatch.parameters,
|
toolParameters: toolMatch.parameters,
|
||||||
});
|
});
|
||||||
|
|
@ -425,9 +425,7 @@ Error: ${error instanceof Error ? error.message : String(error)}
|
||||||
|
|
||||||
callback();
|
callback();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error in wiki operation plugin response handler', {
|
logger.error('Error in wiki operation plugin response handler', { error });
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
});
|
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -262,7 +262,7 @@ async function executeWikiSearchTool(
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`Error retrieving full tiddler content for ${result.title}`, {
|
logger.warn(`Error retrieving full tiddler content for ${result.title}`, {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
});
|
});
|
||||||
fullContentResults.push(result);
|
fullContentResults.push(result);
|
||||||
}
|
}
|
||||||
|
|
@ -278,7 +278,7 @@ async function executeWikiSearchTool(
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Vector search failed', {
|
logger.error('Vector search failed', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
workspaceID,
|
workspaceID,
|
||||||
query,
|
query,
|
||||||
});
|
});
|
||||||
|
|
@ -327,7 +327,7 @@ async function executeWikiSearchTool(
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`Error retrieving tiddler content for ${title}`, {
|
logger.warn(`Error retrieving tiddler content for ${title}`, {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
});
|
});
|
||||||
results.push({ title });
|
results.push({ title });
|
||||||
}
|
}
|
||||||
|
|
@ -377,7 +377,7 @@ async function executeWikiSearchTool(
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Wiki search tool execution error', {
|
logger.error('Wiki search tool execution error', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
parameters,
|
parameters,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -471,7 +471,7 @@ async function executeWikiUpdateEmbeddingsTool(
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Wiki update embeddings tool execution error', {
|
logger.error('Wiki update embeddings tool execution error', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
parameters,
|
parameters,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -551,7 +551,7 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
|
||||||
callback();
|
callback();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error in wiki search tool list injection', {
|
logger.error('Error in wiki search tool list injection', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
pluginId: pluginConfig.id,
|
pluginId: pluginConfig.id,
|
||||||
});
|
});
|
||||||
callback();
|
callback();
|
||||||
|
|
@ -608,7 +608,7 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
|
||||||
latestAiMessage.metadata = { ...latestAiMessage.metadata, isPersisted: true };
|
latestAiMessage.metadata = { ...latestAiMessage.metadata, isPersisted: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('Failed to persist AI message containing tool call immediately', {
|
logger.warn('Failed to persist AI message containing tool call immediately', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
messageId: latestAiMessage.id,
|
messageId: latestAiMessage.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -745,7 +745,7 @@ export const wikiSearchPlugin: PromptConcatPlugin = (hooks) => {
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Wiki search tool execution failed', {
|
logger.error('Wiki search tool execution failed', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
toolCall: toolMatch,
|
toolCall: toolMatch,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -802,9 +802,7 @@ Error: ${error instanceof Error ? error.message : String(error)}
|
||||||
|
|
||||||
callback();
|
callback();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error in wiki search handler plugin', {
|
logger.error('Error in wiki search handler plugin', { error });
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
});
|
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ export const workspacesListPlugin: PromptConcatPlugin = (hooks) => {
|
||||||
callback();
|
callback();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error in workspaces list injection', {
|
logger.error('Error in workspaces list injection', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error,
|
||||||
pluginId: pluginConfig.id,
|
pluginId: pluginConfig.id,
|
||||||
});
|
});
|
||||||
callback();
|
callback();
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export interface IPaths {
|
||||||
LOCALIZATION_FOLDER: string;
|
LOCALIZATION_FOLDER: string;
|
||||||
LOG_FOLDER: string;
|
LOG_FOLDER: string;
|
||||||
MAIN_WINDOW_WEBPACK_ENTRY: string;
|
MAIN_WINDOW_WEBPACK_ENTRY: string;
|
||||||
MENUBAR_ICON_PATH: string;
|
TIDGI_MINI_WINDOW_ICON_PATH: string;
|
||||||
SETTINGS_FOLDER: string;
|
SETTINGS_FOLDER: string;
|
||||||
TIDDLERS_PATH: string;
|
TIDDLERS_PATH: string;
|
||||||
TIDDLYWIKI_TEMPLATE_FOLDER_PATH: string;
|
TIDDLYWIKI_TEMPLATE_FOLDER_PATH: string;
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,7 @@ function fixEmptyAndErrorSettingFileOnStartUp() {
|
||||||
fs.writeJSONSync(settings.file(), {});
|
fs.writeJSONSync(settings.file(), {});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const error_ = error instanceof Error ? error : new Error(String(error));
|
logger.error('Error when checking Setting file format', { function: 'fixEmptyAndErrorSettingFileOnStartUp', error });
|
||||||
logger.error('Error when checking Setting file format', { function: 'fixEmptyAndErrorSettingFileOnStartUp', error: error_.message, stack: error_.stack ?? '' });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,8 +43,8 @@ export function fixSettingFileWhenError(jsonError: Error, providedJSONContent?:
|
||||||
fs.writeJSONSync(settings.file(), repaired);
|
fs.writeJSONSync(settings.file(), repaired);
|
||||||
logger.info('Fix JSON content done, saved', { repaired });
|
logger.info('Fix JSON content done, saved', { repaired });
|
||||||
} catch (fixJSONError) {
|
} catch (fixJSONError) {
|
||||||
const fixError = fixJSONError instanceof Error ? fixJSONError : new Error(String(fixJSONError));
|
const fixError = fixJSONError as Error;
|
||||||
logger.error('Setting file format bad, and cannot be fixed', { function: 'fixSettingFileWhenError', error: fixError.message, stack: fixError.stack ?? '', jsonContent });
|
logger.error('Setting file format bad, and cannot be fixed', { function: 'fixSettingFileWhenError', error: fixError, jsonContent });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,7 +55,6 @@ try {
|
||||||
atomicSave: !isWin,
|
atomicSave: !isWin,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const error_ = error instanceof Error ? error : new Error(String(error));
|
logger.error('Error when configuring settings', { function: 'settings.configure', error });
|
||||||
logger.error('Error when configuring settings', { function: 'settings.configure', error: error_.message, stack: error_.stack ?? '' });
|
|
||||||
}
|
}
|
||||||
fixEmptyAndErrorSettingFileOnStartUp();
|
fixEmptyAndErrorSettingFileOnStartUp();
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,9 @@ export class DatabaseService implements IDatabaseService {
|
||||||
logger.info('loaded settings', {
|
logger.info('loaded settings', {
|
||||||
hasContent: !!this.settingFileContent,
|
hasContent: !!this.settingFileContent,
|
||||||
keys: this.settingFileContent ? Object.keys(this.settingFileContent).length : 0,
|
keys: this.settingFileContent ? Object.keys(this.settingFileContent).length : 0,
|
||||||
|
hasPreferences: !!this.settingFileContent?.preferences,
|
||||||
|
tidgiMiniWindow: this.settingFileContent?.preferences?.tidgiMiniWindow,
|
||||||
|
settingsFilePath: settings.file(),
|
||||||
function: 'DatabaseService.initializeForApp',
|
function: 'DatabaseService.initializeForApp',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -64,8 +67,7 @@ export class DatabaseService implements IDatabaseService {
|
||||||
path: settings.file().replace(/settings\.json$/, ''),
|
path: settings.file().replace(/settings\.json$/, ''),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const error_ = error instanceof Error ? error : new Error(String(error));
|
logger.error('Error initializing setting backup file', { function: 'DatabaseService.initializeForApp', error });
|
||||||
logger.error('Error initializing setting backup file', { function: 'DatabaseService.initializeForApp', error: error_.message, stack: error_.stack ?? '' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure database folder exists
|
// Ensure database folder exists
|
||||||
|
|
@ -171,7 +173,7 @@ export class DatabaseService implements IDatabaseService {
|
||||||
await this.loadSqliteVecExtension(dataSource);
|
await this.loadSqliteVecExtension(dataSource);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`sqlite-vec extension failed to load during initialization for key: ${key}, continuing without vector search functionality`, {
|
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
|
// Don't throw - allow the database to work without vector functionality
|
||||||
}
|
}
|
||||||
|
|
@ -184,7 +186,7 @@ export class DatabaseService implements IDatabaseService {
|
||||||
await dataSource.destroy();
|
await dataSource.destroy();
|
||||||
logger.info(`Database initialized for key: ${key}`);
|
logger.info(`Database initialized for key: ${key}`);
|
||||||
} catch (error) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -216,7 +218,7 @@ export class DatabaseService implements IDatabaseService {
|
||||||
await this.loadSqliteVecExtension(dataSource);
|
await this.loadSqliteVecExtension(dataSource);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`sqlite-vec extension failed to load for key: ${key}, continuing without vector search functionality`, {
|
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;
|
return dataSource;
|
||||||
} catch (error) {
|
} 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) {
|
if (!isRetry) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -234,7 +236,7 @@ export class DatabaseService implements IDatabaseService {
|
||||||
await this.fixDatabaseLock(key);
|
await this.fixDatabaseLock(key);
|
||||||
return await this.getDatabase(key, {}, true);
|
return await this.getDatabase(key, {}, true);
|
||||||
} catch (retryError) {
|
} 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();
|
await this.dataSources.get(key)?.destroy();
|
||||||
this.dataSources.delete(key);
|
this.dataSources.delete(key);
|
||||||
} catch (closeError) {
|
} 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;
|
throw error;
|
||||||
|
|
@ -267,7 +269,7 @@ export class DatabaseService implements IDatabaseService {
|
||||||
const stat = await fs.stat(databasePath);
|
const stat = await fs.stat(databasePath);
|
||||||
return { exists: true, size: stat.size };
|
return { exists: true, size: stat.size };
|
||||||
} catch (error) {
|
} 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 };
|
return { exists: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -289,7 +291,7 @@ export class DatabaseService implements IDatabaseService {
|
||||||
try {
|
try {
|
||||||
await this.dataSources.get(key)?.destroy();
|
await this.dataSources.get(key)?.destroy();
|
||||||
} catch (error) {
|
} 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);
|
this.dataSources.delete(key);
|
||||||
}
|
}
|
||||||
|
|
@ -300,7 +302,7 @@ export class DatabaseService implements IDatabaseService {
|
||||||
logger.info(`Database file deleted for key: ${key}`);
|
logger.info(`Database file deleted for key: ${key}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`deleteDatabase failed for key: ${key}`, { error: (error as Error).message });
|
logger.error(`deleteDatabase failed for key: ${key}`, { error });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -323,7 +325,7 @@ export class DatabaseService implements IDatabaseService {
|
||||||
logger.info(`Database connection closed for key: ${key}`);
|
logger.info(`Database connection closed for key: ${key}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -367,7 +369,7 @@ export class DatabaseService implements IDatabaseService {
|
||||||
await fs.unlink(temporaryPath);
|
await fs.unlink(temporaryPath);
|
||||||
logger.info(`Fixed database lock for key: ${key}`);
|
logger.info(`Fixed database lock for key: ${key}`);
|
||||||
} catch (error) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -413,8 +415,7 @@ export class DatabaseService implements IDatabaseService {
|
||||||
// based on the dimensions needed
|
// based on the dimensions needed
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to load sqlite-vec extension:', {
|
logger.error('Failed to load sqlite-vec extension:', {
|
||||||
error: (error as Error).message,
|
error,
|
||||||
stack: (error as Error).stack,
|
|
||||||
sqliteVecAvailable: typeof sqliteVec !== 'undefined',
|
sqliteVecAvailable: typeof sqliteVec !== 'undefined',
|
||||||
});
|
});
|
||||||
throw new Error(`sqlite-vec extension failed to load: ${(error as Error).message}`);
|
throw new Error(`sqlite-vec extension failed to load: ${(error as Error).message}`);
|
||||||
|
|
|
||||||
|
|
@ -57,11 +57,9 @@ export class Git implements IGitService {
|
||||||
this.gitWorker = createWorkerProxy<GitWorker>(worker);
|
this.gitWorker = createWorkerProxy<GitWorker>(worker);
|
||||||
logger.debug('gitWorker initialized successfully', { function: 'Git.initWorker' });
|
logger.debug('gitWorker initialized successfully', { function: 'Git.initWorker' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const error_ = error instanceof Error ? error : new Error(String(error));
|
|
||||||
logger.error('Failed to initialize gitWorker', {
|
logger.error('Failed to initialize gitWorker', {
|
||||||
function: 'Git.initWorker',
|
function: 'Git.initWorker',
|
||||||
error: error_.message,
|
error,
|
||||||
errorObj: error_,
|
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -178,8 +176,8 @@ export class Git implements IGitService {
|
||||||
await this.nativeService.openInGitGuiApp(wikiFolderPath);
|
await this.nativeService.openInGitGuiApp(wikiFolderPath);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((_error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
logger.error('createFailedDialog failed', _error instanceof Error ? _error : new Error(String(_error)));
|
logger.error('createFailedDialog failed', { error });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -205,14 +203,14 @@ export class Git implements IGitService {
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
await this.updateGitInfoTiddler(workspace, configs.remoteUrl, configs.userInfo?.branch);
|
await this.updateGitInfoTiddler(workspace, configs.remoteUrl, configs.userInfo?.branch);
|
||||||
} catch (_error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error('updateGitInfoTiddler failed when commitAndSync', _error instanceof Error ? _error : new Error(String(_error)));
|
logger.error('updateGitInfoTiddler failed when commitAndSync', { error });
|
||||||
}
|
}
|
||||||
const observable = this.gitWorker?.commitAndSyncWiki(workspace, configs, getErrorMessageI18NDict());
|
const observable = this.gitWorker?.commitAndSyncWiki(workspace, configs, getErrorMessageI18NDict());
|
||||||
return await this.getHasChangeHandler(observable, workspace.wikiFolderLocation, workspaceIDToShowNotification);
|
return await this.getHasChangeHandler(observable, workspace.wikiFolderLocation, workspaceIDToShowNotification);
|
||||||
} catch (_error: unknown) {
|
} catch (error: unknown) {
|
||||||
const error = _error instanceof Error ? _error : new Error(String(_error));
|
const error_ = error as Error;
|
||||||
this.createFailedNotification(error.message, workspaceIDToShowNotification);
|
this.createFailedNotification(error_.message, workspaceIDToShowNotification);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ export default async function getViewBounds(
|
||||||
): Promise<{ height: number; width: number; x: number; y: number }> {
|
): Promise<{ height: number; width: number; x: number; y: number }> {
|
||||||
const { findInPage = false, windowName } = config;
|
const { findInPage = false, windowName } = config;
|
||||||
const preferencesService = container.get<IPreferenceService>(serviceIdentifier.Preference);
|
const preferencesService = container.get<IPreferenceService>(serviceIdentifier.Preference);
|
||||||
const [sidebar, sidebarOnMenubar] = await Promise.all([preferencesService.get('sidebar'), preferencesService.get('sidebarOnMenubar')]);
|
const [sidebar, tidgiMiniWindowShowSidebar] = await Promise.all([preferencesService.get('sidebar'), preferencesService.get('tidgiMiniWindowShowSidebar')]);
|
||||||
const showSidebar = windowName === WindowNames.menuBar ? sidebarOnMenubar : sidebar;
|
const showSidebar = windowName === WindowNames.tidgiMiniWindow ? tidgiMiniWindowShowSidebar : sidebar;
|
||||||
// Now showing sidebar on secondary window
|
// Now showing sidebar on secondary window
|
||||||
const secondary = windowName === WindowNames.secondary;
|
const secondary = windowName === WindowNames.secondary;
|
||||||
const x = (showSidebar && !secondary) ? 68 : 0;
|
const x = (showSidebar && !secondary) ? 68 : 0;
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ export class Backend implements BackendModule {
|
||||||
try {
|
try {
|
||||||
result = JSON.parse(payload.data ?? 'null');
|
result = JSON.parse(payload.data ?? 'null');
|
||||||
} catch (parseError) {
|
} 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)}'.`;
|
parseError_.message = `Error parsing '${String(payload.filename)}'. Message: '${String(parseError)}'.`;
|
||||||
const entry = this.readCallbacks[payload.key];
|
const entry = this.readCallbacks[payload.key];
|
||||||
const callback__ = entry?.callback;
|
const callback__ = entry?.callback;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,30 @@
|
||||||
import { LOG_FOLDER } from '@/constants/appPaths';
|
import { LOG_FOLDER } from '@/constants/appPaths';
|
||||||
import winston, { format } from 'winston';
|
import winston, { format } from 'winston';
|
||||||
import 'winston-daily-rotate-file';
|
import 'winston-daily-rotate-file';
|
||||||
|
import type { TransformableInfo } from 'logform';
|
||||||
|
import { serializeError } from 'serialize-error';
|
||||||
import RendererTransport from './rendererTransport';
|
import RendererTransport from './rendererTransport';
|
||||||
|
|
||||||
export * from './wikiOutput';
|
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<string, unknown>;
|
||||||
|
|
||||||
|
// 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({
|
const logger = winston.createLogger({
|
||||||
transports: [
|
transports: [
|
||||||
|
|
@ -19,7 +40,7 @@ const logger = winston.createLogger({
|
||||||
}),
|
}),
|
||||||
new RendererTransport(),
|
new RendererTransport(),
|
||||||
],
|
],
|
||||||
format: format.combine(format.timestamp(), format.json()),
|
format: format.combine(errorSerializer(), format.timestamp(), format.json()),
|
||||||
});
|
});
|
||||||
export { logger };
|
export { logger };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@ export function getUrlWithCorrectProtocol(workspace: IWorkspace, originalUrl: st
|
||||||
return parsedUrl.toString();
|
return parsedUrl.toString();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Failed to getUrlWithCorrectProtocol for originalUrl ${originalUrl}, fallback to originalUrl. Error: ${(error as Error).message}`,
|
'Failed to getUrlWithCorrectProtocol for originalUrl, fallback to originalUrl',
|
||||||
{ isHttps },
|
{ isHttps, error },
|
||||||
);
|
);
|
||||||
return originalUrl;
|
return originalUrl;
|
||||||
}
|
}
|
||||||
|
|
@ -42,8 +42,10 @@ export function replaceUrlPortWithSettingPort(originalUrl: string, newPort: numb
|
||||||
parsedUrl.port = String(newPort);
|
parsedUrl.port = String(newPort);
|
||||||
return parsedUrl.toString();
|
return parsedUrl.toString();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const error_ = error as Error;
|
||||||
logger.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;
|
return originalUrl;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,8 +86,7 @@ export class MenuService implements IMenuService {
|
||||||
Menu.setApplicationMenu(menu);
|
Menu.setApplicationMenu(menu);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('buildMenu failed', {
|
logger.error('buildMenu failed', {
|
||||||
message: (error as Error).message,
|
error,
|
||||||
stack: (error as Error).stack ?? '',
|
|
||||||
function: 'buildMenu',
|
function: 'buildMenu',
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -139,9 +139,9 @@ async function findApplication(editor: IDarwinExternalEditor): Promise<string |
|
||||||
// app-path not finding the app isn't an error, it just means the
|
// app-path not finding the app isn't an error, it just means the
|
||||||
// bundle isn't registered on the machine.
|
// bundle isn't registered on the machine.
|
||||||
// https://github.com/sindresorhus/app-path/blob/0e776d4e132676976b4a64e09b5e5a4c6e99fcba/index.js#L7-L13
|
// https://github.com/sindresorhus/app-path/blob/0e776d4e132676976b4a64e09b5e5a4c6e99fcba/index.js#L7-L13
|
||||||
const installPath = await appPath(identifier).catch(async (_error: unknown) => {
|
const installPath = await appPath(identifier).catch(async (error_: unknown) => {
|
||||||
const error = _error instanceof Error ? _error : new Error(String(_error));
|
const error = error_ as Error;
|
||||||
logger.info('gets appPath Error', { error: error.message ?? String(error), function: 'darwin.findApplication' });
|
logger.info('gets appPath Error', { error, function: 'darwin.findApplication' });
|
||||||
if (error.message === "Couldn't find the app") {
|
if (error.message === "Couldn't find the app") {
|
||||||
return await Promise.resolve(null);
|
return await Promise.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
@ -166,9 +166,9 @@ async function findApplication(editor: IDarwinExternalEditor): Promise<string |
|
||||||
installPath,
|
installPath,
|
||||||
function: 'darwin.findApplication',
|
function: 'darwin.findApplication',
|
||||||
});
|
});
|
||||||
} catch (_error: unknown) {
|
} catch (error_: unknown) {
|
||||||
const error = _error instanceof Error ? _error : new Error(String(_error));
|
const error = error_ as Error;
|
||||||
logger.info('unable to locate installation', { editorName: editor.name, error: error.message, function: 'darwin.findApplication' });
|
logger.info('unable to locate installation', { editorName: editor.name, error, function: 'darwin.findApplication' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { app, dialog, ipcMain, MessageBoxOptions, shell } from 'electron';
|
import { app, dialog, globalShortcut, ipcMain, MessageBoxOptions, shell } from 'electron';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from 'inversify';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
@ -10,6 +10,7 @@ import { githubDesktopUrl } from '@/constants/urls';
|
||||||
import { container } from '@services/container';
|
import { container } from '@services/container';
|
||||||
import { logger } from '@services/libs/log';
|
import { logger } from '@services/libs/log';
|
||||||
import { getLocalHostUrlWithActualIP, getUrlWithCorrectProtocol, replaceUrlPortWithSettingPort } from '@services/libs/url';
|
import { getLocalHostUrlWithActualIP, getUrlWithCorrectProtocol, replaceUrlPortWithSettingPort } from '@services/libs/url';
|
||||||
|
import type { IPreferenceService } from '@services/preferences/interface';
|
||||||
import serviceIdentifier from '@services/serviceIdentifier';
|
import serviceIdentifier from '@services/serviceIdentifier';
|
||||||
import type { IWikiService } from '@services/wiki/interface';
|
import type { IWikiService } from '@services/wiki/interface';
|
||||||
import { ZxWorkerControlActions } from '@services/wiki/interface';
|
import { ZxWorkerControlActions } from '@services/wiki/interface';
|
||||||
|
|
@ -22,20 +23,112 @@ import i18next from 'i18next';
|
||||||
import { ZxNotInitializedError } from './error';
|
import { ZxNotInitializedError } from './error';
|
||||||
import { findEditorOrDefault, findGitGUIAppOrDefault, launchExternalEditor } from './externalApp';
|
import { findEditorOrDefault, findGitGUIAppOrDefault, launchExternalEditor } from './externalApp';
|
||||||
import type { INativeService, IPickDirectoryOptions } from './interface';
|
import type { INativeService, IPickDirectoryOptions } from './interface';
|
||||||
|
import { getShortcutCallback, registerShortcutByKey } from './keyboardShortcutHelpers';
|
||||||
import { reportErrorToGithubWithTemplates } from './reportError';
|
import { reportErrorToGithubWithTemplates } from './reportError';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class NativeService implements INativeService {
|
export class NativeService implements INativeService {
|
||||||
constructor(@inject(serviceIdentifier.Window) private readonly windowService: IWindowService) {
|
constructor(
|
||||||
|
@inject(serviceIdentifier.Window) private readonly windowService: IWindowService,
|
||||||
|
@inject(serviceIdentifier.Preference) private readonly preferenceService: IPreferenceService,
|
||||||
|
) {
|
||||||
this.setupIpcHandlers();
|
this.setupIpcHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
setupIpcHandlers(): void {
|
public setupIpcHandlers(): void {
|
||||||
ipcMain.on(NativeChannel.showElectronMessageBoxSync, (event, options: MessageBoxOptions, windowName: WindowNames = WindowNames.main) => {
|
ipcMain.on(NativeChannel.showElectronMessageBoxSync, (event, options: MessageBoxOptions, windowName: WindowNames = WindowNames.main) => {
|
||||||
event.returnValue = this.showElectronMessageBoxSync(options, windowName);
|
event.returnValue = this.showElectronMessageBoxSync(options, windowName);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async initialize(): Promise<void> {
|
||||||
|
await this.initializeKeyboardShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initializeKeyboardShortcuts(): Promise<void> {
|
||||||
|
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<T>(serviceName: keyof typeof serviceIdentifier, methodName: keyof T, shortcut: string): Promise<void> {
|
||||||
|
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<IPreferenceService>(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<T>(serviceName: keyof typeof serviceIdentifier, methodName: keyof T): Promise<void> {
|
||||||
|
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<IPreferenceService>(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<Record<string, string>> {
|
||||||
|
const preferences = this.preferenceService.getPreferences();
|
||||||
|
return preferences.keyboardShortcuts || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async executeShortcutCallback(key: string): Promise<void> {
|
||||||
|
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<boolean> {
|
public async openInEditor(filePath: string, editorName?: string): Promise<boolean> {
|
||||||
// TODO: open vscode by default to speed up, support choose favorite editor later
|
// TODO: open vscode by default to speed up, support choose favorite editor later
|
||||||
let defaultEditor = await findEditorOrDefault('Visual Studio Code').catch(() => {});
|
let defaultEditor = await findEditorOrDefault('Visual Studio Code').catch(() => {});
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { MessageBoxOptions } from 'electron';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import { NativeChannel } from '@/constants/channels';
|
import { NativeChannel } from '@/constants/channels';
|
||||||
|
import serviceIdentifier from '@services/serviceIdentifier';
|
||||||
import type { IZxFileInput } from '@services/wiki/wikiWorker';
|
import type { IZxFileInput } from '@services/wiki/wikiWorker';
|
||||||
import { WindowNames } from '@services/windows/WindowProperties';
|
import { WindowNames } from '@services/windows/WindowProperties';
|
||||||
import { ProxyPropertyType } from 'electron-ipc-cat/common';
|
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
|
* Wrap call to electron api, so we won't need remote module in renderer process
|
||||||
*/
|
*/
|
||||||
export interface INativeService {
|
export interface INativeService {
|
||||||
|
/**
|
||||||
|
* Initialize the native service
|
||||||
|
* This should be called during app startup
|
||||||
|
*/
|
||||||
|
initialize(): Promise<void>;
|
||||||
|
/**
|
||||||
|
* 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<T>(serviceName: keyof typeof serviceIdentifier, methodName: keyof T, shortcut: string): Promise<void>;
|
||||||
|
/**
|
||||||
|
* 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<T>(serviceName: keyof typeof serviceIdentifier, methodName: keyof T): Promise<void>;
|
||||||
|
/**
|
||||||
|
* 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<Record<string, string>>;
|
||||||
|
/**
|
||||||
|
* 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<void>;
|
||||||
/**
|
/**
|
||||||
* Copy a file or directory. The directory can have contents.
|
* 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).
|
* @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 = {
|
export const NativeServiceIPCDescriptor = {
|
||||||
channel: NativeChannel.name,
|
channel: NativeChannel.name,
|
||||||
properties: {
|
properties: {
|
||||||
|
initialize: ProxyPropertyType.Function,
|
||||||
|
initializeKeyboardShortcuts: ProxyPropertyType.Function,
|
||||||
|
registerKeyboardShortcut: ProxyPropertyType.Function,
|
||||||
|
unregisterKeyboardShortcut: ProxyPropertyType.Function,
|
||||||
|
getKeyboardShortcuts: ProxyPropertyType.Function,
|
||||||
|
executeShortcutCallback: ProxyPropertyType.Function,
|
||||||
copyPath: ProxyPropertyType.Function,
|
copyPath: ProxyPropertyType.Function,
|
||||||
executeZxScript$: ProxyPropertyType.Function$,
|
executeZxScript$: ProxyPropertyType.Function$,
|
||||||
formatFileUrlToAbsolutePath: ProxyPropertyType.Function,
|
formatFileUrlToAbsolutePath: ProxyPropertyType.Function,
|
||||||
|
|
|
||||||
106
src/services/native/keyboardShortcutHelpers.ts
Normal file
106
src/services/native/keyboardShortcutHelpers.ts
Normal file
|
|
@ -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<void>) | 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<string, symbol>)[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<Record<string, (...arguments_: unknown[]) => 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<void> {
|
||||||
|
// 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -63,10 +63,10 @@ export function reportErrorToGithubWithTemplates(error: Error): void {
|
||||||
const nativeService = container.get<INativeService>(serviceIdentifier.NativeService);
|
const nativeService = container.get<INativeService>(serviceIdentifier.NativeService);
|
||||||
return nativeService.openPath(LOG_FOLDER, true);
|
return nativeService.openPath(LOG_FOLDER, true);
|
||||||
})
|
})
|
||||||
.catch(async (_error: unknown) => {
|
.catch(async (error_: unknown) => {
|
||||||
const error = _error instanceof Error ? _error : new Error(String(_error));
|
const error = error_ as Error;
|
||||||
await import('@services/libs/log').then(({ logger }) => {
|
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({
|
openNewGitHubIssue({
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ export const defaultPreferences: IPreferences = {
|
||||||
allowPrerelease: Boolean(semver.prerelease(app.getVersion())),
|
allowPrerelease: Boolean(semver.prerelease(app.getVersion())),
|
||||||
alwaysOnTop: false,
|
alwaysOnTop: false,
|
||||||
askForDownloadPath: true,
|
askForDownloadPath: true,
|
||||||
attachToMenubar: false,
|
|
||||||
disableAntiAntiLeech: false,
|
disableAntiAntiLeech: false,
|
||||||
disableAntiAntiLeechForUrls: [],
|
disableAntiAntiLeechForUrls: [],
|
||||||
downloadPath: DEFAULT_DOWNLOADS_PATH,
|
downloadPath: DEFAULT_DOWNLOADS_PATH,
|
||||||
|
|
@ -15,8 +14,8 @@ export const defaultPreferences: IPreferences = {
|
||||||
hibernateUnusedWorkspacesAtLaunch: false,
|
hibernateUnusedWorkspacesAtLaunch: false,
|
||||||
hideMenuBar: false,
|
hideMenuBar: false,
|
||||||
ignoreCertificateErrors: false,
|
ignoreCertificateErrors: false,
|
||||||
|
keyboardShortcuts: {},
|
||||||
language: 'zh-Hans',
|
language: 'zh-Hans',
|
||||||
menuBarAlwaysOnTop: false,
|
|
||||||
pauseNotifications: '',
|
pauseNotifications: '',
|
||||||
pauseNotificationsBySchedule: false,
|
pauseNotificationsBySchedule: false,
|
||||||
pauseNotificationsByScheduleFrom: getDefaultPauseNotificationsByScheduleFrom(),
|
pauseNotificationsByScheduleFrom: getDefaultPauseNotificationsByScheduleFrom(),
|
||||||
|
|
@ -28,7 +27,6 @@ export const defaultPreferences: IPreferences = {
|
||||||
showSideBarIcon: true,
|
showSideBarIcon: true,
|
||||||
showSideBarText: true,
|
showSideBarText: true,
|
||||||
sidebar: true,
|
sidebar: true,
|
||||||
sidebarOnMenubar: false,
|
|
||||||
spellcheck: true,
|
spellcheck: true,
|
||||||
spellcheckLanguages: ['en-US'],
|
spellcheckLanguages: ['en-US'],
|
||||||
swipeToNavigate: true,
|
swipeToNavigate: true,
|
||||||
|
|
@ -36,6 +34,12 @@ export const defaultPreferences: IPreferences = {
|
||||||
syncDebounceInterval: 1000 * 60 * 30,
|
syncDebounceInterval: 1000 * 60 * 30,
|
||||||
syncOnlyWhenNoDraft: true,
|
syncOnlyWhenNoDraft: true,
|
||||||
themeSource: 'system' as 'system' | 'light' | 'dark',
|
themeSource: 'system' as 'system' | 'light' | 'dark',
|
||||||
|
tidgiMiniWindow: false,
|
||||||
|
tidgiMiniWindowAlwaysOnTop: false,
|
||||||
|
tidgiMiniWindowFixedWorkspaceId: '',
|
||||||
|
tidgiMiniWindowShowSidebar: false,
|
||||||
|
tidgiMiniWindowShowTitleBar: true,
|
||||||
|
tidgiMiniWindowSyncWorkspaceWithMainWindow: true,
|
||||||
titleBar: true,
|
titleBar: true,
|
||||||
unreadCountBadge: true,
|
unreadCountBadge: true,
|
||||||
useHardwareAcceleration: true,
|
useHardwareAcceleration: true,
|
||||||
|
|
|
||||||
|
|
@ -87,15 +87,22 @@ export class Preference implements IPreferenceService {
|
||||||
const notificationService = container.get<INotificationService>(serviceIdentifier.NotificationService);
|
const notificationService = container.get<INotificationService>(serviceIdentifier.NotificationService);
|
||||||
await notificationService.updatePauseNotificationsInfo();
|
await notificationService.updatePauseNotificationsInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delegate window-related preference changes to WindowService
|
||||||
|
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
|
||||||
|
await windowService.reactWhenPreferencesChanged(key, value);
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'themeSource': {
|
case 'themeSource': {
|
||||||
nativeTheme.themeSource = value as IPreferences['themeSource'];
|
nativeTheme.themeSource = value as IPreferences['themeSource'];
|
||||||
break;
|
return;
|
||||||
}
|
}
|
||||||
case 'language': {
|
case 'language': {
|
||||||
await requestChangeLanguage(value as string);
|
await requestChangeLanguage(value as string);
|
||||||
break;
|
return;
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export interface IPreferences {
|
||||||
allowPrerelease: boolean;
|
allowPrerelease: boolean;
|
||||||
alwaysOnTop: boolean;
|
alwaysOnTop: boolean;
|
||||||
askForDownloadPath: boolean;
|
askForDownloadPath: boolean;
|
||||||
attachToMenubar: boolean;
|
tidgiMiniWindow: boolean;
|
||||||
/**
|
/**
|
||||||
* 完全关闭反盗链
|
* 完全关闭反盗链
|
||||||
*/
|
*/
|
||||||
|
|
@ -26,7 +26,7 @@ export interface IPreferences {
|
||||||
hideMenuBar: boolean;
|
hideMenuBar: boolean;
|
||||||
ignoreCertificateErrors: boolean;
|
ignoreCertificateErrors: boolean;
|
||||||
language: string;
|
language: string;
|
||||||
menuBarAlwaysOnTop: boolean;
|
tidgiMiniWindowAlwaysOnTop: boolean;
|
||||||
pauseNotifications: string | undefined;
|
pauseNotifications: string | undefined;
|
||||||
pauseNotificationsBySchedule: boolean;
|
pauseNotificationsBySchedule: boolean;
|
||||||
pauseNotificationsByScheduleFrom: string;
|
pauseNotificationsByScheduleFrom: string;
|
||||||
|
|
@ -42,12 +42,28 @@ export interface IPreferences {
|
||||||
*/
|
*/
|
||||||
sidebar: boolean;
|
sidebar: boolean;
|
||||||
/**
|
/**
|
||||||
* Should show sidebar on menubar window?
|
* Should show sidebar on tidgi mini window?
|
||||||
*/
|
*/
|
||||||
sidebarOnMenubar: boolean;
|
tidgiMiniWindowShowSidebar: boolean;
|
||||||
spellcheck: boolean;
|
spellcheck: boolean;
|
||||||
spellcheckLanguages: HunspellLanguages[];
|
spellcheckLanguages: HunspellLanguages[];
|
||||||
swipeToNavigate: boolean;
|
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<string, string>;
|
||||||
syncBeforeShutdown: boolean;
|
syncBeforeShutdown: boolean;
|
||||||
syncDebounceInterval: number;
|
syncDebounceInterval: number;
|
||||||
/**
|
/**
|
||||||
|
|
@ -66,6 +82,7 @@ export enum PreferenceSections {
|
||||||
friendLinks = 'friendLinks',
|
friendLinks = 'friendLinks',
|
||||||
general = 'general',
|
general = 'general',
|
||||||
languages = 'languages',
|
languages = 'languages',
|
||||||
|
tidgiMiniWindow = 'tidgiMiniWindow',
|
||||||
misc = 'misc',
|
misc = 'misc',
|
||||||
network = 'network',
|
network = 'network',
|
||||||
notifications = 'notifications',
|
notifications = 'notifications',
|
||||||
|
|
|
||||||
|
|
@ -109,11 +109,8 @@ export class Sync implements ISyncService {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
const error_ = error as Error;
|
||||||
`${(error as Error).message} when checking draft titles. ${
|
logger.error('Error when checking draft titles', { error: error_, function: 'checkCanSyncDueToNoDraft' });
|
||||||
(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.)`,
|
|
||||||
);
|
|
||||||
// when app is on background, might have no draft, because user won't edit it. So just return true
|
// when app is on background, might have no draft, because user won't edit it. So just return true
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,10 +55,11 @@ export function handleNewWindow(
|
||||||
// open external url in browser
|
// open external url in browser
|
||||||
if (nextDomain !== undefined && (disposition === 'foreground-tab' || disposition === 'background-tab')) {
|
if (nextDomain !== undefined && (disposition === 'foreground-tab' || disposition === 'background-tab')) {
|
||||||
logger.debug('openExternal', { nextUrl, nextDomain, disposition, function: 'handleNewWindow' });
|
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(
|
logger.error(
|
||||||
`handleNewWindow() openExternal error ${_error instanceof Error ? _error.message : String(_error)}`,
|
`handleNewWindow() openExternal error ${error.message}`,
|
||||||
_error instanceof Error ? _error : new Error(String(_error)),
|
{ error },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { logger } from '@services/libs/log';
|
||||||
import type { INativeService } from '@services/native/interface';
|
import type { INativeService } from '@services/native/interface';
|
||||||
import { type IBrowserViewMetaData, WindowNames } from '@services/windows/WindowProperties';
|
import { type IBrowserViewMetaData, WindowNames } from '@services/windows/WindowProperties';
|
||||||
import { isWikiWorkspace, type IWorkspace } from '@services/workspaces/interface';
|
import { isWikiWorkspace, type IWorkspace } from '@services/workspaces/interface';
|
||||||
|
import { getTidgiMiniWindowTargetWorkspace } from '@services/workspacesView/utilities';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { setViewEventName } from './constants';
|
import { setViewEventName } from './constants';
|
||||||
import { ViewLoadUrlError } from './error';
|
import { ViewLoadUrlError } from './error';
|
||||||
|
|
@ -247,7 +248,7 @@ export class View implements IViewService {
|
||||||
};
|
};
|
||||||
const checkNotExistResult = await Promise.all([
|
const checkNotExistResult = await Promise.all([
|
||||||
checkNotExist(workspace, WindowNames.main),
|
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);
|
return checkNotExistResult.every((result) => !result);
|
||||||
}
|
}
|
||||||
|
|
@ -312,7 +313,11 @@ export class View implements IViewService {
|
||||||
if (this.shouldMuteAudio !== undefined) {
|
if (this.shouldMuteAudio !== undefined) {
|
||||||
view.webContents.audioMuted = this.shouldMuteAudio;
|
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);
|
browserWindow.contentView.addChildView(view);
|
||||||
const contentSize = browserWindow.getContentSize();
|
const contentSize = browserWindow.getContentSize();
|
||||||
const newViewBounds = await getViewBounds(contentSize as [number, number], { windowName });
|
const newViewBounds = await getViewBounds(contentSize as [number, number], { windowName });
|
||||||
|
|
@ -326,7 +331,7 @@ export class View implements IViewService {
|
||||||
if (updatedWorkspace === undefined) return;
|
if (updatedWorkspace === undefined) return;
|
||||||
// Prevent update non-active (hiding) wiki workspace, so it won't pop up to cover other active agent workspace
|
// 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 (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 contentSize = browserWindow.getContentSize();
|
||||||
const newViewBounds = await getViewBounds(contentSize as [number, number], { windowName });
|
const newViewBounds = await getViewBounds(contentSize as [number, number], { windowName });
|
||||||
view.setBounds(newViewBounds);
|
view.setBounds(newViewBounds);
|
||||||
|
|
@ -406,12 +411,31 @@ export class View implements IViewService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setActiveViewForAllBrowserViews(workspaceID: string): Promise<void> {
|
public async setActiveViewForAllBrowserViews(workspaceID: string): Promise<void> {
|
||||||
await Promise.all([
|
// Set main window workspace
|
||||||
this.setActiveView(workspaceID, WindowNames.main),
|
const mainWindowTask = this.setActiveView(workspaceID, WindowNames.main);
|
||||||
this.preferenceService.get('attachToMenubar').then(async (attachToMenubar) => {
|
const tidgiMiniWindow = await this.preferenceService.get('tidgiMiniWindow');
|
||||||
return await (attachToMenubar && this.setActiveView(workspaceID, WindowNames.menuBar));
|
|
||||||
}),
|
// 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<void> {
|
public async setActiveView(workspaceID: string, windowName: WindowNames): Promise<void> {
|
||||||
|
|
@ -581,9 +605,9 @@ export class View implements IViewService {
|
||||||
const workspace = await workspaceService.getActiveWorkspace();
|
const workspace = await workspaceService.getActiveWorkspace();
|
||||||
if (workspace !== undefined) {
|
if (workspace !== undefined) {
|
||||||
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
|
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
|
||||||
const isMenubarOpen = await windowService.isMenubarOpen();
|
const isTidgiMiniWindowOpen = await windowService.isTidgiMiniWindowOpen();
|
||||||
if (isMenubarOpen) {
|
if (isTidgiMiniWindowOpen) {
|
||||||
return this.getView(workspace.id, WindowNames.menuBar);
|
return this.getView(workspace.id, WindowNames.tidgiMiniWindow);
|
||||||
} else {
|
} else {
|
||||||
return this.getView(workspace.id, WindowNames.main);
|
return this.getView(workspace.id, WindowNames.main);
|
||||||
}
|
}
|
||||||
|
|
@ -594,7 +618,7 @@ export class View implements IViewService {
|
||||||
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
||||||
const workspace = await workspaceService.getActiveWorkspace();
|
const workspace = await workspaceService.getActiveWorkspace();
|
||||||
if (workspace !== undefined) {
|
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:', '') });
|
logger.error(`getActiveBrowserViews workspace !== undefined`, { stack: new Error('stack').stack?.replace('Error:', '') });
|
||||||
return [];
|
return [];
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,11 @@ export interface IViewService {
|
||||||
createViewAddToWindow(workspace: IWorkspace, browserWindow: BrowserWindow, sharedWebPreferences: WebPreferences, windowName: WindowNames): Promise<WebContentsView>;
|
createViewAddToWindow(workspace: IWorkspace, browserWindow: BrowserWindow, sharedWebPreferences: WebPreferences, windowName: WindowNames): Promise<WebContentsView>;
|
||||||
forEachView: (functionToRun: (view: WebContentsView, workspaceID: string, windowName: WindowNames) => void) => void;
|
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<WebContentsView | undefined>;
|
getActiveBrowserView: () => Promise<WebContentsView | undefined>;
|
||||||
/**
|
/**
|
||||||
* Get active workspace's main window and menubar browser view.
|
* Get active workspace's main window and tidgi mini window browser view.
|
||||||
*/
|
*/
|
||||||
getActiveBrowserViews: () => Promise<Array<WebContentsView | undefined>>;
|
getActiveBrowserViews: () => Promise<Array<WebContentsView | undefined>>;
|
||||||
getLoadedViewEnsure(workspaceID: string, windowName: WindowNames): Promise<WebContentsView>;
|
getLoadedViewEnsure(workspaceID: string, windowName: WindowNames): Promise<WebContentsView>;
|
||||||
|
|
@ -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.
|
* 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 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
|
* @returns
|
||||||
*/
|
*/
|
||||||
setActiveView: (workspaceID: string, windowName: WindowNames) => Promise<void>;
|
setActiveView: (workspaceID: string, windowName: WindowNames) => Promise<void>;
|
||||||
|
|
|
||||||
|
|
@ -153,13 +153,11 @@ export function setupIpcServerRoutesHandlers(view: WebContentsView, workspaceID:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const error_ = error instanceof Error ? error : new Error(String(error));
|
|
||||||
logger.error('setupIpcServerRoutesHandlers.handlerCallback error', {
|
logger.error('setupIpcServerRoutesHandlers.handlerCallback error', {
|
||||||
function: 'setupIpcServerRoutesHandlers.handlerCallback',
|
function: 'setupIpcServerRoutesHandlers.handlerCallback',
|
||||||
error: error_.message,
|
error,
|
||||||
stack: error_.stack ?? '',
|
|
||||||
});
|
});
|
||||||
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}`;
|
const statusText = `setupIpcServerRoutesHandlers.handlerCallback: tidgi protocol 404 ${request.url}`;
|
||||||
logger.warn(statusText);
|
logger.warn(statusText);
|
||||||
|
|
@ -174,11 +172,9 @@ export function setupIpcServerRoutesHandlers(view: WebContentsView, workspaceID:
|
||||||
logger.warn('tidgi protocol is not handled', { function: 'setupIpcServerRoutesHandlers.handlerCallback' });
|
logger.warn('tidgi protocol is not handled', { function: 'setupIpcServerRoutesHandlers.handlerCallback' });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const error_ = error instanceof Error ? error : new Error(String(error));
|
|
||||||
logger.error('setupIpcServerRoutesHandlers.handlerCallback error', {
|
logger.error('setupIpcServerRoutesHandlers.handlerCallback error', {
|
||||||
function: 'setupIpcServerRoutesHandlers.handlerCallback',
|
function: 'setupIpcServerRoutesHandlers.handlerCallback',
|
||||||
error: error_.message,
|
error,
|
||||||
stack: error_.stack ?? '',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,17 +95,17 @@ export default function setupViewEventHandlers(
|
||||||
}
|
}
|
||||||
// if is external website
|
// if is external website
|
||||||
logger.debug('will-navigate openExternal', { newUrl, currentUrl, homeUrl, lastUrl });
|
logger.debug('will-navigate openExternal', { newUrl, currentUrl, homeUrl, lastUrl });
|
||||||
await shell.openExternal(newUrl).catch((_error: unknown) => {
|
await shell.openExternal(newUrl).catch((error_: unknown) => {
|
||||||
const error = _error instanceof Error ? _error : new Error(String(_error));
|
const error = error_ as Error;
|
||||||
logger.error(`will-navigate openExternal error ${error.message}`, error);
|
logger.error(`will-navigate openExternal error ${error.message}`, { error });
|
||||||
});
|
});
|
||||||
// if is an external website
|
// if is an external website
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
try {
|
try {
|
||||||
// TODO: do this until https://github.com/electron/electron/issues/31783 fixed
|
// TODO: do this until https://github.com/electron/electron/issues/31783 fixed
|
||||||
await view.webContents.loadURL(currentUrl);
|
await view.webContents.loadURL(currentUrl);
|
||||||
} catch (_error: unknown) {
|
} catch (error_: unknown) {
|
||||||
const error = _error instanceof Error ? _error : new Error(String(_error));
|
const error = error_ as Error;
|
||||||
logger.warn(new ViewLoadUrlError(lastUrl ?? '', `${error.message} ${error.stack ?? ''}`));
|
logger.warn(new ViewLoadUrlError(lastUrl ?? '', `${error.message} ${error.stack ?? ''}`));
|
||||||
}
|
}
|
||||||
// event.stopPropagation();
|
// event.stopPropagation();
|
||||||
|
|
@ -299,8 +299,8 @@ export default function setupViewEventHandlers(
|
||||||
view.webContents.on('update-target-url', (_event, url) => {
|
view.webContents.on('update-target-url', (_event, url) => {
|
||||||
try {
|
try {
|
||||||
view.webContents.send('update-target-url', url);
|
view.webContents.send('update-target-url', url);
|
||||||
} catch (_error: unknown) {
|
} catch (error_: unknown) {
|
||||||
const error = _error instanceof Error ? _error : new Error(String(_error));
|
const error = error_ as Error;
|
||||||
logger.warn(error);
|
logger.warn(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -27,25 +27,25 @@ export function handleOpenFileExternalLink(nextUrl: string, newWindowContext: IN
|
||||||
const fileStat = fs.statSync(absoluteFilePath);
|
const fileStat = fs.statSync(absoluteFilePath);
|
||||||
if (fileStat.isDirectory()) {
|
if (fileStat.isDirectory()) {
|
||||||
logger.info(`Opening directory ${absoluteFilePath}`, { function: 'handleOpenFileExternalLink' });
|
logger.info(`Opening directory ${absoluteFilePath}`, { function: 'handleOpenFileExternalLink' });
|
||||||
void shell.openPath(absoluteFilePath).catch((_error: unknown) => {
|
void shell.openPath(absoluteFilePath).catch((error_: unknown) => {
|
||||||
const error = _error instanceof Error ? _error : new Error(String(_error));
|
const error = error_ as Error;
|
||||||
const message = i18n.t('Log.FailedToOpenDirectory', { path: absoluteFilePath, message: error.message });
|
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]);
|
void wikiService.wikiOperationInBrowser(WikiChannel.generalNotification, newWindowContext.workspace.id, [message]);
|
||||||
});
|
});
|
||||||
} else if (fileStat.isFile()) {
|
} else if (fileStat.isFile()) {
|
||||||
logger.info(`Opening file ${absoluteFilePath}`, { function: 'handleOpenFileExternalLink' });
|
logger.info(`Opening file ${absoluteFilePath}`, { function: 'handleOpenFileExternalLink' });
|
||||||
void shell.openPath(absoluteFilePath).catch((_error: unknown) => {
|
void shell.openPath(absoluteFilePath).catch((error_: unknown) => {
|
||||||
const error = _error instanceof Error ? _error : new Error(String(_error));
|
const error = error_ as Error;
|
||||||
const message = i18n.t('Log.FailedToOpenFile', { path: absoluteFilePath, message: error.message });
|
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]);
|
void wikiService.wikiOperationInBrowser(WikiChannel.generalNotification, newWindowContext.workspace.id, [message]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (_error: unknown) {
|
} catch (error_: unknown) {
|
||||||
const error = _error instanceof Error ? _error : new Error(String(_error));
|
const error = error_ as Error;
|
||||||
const message = `${i18n.t('AddWorkspace.PathNotExist', { path: absoluteFilePath })} ${error.message}`;
|
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]);
|
void wikiService.wikiOperationInBrowser(WikiChannel.generalNotification, newWindowContext.workspace.id, [message]);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -77,10 +77,9 @@ export class Wiki implements IWikiService {
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('failed', {
|
logger.error('failed', {
|
||||||
error: (error as Error).message,
|
error,
|
||||||
newFolderPath,
|
newFolderPath,
|
||||||
folderName,
|
folderName,
|
||||||
stack: (error as Error).stack,
|
|
||||||
function: 'copyWikiTemplate',
|
function: 'copyWikiTemplate',
|
||||||
});
|
});
|
||||||
throw new CopyWikiTemplateError(`${(error as Error).message}, (${newFolderPath}, ${folderName})`);
|
throw new CopyWikiTemplateError(`${(error as Error).message}, (${newFolderPath}, ${folderName})`);
|
||||||
|
|
@ -393,8 +392,7 @@ export class Wiki implements IWikiService {
|
||||||
logger.debug(`terminateWorker for ${id}`);
|
logger.debug(`terminateWorker for ${id}`);
|
||||||
await terminateWorker(nativeWorker);
|
await terminateWorker(nativeWorker);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const error_ = error instanceof Error ? error : new Error(String(error));
|
logger.error('wiki worker stop failed', { function: 'stopWiki', error });
|
||||||
logger.error('wiki worker stop failed', { function: 'stopWiki', error: error_.message, errorObj: error_ });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
delete this.wikiWorkers[id];
|
delete this.wikiWorkers[id];
|
||||||
|
|
@ -709,10 +707,9 @@ export class Wiki implements IWikiService {
|
||||||
function: 'startWiki',
|
function: 'startWiki',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const error_ = error instanceof Error ? error : new Error(String(error));
|
logger.warn('startWiki failed', { function: 'startWiki', error });
|
||||||
logger.warn('startWiki failed', { function: 'startWiki', error: error_.message });
|
|
||||||
if (error instanceof WikiRuntimeError && error.retry) {
|
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.
|
// don't want it to throw here again, so no await here.
|
||||||
|
|
||||||
const workspaceViewService = container.get<IWorkspaceViewService>(serviceIdentifier.WorkspaceView);
|
const workspaceViewService = container.get<IWorkspaceViewService>(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')) {
|
} else if ((error as Error).message.includes('Did not receive an init message from worker after')) {
|
||||||
// https://github.com/andywer/threads.js/issues/426
|
// https://github.com/andywer/threads.js/issues/426
|
||||||
// wait some time and restart the wiki will solve this
|
// 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);
|
await this.restartWiki(workspace);
|
||||||
} else {
|
} else {
|
||||||
logger.warn('unexpected error, throw it', { function: 'startWiki' });
|
logger.warn('unexpected error, throw it', { function: 'startWiki' });
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
const newContent = content.replace(/(branches:\n\s+- )(master)$/m, `$1${options.branch}`);
|
||||||
await fs.writeFile(ghPagesConfigPath, newContent, 'utf8');
|
await fs.writeFile(ghPagesConfigPath, newContent, 'utf8');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const error_ = error instanceof Error ? error : new Error(String(error));
|
logger.error('updateGhConfig failed', { function: 'updateGhConfig', error });
|
||||||
logger.error('updateGhConfig failed', { function: 'updateGhConfig', error: error_.message, errorObj: error_ });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ export async function getSubWikiPluginContent(mainWikiPath: string): Promise<ISu
|
||||||
folderName: getFolderNamePathPart(line),
|
folderName: getFolderNamePathPart(line),
|
||||||
})).filter((item) => item.folderName.length > 0 && item.tagName.length > 0);
|
})).filter((item) => item.folderName.length > 0 && item.tagName.length > 0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error((error as Error).message, { function: 'getSubWikiPluginContent' });
|
logger.error((error as Error).message, { error, function: 'getSubWikiPluginContent' });
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,8 @@ export class WikiOperationsInWikiWorker {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
const result = new Function('$tw', script)(this.wikiInstance) as unknown;
|
const result = new Function('$tw', script)(this.wikiInstance) as unknown;
|
||||||
resolve(result);
|
resolve(result);
|
||||||
} catch (_error: unknown) {
|
} catch (error_: unknown) {
|
||||||
const error = _error instanceof Error ? _error : new Error(String(_error));
|
const error = error_ as Error;
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
}, 1);
|
}, 1);
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,15 @@ export async function extractWikiHTML(htmlWikiPath: string, saveWikiFolderPath:
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (_error: unknown) {
|
} catch (error_: unknown) {
|
||||||
const error = _error instanceof Error ? _error : new Error(String(_error));
|
const error = error_ as Error;
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (_error: unknown) {
|
} catch (error_: unknown) {
|
||||||
// removes the folder function that failed to convert.
|
// removes the folder function that failed to convert.
|
||||||
await remove(saveWikiFolderPath);
|
await remove(saveWikiFolderPath);
|
||||||
const error = _error instanceof Error ? _error : new Error(String(_error));
|
const error = error_ as Error;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -48,8 +48,8 @@ export async function packetHTMLFromWikiFolder(folderWikiPath: string, pathOfNew
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (_error: unknown) {
|
} catch (error_: unknown) {
|
||||||
const error = _error instanceof Error ? _error : new Error(String(_error));
|
const error = error_ as Error;
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -233,8 +233,7 @@ export class WikiEmbeddingService implements IWikiEmbeddingService {
|
||||||
|
|
||||||
return Array.isArray(countResult) && countResult.length > 0 ? Number(countResult[0]) : 0;
|
return Array.isArray(countResult) && countResult.length > 0 ? Number(countResult[0]) : 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
logger.error('Failed to get total notes count', { function: 'getTotalNotesCount', error });
|
||||||
logger.error('Failed to get total notes count', { function: 'getTotalNotesCount', error: errorMessage });
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -714,7 +713,7 @@ export class WikiEmbeddingService implements IWikiEmbeddingService {
|
||||||
await this.statusRepository!.save(entity);
|
await this.statusRepository!.save(entity);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If saving fails, just return the default status
|
// 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;
|
return defaultStatus;
|
||||||
|
|
@ -759,7 +758,7 @@ export class WikiEmbeddingService implements IWikiEmbeddingService {
|
||||||
}).catch((error: unknown) => {
|
}).catch((error: unknown) => {
|
||||||
logger.error('Failed to initialize embedding status subscription', {
|
logger.error('Failed to initialize embedding status subscription', {
|
||||||
function: 'subscribeToEmbeddingStatus',
|
function: 'subscribeToEmbeddingStatus',
|
||||||
error: String(error),
|
error: error as Error,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,8 @@ export class WikiGitWorkspace implements IWikiGitWorkspaceService {
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
} catch (_error: unknown) {
|
} catch (error_: unknown) {
|
||||||
const error = _error instanceof Error ? _error : new Error(String(_error));
|
const error = error_ as Error;
|
||||||
logger.error(`SyncBeforeShutdown failed`, { error });
|
logger.error(`SyncBeforeShutdown failed`, { error });
|
||||||
} finally {
|
} finally {
|
||||||
app.quit();
|
app.quit();
|
||||||
|
|
@ -98,9 +98,9 @@ export class WikiGitWorkspace implements IWikiGitWorkspaceService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return newWorkspace;
|
return newWorkspace;
|
||||||
} catch (_error: unknown) {
|
} catch (error_: unknown) {
|
||||||
// prepare to rollback changes
|
// 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 ?? ''}`;
|
const errorMessage = `initWikiGitTransaction failed, ${error.message} ${error.stack ?? ''}`;
|
||||||
logger.error(errorMessage);
|
logger.error(errorMessage);
|
||||||
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
||||||
|
|
@ -112,9 +112,8 @@ export class WikiGitWorkspace implements IWikiGitWorkspaceService {
|
||||||
} else if (typeof mainWikiToLink === 'string') {
|
} else if (typeof mainWikiToLink === 'string') {
|
||||||
await wikiService.removeWiki(wikiFolderLocation, mainWikiToLink);
|
await wikiService.removeWiki(wikiFolderLocation, mainWikiToLink);
|
||||||
}
|
}
|
||||||
} catch (_error_) {
|
} catch (error_: unknown) {
|
||||||
const error_ = _error_ instanceof Error ? _error_ : new Error(String(_error_));
|
throw new InitWikiGitRevertError(String(error_));
|
||||||
throw new InitWikiGitRevertError(error_.message);
|
|
||||||
}
|
}
|
||||||
throw new InitWikiGitError(errorMessage);
|
throw new InitWikiGitError(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
@ -187,11 +186,10 @@ export class WikiGitWorkspace implements IWikiGitWorkspaceService {
|
||||||
logger.info('Default wiki workspace created successfully', {
|
logger.info('Default wiki workspace created successfully', {
|
||||||
function: 'WikiGitWorkspace.initialize',
|
function: 'WikiGitWorkspace.initialize',
|
||||||
});
|
});
|
||||||
} catch (_error: unknown) {
|
} catch (error_: unknown) {
|
||||||
const error = _error instanceof Error ? _error : new Error(String(_error));
|
const error = error_ as Error;
|
||||||
logger.error('Failed to create default wiki workspace', {
|
logger.error('Failed to create default wiki workspace', {
|
||||||
error: error.message,
|
error,
|
||||||
stack: error.stack,
|
|
||||||
function: 'WikiGitWorkspace.initialize',
|
function: 'WikiGitWorkspace.initialize',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -223,9 +221,9 @@ export class WikiGitWorkspace implements IWikiGitWorkspaceService {
|
||||||
}
|
}
|
||||||
const wikiService = container.get<IWikiService>(serviceIdentifier.Wiki);
|
const wikiService = container.get<IWikiService>(serviceIdentifier.Wiki);
|
||||||
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
||||||
await wikiService.stopWiki(id).catch((_error: unknown) => {
|
await wikiService.stopWiki(id).catch((error_: unknown) => {
|
||||||
const error = _error instanceof Error ? _error : new Error(String(_error));
|
const error = error_ as Error;
|
||||||
logger.error(error.message, error);
|
logger.error(error.message, { error });
|
||||||
});
|
});
|
||||||
if (isSubWiki) {
|
if (isSubWiki) {
|
||||||
if (mainWikiToLink === null) {
|
if (mainWikiToLink === null) {
|
||||||
|
|
@ -251,9 +249,9 @@ export class WikiGitWorkspace implements IWikiGitWorkspaceService {
|
||||||
if (firstWorkspace !== undefined) {
|
if (firstWorkspace !== undefined) {
|
||||||
await container.get<IWorkspaceViewService>(serviceIdentifier.WorkspaceView).setActiveWorkspaceView(firstWorkspace.id);
|
await container.get<IWorkspaceViewService>(serviceIdentifier.WorkspaceView).setActiveWorkspaceView(firstWorkspace.id);
|
||||||
}
|
}
|
||||||
} catch (_error: unknown) {
|
} catch (error_: unknown) {
|
||||||
const error = _error instanceof Error ? _error : new Error(String(_error));
|
const error = error_ as Error;
|
||||||
logger.error(error.message, error);
|
logger.error(error.message, { error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export enum WindowNames {
|
||||||
* We only have a single instance of main window, that is the app window.
|
* We only have a single instance of main window, that is the app window.
|
||||||
*/
|
*/
|
||||||
main = 'main',
|
main = 'main',
|
||||||
menuBar = 'menuBar',
|
tidgiMiniWindow = 'tidgiMiniWindow',
|
||||||
notifications = 'notifications',
|
notifications = 'notifications',
|
||||||
preferences = 'preferences',
|
preferences = 'preferences',
|
||||||
/**
|
/**
|
||||||
|
|
@ -46,7 +46,7 @@ export const windowDimension: Record<WindowNames, { height?: number; width?: num
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 768,
|
height: 768,
|
||||||
},
|
},
|
||||||
[WindowNames.menuBar]: {
|
[WindowNames.tidgiMiniWindow]: {
|
||||||
width: 500,
|
width: 500,
|
||||||
height: 600,
|
height: 600,
|
||||||
},
|
},
|
||||||
|
|
@ -100,7 +100,7 @@ export interface WindowMeta {
|
||||||
[WindowNames.auth]: undefined;
|
[WindowNames.auth]: undefined;
|
||||||
[WindowNames.editWorkspace]: { workspaceID?: string };
|
[WindowNames.editWorkspace]: { workspaceID?: string };
|
||||||
[WindowNames.main]: { forceClose?: boolean };
|
[WindowNames.main]: { forceClose?: boolean };
|
||||||
[WindowNames.menuBar]: undefined;
|
[WindowNames.tidgiMiniWindow]: undefined;
|
||||||
[WindowNames.notifications]: undefined;
|
[WindowNames.notifications]: undefined;
|
||||||
[WindowNames.preferences]: IPreferenceWindowMeta;
|
[WindowNames.preferences]: IPreferenceWindowMeta;
|
||||||
[WindowNames.spellcheck]: undefined;
|
[WindowNames.spellcheck]: undefined;
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,31 @@
|
||||||
import { MENUBAR_ICON_PATH } from '@/constants/paths';
|
import { TIDGI_MINI_WINDOW_ICON_PATH } from '@/constants/paths';
|
||||||
import { isMac } from '@/helpers/system';
|
import { isMac } from '@/helpers/system';
|
||||||
import { container } from '@services/container';
|
import { container } from '@services/container';
|
||||||
import { i18n } from '@services/libs/i18n';
|
import { i18n } from '@services/libs/i18n';
|
||||||
import { logger } from '@services/libs/log';
|
import { logger } from '@services/libs/log';
|
||||||
import type { IMenuService } from '@services/menu/interface';
|
import type { IMenuService } from '@services/menu/interface';
|
||||||
|
import type { IPreferenceService } from '@services/preferences/interface';
|
||||||
import serviceIdentifier from '@services/serviceIdentifier';
|
import serviceIdentifier from '@services/serviceIdentifier';
|
||||||
import type { IViewService } from '@services/view/interface';
|
import type { IViewService } from '@services/view/interface';
|
||||||
import { BrowserWindowConstructorOptions, Menu, nativeImage, Tray } from 'electron';
|
import { BrowserWindowConstructorOptions, Menu, nativeImage, Tray } from 'electron';
|
||||||
import windowStateKeeper from 'electron-window-state';
|
import windowStateKeeper from 'electron-window-state';
|
||||||
import { debounce, merge as mergeDeep } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { Menubar, menubar } from 'menubar';
|
import { Menubar, menubar } from 'menubar';
|
||||||
import type { IWindowService } from './interface';
|
import type { IWindowService } from './interface';
|
||||||
import { getMainWindowEntry } from './viteEntry';
|
import { getMainWindowEntry } from './viteEntry';
|
||||||
import { WindowNames } from './WindowProperties';
|
import { WindowNames } from './WindowProperties';
|
||||||
|
|
||||||
export async function handleAttachToMenuBar(windowConfig: BrowserWindowConstructorOptions, windowWithBrowserViewState: windowStateKeeper.State | undefined): Promise<Menubar> {
|
export async function handleAttachToTidgiMiniWindow(
|
||||||
|
windowConfig: BrowserWindowConstructorOptions,
|
||||||
|
windowWithBrowserViewState: windowStateKeeper.State | undefined,
|
||||||
|
): Promise<Menubar> {
|
||||||
const menuService = container.get<IMenuService>(serviceIdentifier.MenuService);
|
const menuService = container.get<IMenuService>(serviceIdentifier.MenuService);
|
||||||
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
|
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
|
||||||
const viewService = container.get<IViewService>(serviceIdentifier.View);
|
const viewService = container.get<IViewService>(serviceIdentifier.View);
|
||||||
|
const preferenceService = container.get<IPreferenceService>(serviceIdentifier.Preference);
|
||||||
|
|
||||||
|
// Get tidgi mini window-specific titleBar preference
|
||||||
|
const tidgiMiniWindowShowTitleBar = await preferenceService.get('tidgiMiniWindowShowTitleBar');
|
||||||
|
|
||||||
// setImage after Tray instance is created to avoid
|
// setImage after Tray instance is created to avoid
|
||||||
// "Segmentation fault (core dumped)" bug on Linux
|
// "Segmentation fault (core dumped)" bug on Linux
|
||||||
|
|
@ -25,54 +33,71 @@ export async function handleAttachToMenuBar(windowConfig: BrowserWindowConstruct
|
||||||
// https://github.com/atomery/translatium/issues/164
|
// https://github.com/atomery/translatium/issues/164
|
||||||
const tray = new Tray(nativeImage.createEmpty());
|
const tray = new Tray(nativeImage.createEmpty());
|
||||||
// icon template is not supported on Windows & Linux
|
// 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(),
|
index: getMainWindowEntry(),
|
||||||
tray,
|
tray,
|
||||||
activateWithApp: false,
|
activateWithApp: false,
|
||||||
showDockIcon: true,
|
showDockIcon: true,
|
||||||
preloadWindow: true,
|
preloadWindow: true,
|
||||||
tooltip: i18n.t('Menu.TidGiMenuBar'),
|
tooltip: i18n.t('Menu.TidGiMiniWindow'),
|
||||||
browserWindow: mergeDeep(windowConfig, {
|
browserWindow: tidgiMiniWindowConfig,
|
||||||
show: false,
|
|
||||||
minHeight: 100,
|
|
||||||
minWidth: 250,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
menuBar.on('after-create-window', () => {
|
tidgiMiniWindow.on('after-create-window', () => {
|
||||||
if (menuBar.window !== undefined) {
|
if (tidgiMiniWindow.window !== undefined) {
|
||||||
menuBar.window.on('focus', async () => {
|
tidgiMiniWindow.window.on('focus', async () => {
|
||||||
logger.debug('restore window position');
|
logger.debug('restore window position', { function: 'handleAttachToTidgiMiniWindow' });
|
||||||
if (windowWithBrowserViewState === undefined) {
|
if (windowWithBrowserViewState === undefined) {
|
||||||
logger.debug('windowWithBrowserViewState is undefined for menuBar');
|
logger.debug('windowWithBrowserViewState is undefined for tidgiMiniWindow', { function: 'handleAttachToTidgiMiniWindow' });
|
||||||
} else {
|
} else {
|
||||||
if (menuBar.window === undefined) {
|
if (tidgiMiniWindow.window === undefined) {
|
||||||
logger.debug('menuBar.window is undefined');
|
logger.debug('tidgiMiniWindow.window is undefined', { function: 'handleAttachToTidgiMiniWindow' });
|
||||||
} else {
|
} else {
|
||||||
const haveXYValue = [windowWithBrowserViewState.x, windowWithBrowserViewState.y].every((value) => Number.isFinite(value));
|
const haveXYValue = [windowWithBrowserViewState.x, windowWithBrowserViewState.y].every((value) => Number.isFinite(value));
|
||||||
const haveWHValue = [windowWithBrowserViewState.width, windowWithBrowserViewState.height].every((value) => Number.isFinite(value));
|
const haveWHValue = [windowWithBrowserViewState.width, windowWithBrowserViewState.height].every((value) => Number.isFinite(value));
|
||||||
if (haveXYValue) {
|
if (haveXYValue) {
|
||||||
menuBar.window.setPosition(windowWithBrowserViewState.x, windowWithBrowserViewState.y, false);
|
tidgiMiniWindow.window.setPosition(windowWithBrowserViewState.x, windowWithBrowserViewState.y, false);
|
||||||
}
|
}
|
||||||
if (haveWHValue) {
|
if (haveWHValue) {
|
||||||
menuBar.window.setSize(windowWithBrowserViewState.width, windowWithBrowserViewState.height, false);
|
tidgiMiniWindow.window.setSize(windowWithBrowserViewState.width, windowWithBrowserViewState.height, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const view = await viewService.getActiveBrowserView();
|
const view = await viewService.getActiveBrowserView();
|
||||||
view?.webContents.focus();
|
view?.webContents.focus();
|
||||||
});
|
});
|
||||||
menuBar.window.removeAllListeners('close');
|
tidgiMiniWindow.window.removeAllListeners('close');
|
||||||
menuBar.window.on('close', (event) => {
|
tidgiMiniWindow.window.on('close', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
menuBar.hideWindow();
|
tidgiMiniWindow.hideWindow();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
menuBar.on('hide', async () => {
|
tidgiMiniWindow.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.
|
// 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) {
|
if (isMac) {
|
||||||
const mainWindow = windowService.get(WindowNames.main);
|
const mainWindow = windowService.get(WindowNames.main);
|
||||||
if (mainWindow?.isVisible() === true) {
|
if (mainWindow?.isVisible() === true) {
|
||||||
|
|
@ -81,29 +106,29 @@ export async function handleAttachToMenuBar(windowConfig: BrowserWindowConstruct
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// https://github.com/maxogden/menubar/issues/120
|
// https://github.com/maxogden/menubar/issues/120
|
||||||
menuBar.on('after-hide', () => {
|
tidgiMiniWindow.on('after-hide', () => {
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
menuBar.app.hide();
|
tidgiMiniWindow.app.hide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// manually save window state https://github.com/mawie81/electron-window-state/issues/64
|
// manually save window state https://github.com/mawie81/electron-window-state/issues/64
|
||||||
const debouncedSaveWindowState = debounce(
|
const debouncedSaveWindowState = debounce(
|
||||||
() => {
|
() => {
|
||||||
if (menuBar.window !== undefined) {
|
if (tidgiMiniWindow.window !== undefined) {
|
||||||
windowWithBrowserViewState?.saveState(menuBar.window);
|
windowWithBrowserViewState?.saveState(tidgiMiniWindow.window);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
500,
|
500,
|
||||||
);
|
);
|
||||||
// menubar is hide, not close, so not managed by windowStateKeeper, need to save manually
|
// tidgi mini window is hide, not close, so not managed by windowStateKeeper, need to save manually
|
||||||
menuBar.window?.on('resize', debouncedSaveWindowState);
|
tidgiMiniWindow.window?.on('resize', debouncedSaveWindowState);
|
||||||
menuBar.window?.on('move', debouncedSaveWindowState);
|
tidgiMiniWindow.window?.on('move', debouncedSaveWindowState);
|
||||||
|
|
||||||
return await new Promise<Menubar>((resolve) => {
|
return await new Promise<Menubar>((resolve) => {
|
||||||
menuBar.on('ready', async () => {
|
tidgiMiniWindow.on('ready', async () => {
|
||||||
// right on tray icon
|
// right on tray icon
|
||||||
menuBar.tray.on('right-click', () => {
|
tidgiMiniWindow.tray.on('right-click', () => {
|
||||||
const contextMenu = Menu.buildFromTemplate([
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
{
|
{
|
||||||
label: i18n.t('ContextMenu.OpenTidGi'),
|
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 () => {
|
click: async () => {
|
||||||
await menuBar.showWindow();
|
await tidgiMiniWindow.showWindow();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -143,23 +168,23 @@ export async function handleAttachToMenuBar(windowConfig: BrowserWindowConstruct
|
||||||
{
|
{
|
||||||
label: i18n.t('ContextMenu.Quit'),
|
label: i18n.t('ContextMenu.Quit'),
|
||||||
click: () => {
|
click: () => {
|
||||||
menuBar.app.quit();
|
tidgiMiniWindow.app.quit();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
menuBar.tray.popUpContextMenu(contextMenu);
|
tidgiMiniWindow.tray.popUpContextMenu(contextMenu);
|
||||||
});
|
});
|
||||||
|
|
||||||
// right click on window content
|
// right click on window content
|
||||||
if (menuBar.window?.webContents !== undefined) {
|
if (tidgiMiniWindow.window?.webContents !== undefined) {
|
||||||
const unregisterContextMenu = await menuService.initContextMenuForWindowWebContents(menuBar.window.webContents);
|
const unregisterContextMenu = await menuService.initContextMenuForWindowWebContents(tidgiMiniWindow.window.webContents);
|
||||||
menuBar.on('after-close', () => {
|
tidgiMiniWindow.on('after-close', () => {
|
||||||
unregisterContextMenu();
|
unregisterContextMenu();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(menuBar);
|
resolve(tidgiMiniWindow);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -8,7 +8,9 @@ import { windowDimension, WindowMeta, WindowNames } from '@services/windows/Wind
|
||||||
|
|
||||||
import { Channels, MetaDataChannel, ViewChannel, WindowChannel } from '@/constants/channels';
|
import { Channels, MetaDataChannel, ViewChannel, WindowChannel } from '@/constants/channels';
|
||||||
import type { IPreferenceService } from '@services/preferences/interface';
|
import type { IPreferenceService } from '@services/preferences/interface';
|
||||||
|
import type { IViewService } from '@services/view/interface';
|
||||||
import type { IWorkspaceService } from '@services/workspaces/interface';
|
import type { IWorkspaceService } from '@services/workspaces/interface';
|
||||||
|
import type { IWorkspaceViewService } from '@services/workspacesView/interface';
|
||||||
|
|
||||||
import { SETTINGS_FOLDER } from '@/constants/appPaths';
|
import { SETTINGS_FOLDER } from '@/constants/appPaths';
|
||||||
import { isTest } from '@/constants/environment';
|
import { isTest } from '@/constants/environment';
|
||||||
|
|
@ -19,8 +21,8 @@ import { container } from '@services/container';
|
||||||
import getViewBounds from '@services/libs/getViewBounds';
|
import getViewBounds from '@services/libs/getViewBounds';
|
||||||
import { logger } from '@services/libs/log';
|
import { logger } from '@services/libs/log';
|
||||||
import type { IThemeService } from '@services/theme/interface';
|
import type { IThemeService } from '@services/theme/interface';
|
||||||
import type { IViewService } from '@services/view/interface';
|
import { getTidgiMiniWindowTargetWorkspace } from '@services/workspacesView/utilities';
|
||||||
import { handleAttachToMenuBar } from './handleAttachToMenuBar';
|
import { handleAttachToTidgiMiniWindow } from './handleAttachToTidgiMiniWindow';
|
||||||
import { handleCreateBasicWindow } from './handleCreateBasicWindow';
|
import { handleCreateBasicWindow } from './handleCreateBasicWindow';
|
||||||
import type { IWindowOpenConfig, IWindowService } from './interface';
|
import type { IWindowOpenConfig, IWindowService } from './interface';
|
||||||
import { registerBrowserViewWindowListeners } from './registerBrowserViewWindowListeners';
|
import { registerBrowserViewWindowListeners } from './registerBrowserViewWindowListeners';
|
||||||
|
|
@ -31,8 +33,8 @@ import { getPreloadPath } from './viteEntry';
|
||||||
export class Window implements IWindowService {
|
export class Window implements IWindowService {
|
||||||
private readonly windows = new Map<WindowNames, BrowserWindow>();
|
private readonly windows = new Map<WindowNames, BrowserWindow>();
|
||||||
private windowMeta = {} as Partial<WindowMeta>;
|
private windowMeta = {} as Partial<WindowMeta>;
|
||||||
/** menubar version of main window, if user set openInMenubar to true in preferences */
|
/** tidgi mini window version of main window, if user set attachToTidgiMiniWindow to true in preferences */
|
||||||
private mainWindowMenuBar?: Menubar;
|
private tidgiMiniWindowMenubar?: Menubar;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@inject(serviceIdentifier.Preference) private readonly preferenceService: IPreferenceService,
|
@inject(serviceIdentifier.Preference) private readonly preferenceService: IPreferenceService,
|
||||||
|
|
@ -76,8 +78,8 @@ export class Window implements IWindowService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public get(windowName: WindowNames = WindowNames.main): BrowserWindow | undefined {
|
public get(windowName: WindowNames = WindowNames.main): BrowserWindow | undefined {
|
||||||
if (windowName === WindowNames.menuBar) {
|
if (windowName === WindowNames.tidgiMiniWindow) {
|
||||||
return this.mainWindowMenuBar?.window;
|
return this.tidgiMiniWindowMenubar?.window;
|
||||||
}
|
}
|
||||||
return this.windows.get(windowName);
|
return this.windows.get(windowName);
|
||||||
}
|
}
|
||||||
|
|
@ -124,8 +126,8 @@ export class Window implements IWindowService {
|
||||||
this.windows.clear();
|
this.windows.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async isMenubarOpen(): Promise<boolean> {
|
public async isTidgiMiniWindowOpen(): Promise<boolean> {
|
||||||
return this.mainWindowMenuBar?.window?.isFocused() ?? false;
|
return this.tidgiMiniWindowMenubar?.window?.isVisible() ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async open<N extends WindowNames>(windowName: N, meta?: WindowMeta[N], config?: IWindowOpenConfig<N>): Promise<undefined>;
|
public async open<N extends WindowNames>(windowName: N, meta?: WindowMeta[N], config?: IWindowOpenConfig<N>): Promise<undefined>;
|
||||||
|
|
@ -164,11 +166,11 @@ export class Window implements IWindowService {
|
||||||
|
|
||||||
// create new window
|
// create new window
|
||||||
const preferenceService = container.get<IPreferenceService>(serviceIdentifier.Preference);
|
const preferenceService = container.get<IPreferenceService>(serviceIdentifier.Preference);
|
||||||
const { hideMenuBar: autoHideMenuBar, titleBar: showTitleBar, menuBarAlwaysOnTop, alwaysOnTop } = preferenceService.getPreferences();
|
const { hideMenuBar: autoHideMenuBar, titleBar: showTitleBar, tidgiMiniWindowAlwaysOnTop, alwaysOnTop } = preferenceService.getPreferences();
|
||||||
let windowWithBrowserViewConfig: Partial<BrowserWindowConstructorOptions> = {};
|
let windowWithBrowserViewConfig: Partial<BrowserWindowConstructorOptions> = {};
|
||||||
let windowWithBrowserViewState: windowStateKeeperState | undefined;
|
let windowWithBrowserViewState: windowStateKeeperState | undefined;
|
||||||
const WindowToKeepPositionState = [WindowNames.main, WindowNames.menuBar];
|
const WindowToKeepPositionState = [WindowNames.main, WindowNames.tidgiMiniWindow];
|
||||||
const WindowWithBrowserView = [WindowNames.main, WindowNames.menuBar, WindowNames.secondary];
|
const WindowWithBrowserView = [WindowNames.main, WindowNames.tidgiMiniWindow, WindowNames.secondary];
|
||||||
const isWindowWithBrowserView = WindowWithBrowserView.includes(windowName);
|
const isWindowWithBrowserView = WindowWithBrowserView.includes(windowName);
|
||||||
if (WindowToKeepPositionState.includes(windowName)) {
|
if (WindowToKeepPositionState.includes(windowName)) {
|
||||||
windowWithBrowserViewState = windowStateKeeper({
|
windowWithBrowserViewState = windowStateKeeper({
|
||||||
|
|
@ -185,7 +187,7 @@ export class Window implements IWindowService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// hide titleBar should not take effect on setting window
|
// 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 = {
|
const windowConfig: BrowserWindowConstructorOptions = {
|
||||||
...windowDimension[windowName],
|
...windowDimension[windowName],
|
||||||
...windowWithBrowserViewConfig,
|
...windowWithBrowserViewConfig,
|
||||||
|
|
@ -197,7 +199,7 @@ export class Window implements IWindowService {
|
||||||
titleBarStyle: hideTitleBar ? 'hidden' : 'default',
|
titleBarStyle: hideTitleBar ? 'hidden' : 'default',
|
||||||
// https://www.electronjs.org/docs/latest/tutorial/custom-title-bar#add-native-window-controls-windows-linux
|
// https://www.electronjs.org/docs/latest/tutorial/custom-title-bar#add-native-window-controls-windows-linux
|
||||||
...(hideTitleBar && process.platform !== 'darwin' ? { titleBarOverlay: true } : {}),
|
...(hideTitleBar && process.platform !== 'darwin' ? { titleBarOverlay: true } : {}),
|
||||||
alwaysOnTop: windowName === WindowNames.menuBar ? menuBarAlwaysOnTop : alwaysOnTop,
|
alwaysOnTop: windowName === WindowNames.tidgiMiniWindow ? tidgiMiniWindowAlwaysOnTop : alwaysOnTop,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
devTools: !isTest,
|
devTools: !isTest,
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
|
|
@ -214,12 +216,12 @@ export class Window implements IWindowService {
|
||||||
parent: isWindowWithBrowserView ? undefined : this.get(WindowNames.main),
|
parent: isWindowWithBrowserView ? undefined : this.get(WindowNames.main),
|
||||||
};
|
};
|
||||||
let newWindow: BrowserWindow;
|
let newWindow: BrowserWindow;
|
||||||
if (windowName === WindowNames.menuBar) {
|
if (windowName === WindowNames.tidgiMiniWindow) {
|
||||||
this.mainWindowMenuBar = await handleAttachToMenuBar(windowConfig, windowWithBrowserViewState);
|
this.tidgiMiniWindowMenubar = await handleAttachToTidgiMiniWindow(windowConfig, windowWithBrowserViewState);
|
||||||
if (this.mainWindowMenuBar.window === undefined) {
|
if (this.tidgiMiniWindowMenubar.window === undefined) {
|
||||||
throw new Error('MenuBar failed to create window.');
|
throw new Error('TidgiMiniWindow failed to create window.');
|
||||||
}
|
}
|
||||||
newWindow = this.mainWindowMenuBar.window;
|
newWindow = this.tidgiMiniWindowMenubar.window;
|
||||||
} else {
|
} else {
|
||||||
newWindow = await handleCreateBasicWindow(windowName, windowConfig, meta, config);
|
newWindow = await handleCreateBasicWindow(windowName, windowConfig, meta, config);
|
||||||
if (isWindowWithBrowserView) {
|
if (isWindowWithBrowserView) {
|
||||||
|
|
@ -337,4 +339,243 @@ export class Window implements IWindowService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async toggleTidgiMiniWindow(): Promise<void> {
|
||||||
|
logger.info('toggleTidgiMiniWindow called', { function: 'toggleTidgiMiniWindow' });
|
||||||
|
try {
|
||||||
|
const preferenceService = container.get<IPreferenceService>(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<void> {
|
||||||
|
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<IWorkspaceService>(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<IViewService>(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<IWorkspaceViewService>(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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<IWorkspaceService>(serviceIdentifier.Workspace);
|
||||||
|
const viewService = container.get<IViewService>(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<IWorkspaceService>(serviceIdentifier.Workspace);
|
||||||
|
const viewService = container.get<IViewService>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export interface IWindowService {
|
||||||
*/
|
*/
|
||||||
hide(windowName: WindowNames): Promise<void>;
|
hide(windowName: WindowNames): Promise<void>;
|
||||||
isFullScreen(windowName?: WindowNames): Promise<boolean | undefined>;
|
isFullScreen(windowName?: WindowNames): Promise<boolean | undefined>;
|
||||||
isMenubarOpen(): Promise<boolean>;
|
isTidgiMiniWindowOpen(): Promise<boolean>;
|
||||||
loadURL(windowName: WindowNames, newUrl?: string): Promise<void>;
|
loadURL(windowName: WindowNames, newUrl?: string): Promise<void>;
|
||||||
maximize(): Promise<void>;
|
maximize(): Promise<void>;
|
||||||
/**
|
/**
|
||||||
|
|
@ -55,7 +55,16 @@ export interface IWindowService {
|
||||||
set(windowName: WindowNames, win: BrowserWindow | undefined): void;
|
set(windowName: WindowNames, win: BrowserWindow | undefined): void;
|
||||||
setWindowMeta<N extends WindowNames>(windowName: N, meta?: WindowMeta[N]): Promise<void>;
|
setWindowMeta<N extends WindowNames>(windowName: N, meta?: WindowMeta[N]): Promise<void>;
|
||||||
stopFindInPage(close?: boolean, windowName?: WindowNames): Promise<void>;
|
stopFindInPage(close?: boolean, windowName?: WindowNames): Promise<void>;
|
||||||
|
toggleTidgiMiniWindow(): Promise<void>;
|
||||||
updateWindowMeta<N extends WindowNames>(windowName: N, meta?: WindowMeta[N]): Promise<void>;
|
updateWindowMeta<N extends WindowNames>(windowName: N, meta?: WindowMeta[N]): Promise<void>;
|
||||||
|
/** Open tidgi mini window without restart - hot reload. enableIt=true means fully enable and open. */
|
||||||
|
openTidgiMiniWindow(enableIt?: boolean, showWindow?: boolean): Promise<void>;
|
||||||
|
/** Close tidgi mini window. disableIt=true means fully disable and cleanup tray. */
|
||||||
|
closeTidgiMiniWindow(disableIt?: boolean): Promise<void>;
|
||||||
|
/** Update window properties without restart - hot reload */
|
||||||
|
updateWindowProperties(windowName: WindowNames, properties: { alwaysOnTop?: boolean }): Promise<void>;
|
||||||
|
/** React to preference changes related to windows (tidgi mini window etc.) */
|
||||||
|
reactWhenPreferencesChanged(key: string, value: unknown): Promise<void>;
|
||||||
}
|
}
|
||||||
export const WindowServiceIPCDescriptor = {
|
export const WindowServiceIPCDescriptor = {
|
||||||
channel: WindowChannel.name,
|
channel: WindowChannel.name,
|
||||||
|
|
@ -69,7 +78,7 @@ export const WindowServiceIPCDescriptor = {
|
||||||
goForward: ProxyPropertyType.Function,
|
goForward: ProxyPropertyType.Function,
|
||||||
goHome: ProxyPropertyType.Function,
|
goHome: ProxyPropertyType.Function,
|
||||||
isFullScreen: ProxyPropertyType.Function,
|
isFullScreen: ProxyPropertyType.Function,
|
||||||
isMenubarOpen: ProxyPropertyType.Function,
|
isTidgiMiniWindowOpen: ProxyPropertyType.Function,
|
||||||
loadURL: ProxyPropertyType.Function,
|
loadURL: ProxyPropertyType.Function,
|
||||||
maximize: ProxyPropertyType.Function,
|
maximize: ProxyPropertyType.Function,
|
||||||
open: ProxyPropertyType.Function,
|
open: ProxyPropertyType.Function,
|
||||||
|
|
@ -78,6 +87,10 @@ export const WindowServiceIPCDescriptor = {
|
||||||
sendToAllWindows: ProxyPropertyType.Function,
|
sendToAllWindows: ProxyPropertyType.Function,
|
||||||
setWindowMeta: ProxyPropertyType.Function,
|
setWindowMeta: ProxyPropertyType.Function,
|
||||||
stopFindInPage: ProxyPropertyType.Function,
|
stopFindInPage: ProxyPropertyType.Function,
|
||||||
|
toggleTidgiMiniWindow: ProxyPropertyType.Function,
|
||||||
updateWindowMeta: ProxyPropertyType.Function,
|
updateWindowMeta: ProxyPropertyType.Function,
|
||||||
|
openTidgiMiniWindow: ProxyPropertyType.Function,
|
||||||
|
closeTidgiMiniWindow: ProxyPropertyType.Function,
|
||||||
|
updateWindowProperties: ProxyPropertyType.Function,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -123,10 +123,6 @@ export async function registerMenu(): Promise<void> {
|
||||||
// if back is called in popup window
|
// if back is called in popup window
|
||||||
// navigate in the popup window instead
|
// navigate in the popup window instead
|
||||||
if (browserWindow !== undefined) {
|
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<IBrowserViewMetaData>(MetaDataChannel.getViewMetaData, browserWindow);
|
|
||||||
// const windowName = isPopup ? WindowNames.menuBar : WindowNames.main
|
|
||||||
|
|
||||||
await windowService.goForward();
|
await windowService.goForward();
|
||||||
}
|
}
|
||||||
ipcMain.emit('request-go-forward');
|
ipcMain.emit('request-go-forward');
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import { DELAY_MENU_REGISTER } from '@/constants/parameters';
|
||||||
import type { ISyncService } from '@services/sync/interface';
|
import type { ISyncService } from '@services/sync/interface';
|
||||||
import type { IInitializeWorkspaceOptions, IWorkspaceViewService } from './interface';
|
import type { IInitializeWorkspaceOptions, IWorkspaceViewService } from './interface';
|
||||||
import { registerMenu } from './registerMenu';
|
import { registerMenu } from './registerMenu';
|
||||||
|
import { getTidgiMiniWindowTargetWorkspace } from './utilities';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class WorkspaceView implements IWorkspaceViewService {
|
export class WorkspaceView implements IWorkspaceViewService {
|
||||||
|
|
@ -153,11 +154,9 @@ export class WorkspaceView implements IWorkspaceViewService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const error_ = error instanceof Error ? error : new Error(String(error));
|
|
||||||
logger.error('wikiStartup sync failed', {
|
logger.error('wikiStartup sync failed', {
|
||||||
function: 'initializeAllWorkspaceView',
|
function: 'initializeAllWorkspaceView',
|
||||||
error: error_.message,
|
error,
|
||||||
stack: error_.stack ?? 'no stack',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -173,7 +172,7 @@ export class WorkspaceView implements IWorkspaceViewService {
|
||||||
logger.debug('Skip because alreadyHaveView');
|
logger.debug('Skip because alreadyHaveView');
|
||||||
return;
|
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);
|
await this.addViewForAllBrowserViews(workspace);
|
||||||
if (isNew && options.from === WikiCreationMethod.Create) {
|
if (isNew && options.from === WikiCreationMethod.Create) {
|
||||||
const view = container.get<IViewService>(serviceIdentifier.View).getView(workspace.id, WindowNames.main);
|
const view = container.get<IViewService>(serviceIdentifier.View).getView(workspace.id, WindowNames.main);
|
||||||
|
|
@ -205,12 +204,28 @@ export class WorkspaceView implements IWorkspaceViewService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addViewForAllBrowserViews(workspace: IWorkspace): Promise<void> {
|
public async addViewForAllBrowserViews(workspace: IWorkspace): Promise<void> {
|
||||||
await Promise.all([
|
const mainTask = container.get<IViewService>(serviceIdentifier.View).addView(workspace, WindowNames.main);
|
||||||
container.get<IViewService>(serviceIdentifier.View).addView(workspace, WindowNames.main),
|
|
||||||
this.preferenceService.get('attachToMenubar').then(async (attachToMenubar) => {
|
// For tidgi mini window, decide which workspace to show based on preferences
|
||||||
return await (attachToMenubar && container.get<IViewService>(serviceIdentifier.View).addView(workspace, WindowNames.menuBar));
|
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<IViewService>(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<IViewService>(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<void> {
|
public async openWorkspaceWindowWithView(workspace: IWorkspace, configs?: { uri?: string }): Promise<void> {
|
||||||
|
|
@ -355,11 +370,9 @@ export class WorkspaceView implements IWorkspaceViewService {
|
||||||
await container.get<IViewService>(serviceIdentifier.View).setActiveViewForAllBrowserViews(nextWorkspaceID);
|
await container.get<IViewService>(serviceIdentifier.View).setActiveViewForAllBrowserViews(nextWorkspaceID);
|
||||||
await this.realignActiveWorkspace(nextWorkspaceID);
|
await this.realignActiveWorkspace(nextWorkspaceID);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const error_ = error instanceof Error ? error : new Error(String(error));
|
|
||||||
logger.error('setActiveWorkspaceView error', {
|
logger.error('setActiveWorkspaceView error', {
|
||||||
function: 'setActiveWorkspaceView',
|
function: 'setActiveWorkspaceView',
|
||||||
error: error_.message,
|
error,
|
||||||
errorObj: error_,
|
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -388,11 +401,9 @@ export class WorkspaceView implements IWorkspaceViewService {
|
||||||
try {
|
try {
|
||||||
await this.hideWorkspaceView(activeWorkspace.id);
|
await this.hideWorkspaceView(activeWorkspace.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const error_ = error instanceof Error ? error : new Error(String(error));
|
|
||||||
logger.error('setActiveWorkspaceView error', {
|
logger.error('setActiveWorkspaceView error', {
|
||||||
function: 'clearActiveWorkspaceView',
|
function: 'clearActiveWorkspaceView',
|
||||||
error: error_.message,
|
error,
|
||||||
errorObj: error_,
|
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -461,7 +472,7 @@ export class WorkspaceView implements IWorkspaceViewService {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
workspaces.map(async (workspace) => {
|
workspaces.map(async (workspace) => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
[WindowNames.main, WindowNames.menuBar].map(async (windowName) => {
|
[WindowNames.main, WindowNames.tidgiMiniWindow].map(async (windowName) => {
|
||||||
const view = container.get<IViewService>(serviceIdentifier.View).getView(workspace.id, windowName);
|
const view = container.get<IViewService>(serviceIdentifier.View).getView(workspace.id, windowName);
|
||||||
if (view !== undefined) {
|
if (view !== undefined) {
|
||||||
await container.get<IViewService>(serviceIdentifier.View).loadUrlForView(workspace, view);
|
await container.get<IViewService>(serviceIdentifier.View).loadUrlForView(workspace, view);
|
||||||
|
|
@ -529,11 +540,9 @@ export class WorkspaceView implements IWorkspaceViewService {
|
||||||
try {
|
try {
|
||||||
await container.get<IMenuService>(serviceIdentifier.MenuService).buildMenu();
|
await container.get<IMenuService>(serviceIdentifier.MenuService).buildMenu();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const error_ = error instanceof Error ? error : new Error(String(error));
|
|
||||||
logger.error('realignActiveWorkspace buildMenu error', {
|
logger.error('realignActiveWorkspace buildMenu error', {
|
||||||
function: 'realignActiveWorkspace',
|
function: 'realignActiveWorkspace',
|
||||||
error: error_.message,
|
error,
|
||||||
errorObj: error_,
|
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -556,7 +565,7 @@ export class WorkspaceView implements IWorkspaceViewService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const mainWindow = container.get<IWindowService>(serviceIdentifier.Window).get(WindowNames.main);
|
const mainWindow = container.get<IWindowService>(serviceIdentifier.Window).get(WindowNames.main);
|
||||||
const menuBarWindow = container.get<IWindowService>(serviceIdentifier.Window).get(WindowNames.menuBar);
|
const tidgiMiniWindow = container.get<IWindowService>(serviceIdentifier.Window).get(WindowNames.tidgiMiniWindow);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`realignActiveWorkspaceView: id ${workspaceToRealign?.id ?? 'undefined'}`,
|
`realignActiveWorkspaceView: id ${workspaceToRealign?.id ?? 'undefined'}`,
|
||||||
|
|
@ -565,7 +574,7 @@ export class WorkspaceView implements IWorkspaceViewService {
|
||||||
logger.warn('realignActiveWorkspaceView: no active workspace');
|
logger.warn('realignActiveWorkspaceView: no active workspace');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mainWindow === undefined && menuBarWindow === undefined) {
|
if (mainWindow === undefined && tidgiMiniWindow === undefined) {
|
||||||
logger.warn('realignActiveWorkspaceView: no active window');
|
logger.warn('realignActiveWorkspaceView: no active window');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -576,18 +585,20 @@ export class WorkspaceView implements IWorkspaceViewService {
|
||||||
tasks.push(container.get<IViewService>(serviceIdentifier.View).realignActiveView(mainWindow, workspaceToRealign.id, WindowNames.main));
|
tasks.push(container.get<IViewService>(serviceIdentifier.View).realignActiveView(mainWindow, workspaceToRealign.id, WindowNames.main));
|
||||||
logger.debug(`realignActiveWorkspaceView: realign main window for ${workspaceToRealign.id}.`);
|
logger.debug(`realignActiveWorkspaceView: realign main window for ${workspaceToRealign.id}.`);
|
||||||
}
|
}
|
||||||
if (menuBarWindow === undefined) {
|
if (tidgiMiniWindow === undefined) {
|
||||||
logger.info(`realignActiveWorkspaceView: no menuBarBrowserViewWebContent, skip menu bar window for ${workspaceToRealign.id}.`);
|
logger.info(`realignActiveWorkspaceView: no tidgiMiniWindowBrowserViewWebContent, skip tidgi mini window for ${workspaceToRealign.id}.`);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`realignActiveWorkspaceView: realign menu bar window for ${workspaceToRealign.id}.`);
|
// For tidgi mini window, decide which workspace to show based on preferences
|
||||||
tasks.push(container.get<IViewService>(serviceIdentifier.View).realignActiveView(menuBarWindow, workspaceToRealign.id, WindowNames.menuBar));
|
const { targetWorkspaceId } = await getTidgiMiniWindowTargetWorkspace(workspaceToRealign.id);
|
||||||
|
const tidgiMiniWindowWorkspaceId = targetWorkspaceId || workspaceToRealign.id;
|
||||||
|
tasks.push(container.get<IViewService>(serviceIdentifier.View).realignActiveView(tidgiMiniWindow, tidgiMiniWindowWorkspaceId, WindowNames.tidgiMiniWindow));
|
||||||
}
|
}
|
||||||
await Promise.all(tasks);
|
await Promise.all(tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async hideWorkspaceView(idToDeactivate: string): Promise<void> {
|
private async hideWorkspaceView(idToDeactivate: string): Promise<void> {
|
||||||
const mainWindow = container.get<IWindowService>(serviceIdentifier.Window).get(WindowNames.main);
|
const mainWindow = container.get<IWindowService>(serviceIdentifier.Window).get(WindowNames.main);
|
||||||
const menuBarWindow = container.get<IWindowService>(serviceIdentifier.Window).get(WindowNames.menuBar);
|
const tidgiMiniWindow = container.get<IWindowService>(serviceIdentifier.Window).get(WindowNames.tidgiMiniWindow);
|
||||||
const tasks = [];
|
const tasks = [];
|
||||||
if (mainWindow === undefined) {
|
if (mainWindow === undefined) {
|
||||||
logger.warn(`hideWorkspaceView: no mainBrowserWindow, skip main window browserView.`);
|
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.`);
|
logger.info(`hideWorkspaceView: hide main window browserView.`);
|
||||||
tasks.push(container.get<IViewService>(serviceIdentifier.View).hideView(mainWindow, WindowNames.main, idToDeactivate));
|
tasks.push(container.get<IViewService>(serviceIdentifier.View).hideView(mainWindow, WindowNames.main, idToDeactivate));
|
||||||
}
|
}
|
||||||
if (menuBarWindow === undefined) {
|
if (tidgiMiniWindow === undefined) {
|
||||||
logger.debug(`hideWorkspaceView: no menuBarBrowserWindow, skip menu bar window browserView.`);
|
logger.debug(`hideWorkspaceView: no tidgiMiniWindowBrowserWindow, skip tidgi mini window browserView.`);
|
||||||
} else {
|
} else {
|
||||||
logger.info(`hideWorkspaceView: hide menu bar window browserView.`);
|
// For tidgi mini window, only hide if syncing with main window OR if this is the fixed workspace being deactivated
|
||||||
tasks.push(container.get<IViewService>(serviceIdentifier.View).hideView(menuBarWindow, WindowNames.menuBar, idToDeactivate));
|
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<IViewService>(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);
|
await Promise.all(tasks);
|
||||||
logger.info(`hideWorkspaceView: done.`);
|
logger.info(`hideWorkspaceView: done.`);
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,7 @@ export async function registerMenu(): Promise<void> {
|
||||||
if (error instanceof DownloadCancelError) {
|
if (error instanceof DownloadCancelError) {
|
||||||
logger.debug('cancelled', { function: 'registerMenu.printPage' });
|
logger.debug('cancelled', { function: 'registerMenu.printPage' });
|
||||||
} else {
|
} else {
|
||||||
const error_ = error instanceof Error ? error : new Error(String(error));
|
logger.error('print page error', { function: 'registerMenu.printPage', error });
|
||||||
logger.error('print page error', { function: 'registerMenu.printPage', error: error_.message, errorObj: error_ });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
34
src/services/workspacesView/utilities.ts
Normal file
34
src/services/workspacesView/utilities.ts
Normal file
|
|
@ -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<IPreferenceService>(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<IWorkspaceService>(serviceIdentifier.Workspace).getActiveWorkspace())?.id;
|
||||||
|
} else {
|
||||||
|
// Use fixed workspace
|
||||||
|
targetWorkspaceId = tidgiMiniWindowFixedWorkspaceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { shouldSync, targetWorkspaceId };
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,7 @@ import { Search } from './sections/Search';
|
||||||
import { Sync } from './sections/Sync';
|
import { Sync } from './sections/Sync';
|
||||||
import { System } from './sections/System';
|
import { System } from './sections/System';
|
||||||
import { TiddlyWiki } from './sections/TiddlyWiki';
|
import { TiddlyWiki } from './sections/TiddlyWiki';
|
||||||
|
import { TidGiMiniWindow } from './sections/TidGiMiniWindow';
|
||||||
import { Updates } from './sections/Updates';
|
import { Updates } from './sections/Updates';
|
||||||
import { SectionSideBar } from './SectionsSideBar';
|
import { SectionSideBar } from './SectionsSideBar';
|
||||||
import { usePreferenceSections } from './useSections';
|
import { usePreferenceSections } from './useSections';
|
||||||
|
|
@ -66,6 +67,7 @@ export default function Preferences(): React.JSX.Element {
|
||||||
<Inner>
|
<Inner>
|
||||||
<TiddlyWiki sections={sections} requestRestartCountDown={requestRestartCountDown} />
|
<TiddlyWiki sections={sections} requestRestartCountDown={requestRestartCountDown} />
|
||||||
<General sections={sections} requestRestartCountDown={requestRestartCountDown} />
|
<General sections={sections} requestRestartCountDown={requestRestartCountDown} />
|
||||||
|
<TidGiMiniWindow sections={sections} />
|
||||||
<Sync sections={sections} requestRestartCountDown={requestRestartCountDown} />
|
<Sync sections={sections} requestRestartCountDown={requestRestartCountDown} />
|
||||||
<ExternalAPI sections={sections} />
|
<ExternalAPI sections={sections} />
|
||||||
<AIAgent sections={sections} />
|
<AIAgent sections={sections} />
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export function AIAgent(props: ISectionProps): React.JSX.Element {
|
||||||
'AIAgent: fetch agent database info failed',
|
'AIAgent: fetch agent database info failed',
|
||||||
{
|
{
|
||||||
function: 'AIAgent.fetchInfo',
|
function: 'AIAgent.fetchInfo',
|
||||||
error: String(error),
|
error,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +54,7 @@ export function AIAgent(props: ISectionProps): React.JSX.Element {
|
||||||
'AIAgent: open database folder failed',
|
'AIAgent: open database folder failed',
|
||||||
{
|
{
|
||||||
function: 'AIAgent.openDatabaseFolder',
|
function: 'AIAgent.openDatabaseFolder',
|
||||||
error: String(error),
|
error,
|
||||||
path: agentInfo.path,
|
path: agentInfo.path,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -118,7 +118,7 @@ export function AIAgent(props: ISectionProps): React.JSX.Element {
|
||||||
'AIAgent: delete agent database failed',
|
'AIAgent: delete agent database failed',
|
||||||
{
|
{
|
||||||
function: 'AIAgent.handleDelete',
|
function: 'AIAgent.handleDelete',
|
||||||
error: String(error),
|
error,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export function DeveloperTools(props: ISectionProps): React.JSX.Element {
|
||||||
'DeveloperTools: fetch externalApi database info failed',
|
'DeveloperTools: fetch externalApi database info failed',
|
||||||
{
|
{
|
||||||
function: 'DeveloperTools.fetchInfo',
|
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',
|
'DeveloperTools: open V8 cache folder failed',
|
||||||
{
|
{
|
||||||
function: 'DeveloperTools.openV8CacheFolder',
|
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',
|
'DeveloperTools: open externalApi database folder failed',
|
||||||
{
|
{
|
||||||
function: 'DeveloperTools.openExternalApiDatabaseFolder',
|
function: 'DeveloperTools.openExternalApiDatabaseFolder',
|
||||||
error: String(error),
|
error,
|
||||||
path: externalApiInfo.path,
|
path: externalApiInfo.path,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -202,7 +202,7 @@ export function DeveloperTools(props: ISectionProps): React.JSX.Element {
|
||||||
'DeveloperTools: delete externalApi database failed',
|
'DeveloperTools: delete externalApi database failed',
|
||||||
{
|
{
|
||||||
function: 'DeveloperTools.handleDelete',
|
function: 'DeveloperTools.handleDelete',
|
||||||
error: String(error),
|
error,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export function Downloads(props: Required<ISectionProps>): React.JSX.Element {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error: unknown) => {
|
.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 });
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue