Fix/start error (#654)

* fix: lint

* chore: upgrade electron-ipc-cat to add try catch but useless

IPC Server: Sending response {
channel: 'ContextChannel',
request: { type: 'apply', propKey: 'get', args: [ 'supportedLanguagesMap' ] },
correlationId: '0.36061460136077916',
result: {}
}
Error sending from webFrameMain: Error: Failed to serialize arguments
at WebFrameMain.s.send (node:electron/js2c/browser_init:2:94282)
at WebContents.b.send (node:electron/js2c/browser_init:2:78703)
at I:\github\TidGi-Desktop.vite\build\main-BW_u7Pqi.js:39200:28

IPC Server: Sending response {
channel: 'ContextChannel',
request: { type: 'apply', propKey: 'get', args: [ 'supportedLanguagesMap' ] },
correlationId: '0.7064988939670734',
result: {}
}
Error sending from webFrameMain: Error: Failed to serialize arguments
at WebFrameMain.s.send (node:electron/js2c/browser_init:2:94282)
at WebContents.b.send (node:electron/js2c/browser_init:2:78703)
at I:\github\TidGi-Desktop.vite\build\main-BW_u7Pqi.js:39200:28

Proxy 对象不能被序列化

* fix: process.resourcesPath changes during app initialization, need to wait for it when start with scheme

* fix: Realign workspace view when reopening window to ensure browser view is properly positioned

fixes #626

* feat: api for git-sync-js to get deleted files

* fix: wikiWorker  methods should be async

* log debug not info

* fix: database should init frist before i18n

* fix: better error log when workspace config error

* chore: add maker-msix for windows

* fix: window.meta is not a function when view on browser

* feat: add more git services

* fix: discard file content cause lots of logs

fixes #653

* Update wiki

* test: Git Log window auto-refreshes when files change (only when window is open)

* test: use test id to wait and make test id debug log

* update i18n

* i18n

* lint

* Update test.yml

* Update test.yml

* Update index.tsx
This commit is contained in:
lin onetwo 2025-11-20 17:17:11 +08:00 committed by GitHub
parent 99c6d78078
commit 0e96d94809
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 1416 additions and 600 deletions

View file

@ -33,7 +33,7 @@ jobs:
run: pnpm exec electron-rebuild -f -w better-sqlite3,nsfw run: pnpm exec electron-rebuild -f -w better-sqlite3,nsfw
- name: Run linting - name: Run linting
run: pnpm run lint run: pnpm run lint --max-warnings 0
# Install minimal Linux dependencies for Electron GUI testing https://www.electronjs.org/docs/latest/tutorial/testing-on-headless-ci # Install minimal Linux dependencies for Electron GUI testing https://www.electronjs.org/docs/latest/tutorial/testing-on-headless-ci
- name: Install Linux GUI dependencies - name: Install Linux GUI dependencies

View file

@ -92,3 +92,95 @@ Feature: Git Log Window
# The modified content should be reverted, and make sure file won't be deleted # The modified content should be reverted, and make sure file won't be deleted
Then I should not see a "missing tiddler indicator" element in browser view with selector "[data-tiddler-title='Index']:has-text('')" Then I should not see a "missing tiddler indicator" element in browser view with selector "[data-tiddler-title='Index']:has-text('')"
Then I should not see a "modified content in Index tiddler" element in browser view with selector "[data-tiddler-title='Index']:has-text('Modified Index content')" Then I should not see a "modified content in Index tiddler" element in browser view with selector "[data-tiddler-title='Index']:has-text('Modified Index content')"
@git
Scenario: Discard uncommitted changes for a single file
# Modify the existing Index.tid file to create uncommitted changes
And I modify file "{tmpDir}/wiki/tiddlers/Index.tid" to contain "Discard test content - should be reverted!"
Then I wait for tiddler "Index" to be updated by watch-fs
And I wait for 1 seconds for "git to detect file changes"
# Open Git Log window
When I click menu " > "
And I wait for 1 seconds for "git log window to open"
And I should see "Discard test content" in the browser view content
And I switch to "gitHistory" window
And I wait for the page to load completely
# Wait for git log data to load
And I wait for 2 seconds for "git log data to load"
Then I should see a "uncommitted changes row" element with selector "tr:has-text('')"
# Click on the uncommitted changes row
When I click on a "uncommitted changes row" element with selector "tr:has-text('')"
And I wait for 0.5 seconds for "file list to populate"
# Verify we can see the modified Index.tid file
Then I should see a "Index.tid file in uncommitted list" element with selector "li:has-text('Index.tid')"
# Click on the Index.tid file to select it
When I click on a "Index.tid file in list" element with selector "li:has-text('Index.tid')"
And I wait for 1 seconds for "file diff to load in right panel"
# Verify the file diff panel has loaded by checking for the file name header
Then I should see a "file name header in diff panel" element with selector "h6:has-text('Index.tid')"
# Click the Actions tab in the file diff panel (the one that has the file name above it)
# We need to find the Actions tab that is a sibling of the h6 containing "Index.tid"
When I click on a "actions tab in file diff panel" element with selector "h6:has-text('Index.tid') ~ div button[role='tab']:has-text(''), h6:has-text('Index.tid') ~ div button[role='tab']:has-text('Actions')"
And I wait for 1 seconds for "actions tab content to render"
# Verify the discard changes button exists (only shows for uncommitted changes)
Then I should see a "discard changes button" element with selector "button:has-text(''), button:has-text('Discard changes')"
When I click on a "discard changes button" element with selector "button:has-text(''), button:has-text('Discard changes')"
# Wait for git discard operation to complete
And I wait for 2 seconds for "git discard to complete and UI to refresh"
# Verify the file is no longer in the uncommitted list (should go back to showing no selection)
Then I should not see a "Index.tid file still selected" element with selector "li:has-text('Index.tid')[class*='selected']"
# Switch back to main window to verify the discard
When I switch to "main" window
# Wait for file system events to stabilize after git discard
And I wait for 2 seconds for "file system events to stabilize after git discard"
# The modified content should be discarded
Then I should not see a "modified content in Index tiddler" element in browser view with selector "[data-tiddler-title='Index']:has-text('Discard test content')"
@git
Scenario: Git Log window auto-refreshes when files change (only when window is open)
# Open Git Log window FIRST
When I click menu " > "
And I wait for 1 seconds for "git log window to open"
And I switch to "gitHistory" window
And I wait for the page to load completely
# Should see initial commits
Then I should see a "commit history table" element with selector "table"
# Now modify a file WHILE window is open - this should trigger auto-refresh
When I switch to "main" window
And I modify file "{tmpDir}/wiki/tiddlers/Index.tid" to contain "Modified with window open"
Then I wait for tiddler "Index" to be updated by watch-fs
# Wait for git log to auto-refresh after detecting file changes
And I wait for "git log auto-refreshed after file change" log marker "[test-id-git-log-refreshed]"
# Switch back to git log window
When I switch to "gitHistory" window
# Should see uncommitted changes row appear or update
Then I should see a "uncommitted changes row" element with selector "tr:has-text('')"
# Click on uncommitted changes to verify the modified file is there
When I click on a "uncommitted changes row" element with selector "tr:has-text('')"
And I wait for 1 seconds for "file list to load"
# Should see Index.tid in the uncommitted list
Then I should see a "Index.tid in uncommitted list" element with selector "li:has-text('Index.tid')"
# Now create a NEW file while window is still open
When I switch to "main" window
And I create file "{tmpDir}/wiki/tiddlers/AutoRefreshTest.tid" with content:
"""
created: 20250227070000000
modified: 20250227070000000
title: AutoRefreshTest
tags: TestTag
This file is created to test auto-refresh when git log window is open.
"""
Then I wait for tiddler "AutoRefreshTest" to be added by watch-fs
# Wait for git log to auto-refresh after detecting new file
And I wait for "git log auto-refreshed after new file" log marker "[test-id-git-log-refreshed]"
# Switch back to git log window
When I switch to "gitHistory" window
# The uncommitted changes row should still be visible
Then I should see a "uncommitted changes row" element with selector "tr:has-text('')"
# Click on uncommitted changes again to see both files
When I click on a "uncommitted changes row" element with selector "tr:has-text('')"
And I wait for 1 seconds for "file list to reload"
# Both Index.tid and AutoRefreshTest.tid should be in the uncommitted list
Then I should see a "Index.tid in uncommitted list" element with selector "li:has-text('Index.tid')"
And I should see a "AutoRefreshTest.tid in uncommitted list" element with selector "li:has-text('AutoRefreshTest.tid')"

View file

@ -97,9 +97,9 @@ When('I cleanup test wiki so it could create a new one on start', async function
}, },
}, },
); );
} catch (_error) { } catch (error) {
// If file is corrupted or all retries failed, create empty settings // If file is corrupted or all retries failed, create empty settings
console.warn('Settings file is corrupted or failed to read after retries, recreating with empty workspaces'); console.warn('Settings file is corrupted or failed to read after retries, recreating with empty workspaces', error);
settings = { workspaces: {} }; settings = { workspaces: {} };
} }
@ -129,8 +129,8 @@ When('I cleanup test wiki so it could create a new one on start', async function
}, },
}, },
); );
} catch (_error) { } catch (error) {
console.error('Failed to write settings.json after 3 attempts, continuing anyway'); console.error('Failed to write settings.json after 3 attempts, continuing anyway', error);
} }
}); });

View file

@ -57,6 +57,17 @@ const config: ForgeConfig = {
}; };
}, },
}, },
{
name: '@electron-forge/maker-msix',
platforms: ['win32'],
config: {
packageAssets: 'build-resources/icon.ico',
sign: false,
manifestVariables: {
publisher: 'CN=TiddlyWiki Community',
},
},
},
{ {
name: '@electron-forge/maker-zip', name: '@electron-forge/maker-zip',
platforms: ['darwin'], platforms: ['darwin'],

View file

@ -20,6 +20,7 @@
"InvalidTabType": "Invalid tab type. A chat tab is required.", "InvalidTabType": "Invalid tab type. A chat tab is required.",
"LoadingChat": "Loading conversation...", "LoadingChat": "Loading conversation...",
"StartConversation": "Start a conversation", "StartConversation": "Start a conversation",
"ThinkingProcess": "thinking",
"Untitled": "Untitled" "Untitled": "Untitled"
}, },
"Browser": { "Browser": {
@ -44,6 +45,7 @@
} }
}, },
"Common": { "Common": {
"None": "Not selected"
}, },
"ContextMenu": { "ContextMenu": {
"AddToCurrentSplitView": "Add to current split view", "AddToCurrentSplitView": "Add to current split view",
@ -150,19 +152,20 @@
"ConfigureModelParameters": "configuration parameters", "ConfigureModelParameters": "configuration parameters",
"ConfigureProvider": "Configure {{provider}}", "ConfigureProvider": "Configure {{provider}}",
"ConfirmDeleteAgentDatabase": "Are you sure you want to delete the database containing all AI conversation records? This action cannot be undone.", "ConfirmDeleteAgentDatabase": "Are you sure you want to delete the database containing all AI conversation records? This action cannot be undone.",
"ConfirmDeleteProvider": "Confirm deletion of provider",
"CustomProvider": "Custom Provider", "CustomProvider": "Custom Provider",
"DefaultAIModelSelection": "Default AI Model Selection", "DefaultAIModelSelection": "Default AI Model Selection",
"DefaultAIModelSelectionDescription": "Choose the default AI provider and model to use when not specifically set", "DefaultAIModelSelectionDescription": "Choose the default AI provider and model to use when not specifically set",
"DefaultEmbeddingModelSelection": "Default Embedding Model Selection", "DefaultEmbeddingModelSelection": "Default Embedding Model Selection",
"DefaultEmbeddingModelSelectionDescription": "Choose the default embedding model to use for semantic search and vector operations", "DefaultEmbeddingModelSelectionDescription": "Choose the default embedding model to use for semantic search and vector operations",
"DefaultFreeModelSelection": "Default Free Model Selection",
"DefaultFreeModelSelectionDescription": "Free small model for generating summary titles and backup titles",
"DefaultImageGenerationModelSelection": "Default Image Generation Model Selection", "DefaultImageGenerationModelSelection": "Default Image Generation Model Selection",
"DefaultImageGenerationModelSelectionDescription": "Choose the default image generation model to use for creating images from text", "DefaultImageGenerationModelSelectionDescription": "Choose the default image generation model to use for creating images from text",
"DefaultSpeechModelSelection": "Default Speech Generation Model Selection", "DefaultSpeechModelSelection": "Default Speech Generation Model Selection",
"DefaultSpeechModelSelectionDescription": "Choose the default speech generation model to use for text-to-speech operations", "DefaultSpeechModelSelectionDescription": "Choose the default speech generation model to use for text-to-speech operations",
"DefaultTranscriptionsModelSelection": "Default Transcriptions Model Selection", "DefaultTranscriptionsModelSelection": "Default Transcriptions Model Selection",
"DefaultTranscriptionsModelSelectionDescription": "Choose the default transcriptions model to use for speech-to-text operations", "DefaultTranscriptionsModelSelectionDescription": "Choose the default transcriptions model to use for speech-to-text operations",
"DefaultFreeModelSelection": "Default Free Model Selection",
"DefaultFreeModelSelectionDescription": "Free small model for generating summary titles and backup titles",
"DeleteAgentDatabase": "Delete AI Conversation Database", "DeleteAgentDatabase": "Delete AI Conversation Database",
"DeleteProvider": "Delete provider", "DeleteProvider": "Delete provider",
"DisabledProviderInfo": "This provider is disabled, and its models will not appear in the model selection list", "DisabledProviderInfo": "This provider is disabled, and its models will not appear in the model selection list",
@ -382,6 +385,8 @@
"Description": "Provider and Model Configuration", "Description": "Provider and Model Configuration",
"EmbeddingModel": "Embedding model name for semantic search and vector operations", "EmbeddingModel": "Embedding model name for semantic search and vector operations",
"EmbeddingModelTitle": "Embedding Model", "EmbeddingModelTitle": "Embedding Model",
"FreeModel": "Free small model for generating summary titles and backup titles",
"FreeModelTitle": "Free Model",
"ImageGenerationModel": "Image generation model name for creating images from text", "ImageGenerationModel": "Image generation model name for creating images from text",
"ImageGenerationModelTitle": "Image Generation Model", "ImageGenerationModelTitle": "Image Generation Model",
"Model": "AI model name", "Model": "AI model name",
@ -392,9 +397,7 @@
"SpeechModelTitle": "Speech Model", "SpeechModelTitle": "Speech Model",
"Title": "Provider and Model", "Title": "Provider and Model",
"TranscriptionsModel": "Transcriptions model name for speech-to-text operations", "TranscriptionsModel": "Transcriptions model name for speech-to-text operations",
"TranscriptionsModelTitle": "Transcriptions Model", "TranscriptionsModelTitle": "Transcriptions Model"
"FreeModel": "Free small model for generating summary titles and backup titles",
"FreeModelTitle": "Free Model"
}, },
"RAG": { "RAG": {
"Removal": { "Removal": {
@ -543,7 +546,7 @@
"EditAgentDefinition": "Edit Agent", "EditAgentDefinition": "Edit Agent",
"NewTab": "New Tab", "NewTab": "New Tab",
"NewWeb": "Create a new webpage", "NewWeb": "Create a new webpage",
"SplitView": "" "SplitView": "Split view"
} }
}, },
"Tool": { "Tool": {

View file

@ -9,8 +9,6 @@
"Choose": "Choose", "Choose": "Choose",
"CloneOnlineWiki": "Import Online Wiki", "CloneOnlineWiki": "Import Online Wiki",
"CloneWiki": "Import Online Wiki: ", "CloneWiki": "Import Online Wiki: ",
"CreateLinkFromSubWikiToMainWikiFailed": "Cannot link folder \"{{subWikiPath}}\" to \"{{mainWikiTiddlersFolderPath}}\"",
"CreateLinkFromSubWikiToMainWikiSucceed": "The shortcut to the sub-wiki is successfully created in the main Wiki, and the shortcut that saves the file in the main Wiki will automatically save the file in the sub-Wiki.",
"CreateNewWiki": "Create New Wiki", "CreateNewWiki": "Create New Wiki",
"CreatePrivateRepository": "Create private repository", "CreatePrivateRepository": "Create private repository",
"CreatePublicRepository": "Create a public repository", "CreatePublicRepository": "Create a public repository",
@ -54,7 +52,6 @@
"StartCloningSubWiki": "Start cloning sub-Wiki", "StartCloningSubWiki": "Start cloning sub-Wiki",
"StartCloningWiki": "Start cloning Wiki", "StartCloningWiki": "Start cloning Wiki",
"StartCreatingSubWiki": "Start creating sub-wiki", "StartCreatingSubWiki": "Start creating sub-wiki",
"StartLinkingSubWikiToMainWiki": "Start linking sub-Wiki to main-Wiki",
"StartUsingTemplateToCreateWiki": "Start creating a wiki with templates", "StartUsingTemplateToCreateWiki": "Start creating a wiki with templates",
"SubWikiCreationCompleted": "Sub Wiki is created", "SubWikiCreationCompleted": "Sub Wiki is created",
"SubWorkspace": "Sub Workspace", "SubWorkspace": "Sub Workspace",
@ -79,13 +76,12 @@
}, },
"Cancel": "Cancel", "Cancel": "Cancel",
"ClickForDetails": "Click For Details", "ClickForDetails": "Click For Details",
"Confirm": "confirm",
"ContextMenu": { "ContextMenu": {
"About": "About", "About": "About",
"AddToDictionary": "Add To Dictionary", "AddToDictionary": "Add To Dictionary",
"Back": "Back←", "Back": "Back←",
"BackupNow": "Git Backup Locally", "BackupNow": "Git Backup Locally",
"RevertCommit": "Revert commit: {{message}}",
"UncommittedChanges": "Uncommitted Changes",
"Copy": "Copy", "Copy": "Copy",
"CopyEmailAddress": "Copy Email Address", "CopyEmailAddress": "Copy Email Address",
"CopyImage": "Copy Image", "CopyImage": "Copy Image",
@ -110,36 +106,14 @@
"Reload": "Reload", "Reload": "Reload",
"RestartService": "Restart Service", "RestartService": "Restart Service",
"RestartServiceComplete": "Restart Service Complete", "RestartServiceComplete": "Restart Service Complete",
"RevertCommit": "Revert commit: {{message}}",
"SearchWithGoogle": "Search With Google", "SearchWithGoogle": "Search With Google",
"SyncNow": "Sync To Cloud", "SyncNow": "Sync To Cloud",
"TidGiSupport": "TidGi Support", "TidGiSupport": "TidGi Support",
"TidGiWebsite": "TidGi Website", "TidGiWebsite": "TidGi Website",
"UncommittedChanges": "Uncommitted Changes",
"WithAI": " (AI Naming)" "WithAI": " (AI Naming)"
}, },
"Notification": {
"AdjustSchedule": "Adjust schedule...",
"AdjustTime": "Adjust time",
"Custom": "Custom...",
"Pause10Hours": "10 hours",
"Pause12Hours": "12 hours",
"Pause15Minutes": "15 minutes",
"Pause1Hour": "1 hour",
"Pause2Hours": "2 hours",
"Pause30Minutes": "30 minutes",
"Pause45Minutes": "45 minutes",
"Pause4Hours": "4 hours",
"Pause6Hours": "6 hours",
"Pause8Hours": "8 hours",
"PauseBySchedule": "Pause notifications by schedule...",
"PauseNotifications": "Pause notifications",
"Paused": "Notifications paused",
"PausedUntil": "Notifications paused until {{date}}.",
"PauseUntilNextWeek": "Until next week",
"PauseUntilTomorrow": "Until tomorrow",
"Resume": "Resume notifications",
"Resumed": "Notifications resumed",
"NotificationsNowResumed": "Notifications are now resumed."
},
"Delete": "Delete", "Delete": "Delete",
"Dialog": { "Dialog": {
"CantFindWorkspaceFolderRemoveWorkspace": "Cannot find the workspace folder that was still there before! \nThe folders that should have existed here may have been removed, or there is no wiki in this folder! \nDo you want to remove the workspace?", "CantFindWorkspaceFolderRemoveWorkspace": "Cannot find the workspace folder that was still there before! \nThe folders that should have existed here may have been removed, or there is no wiki in this folder! \nDo you want to remove the workspace?",
@ -148,7 +122,7 @@
"FocusedTiddlerNotFoundTitleDetail": "You can install the FocusedTiddler plugin in CPL.", "FocusedTiddlerNotFoundTitleDetail": "You can install the FocusedTiddler plugin in CPL.",
"Later": "Later", "Later": "Later",
"MadeWithLove": "<0>Made with </0><1>❤</1><2> by </2>", "MadeWithLove": "<0>Made with </0><1>❤</1><2> by </2>",
"NeedCorrectTiddlywikiFolderPath": "The correct path needs to be passed in, and this path cannot be recognized by TiddlyWiki.", "NeedCorrectTiddlywikiFolderPath": "The correct path needs to be passed in, and this path cannot be recognized by TiddlyWiki. Name: {{name}} , Path: {{wikiFolderLocation}}",
"PathPassInCantUse": "The path passed in cannot be used", "PathPassInCantUse": "The path passed in cannot be used",
"RemoveWorkspace": "Remove workspace", "RemoveWorkspace": "Remove workspace",
"ReportBug": "Report Bug", "ReportBug": "Report Bug",
@ -265,51 +239,46 @@
"GitLog": { "GitLog": {
"Actions": "Actions", "Actions": "Actions",
"AllBranches": "All Branches", "AllBranches": "All Branches",
"AndMoreFiles": "+{{count}} more",
"Author": "author", "Author": "author",
"CheckoutCommit": "Check out this commit", "BinaryFileCannotDisplay": "Binary file cannot be displayed as text",
"CommitDetails": "Submission Details",
"CommitFailed": "Commit failed",
"Committing": "Committing...", "Committing": "Committing...",
"CommitSuccess": "Commit successful",
"Details": "Details",
"Reverting": "Reverting...",
"ContentView": "file content", "ContentView": "file content",
"CopyFilePath": "Copy file path",
"CopyHash": "Copy submission hash", "CopyHash": "Copy submission hash",
"CopyRelativeFilePath": "Copy relative file path",
"CurrentVersion": "current version", "CurrentVersion": "current version",
"Date": "date", "Date": "date",
"Details": "Details",
"DiffView": "Difference Comparison", "DiffView": "Difference Comparison",
"DiscardChanges": "Discard changes",
"FailedToLoadDiff": "Failed to load differences", "FailedToLoadDiff": "Failed to load differences",
"Files": "a file", "Files": "a file",
"FilesChanged": "{{count}} file changed", "FilesChanged": "{{count}} file changed",
"FilesChanged_other": "{{count}} files changed", "FilesChanged_other": "{{count}} files changed",
"FilesCount": "number of files",
"Hash": "hash value", "Hash": "hash value",
"IgnoreExtension": "Ignore all .{{ext}} files",
"IgnoreFile": "Ignore file (add to .gitignore)",
"ImageInCommit": "Image at this commit", "ImageInCommit": "Image at this commit",
"ImageNotAvailable": "Image not available", "ImageNotAvailable": "Image not available",
"LoadingFull": "Loading...", "LoadingFull": "Loading...",
"Message": "Submit information", "Message": "Submit information",
"NewImage": "New image (added in this commit)",
"NoCommits": "No submission records", "NoCommits": "No submission records",
"NoFilesChanged": "No file changes", "NoFilesChanged": "No file changes",
"ShowFull": "Show Full", "OpenInExternalEditor": "Open in External editor",
"AndMoreFiles": "+{{count}} more",
"UnknownDate": "Unknown date",
"OpenInGitHub": "Open in GitHub", "OpenInGitHub": "Open in GitHub",
"RelativeTime": "relative time", "OpenWithDefaultProgram": "Open with default program",
"PreviousVersion": "Previous version",
"RevertCommit": "Revert this commit", "RevertCommit": "Revert this commit",
"Reverting": "Reverting...",
"SelectCommit": "Select a submission to view details", "SelectCommit": "Select a submission to view details",
"SelectFileToViewDiff": "Select a file to view differences", "SelectFileToViewDiff": "Select a file to view differences",
"Title": "Git history", "ShowFull": "Show Full",
"PreviousVersion": "Previous version",
"NewImage": "New image (added in this commit)",
"BinaryFileCannotDisplay": "Binary file cannot be displayed as text",
"DiscardChanges": "Discard changes",
"IgnoreFile": "Ignore file (add to .gitignore)",
"IgnoreExtension": "Ignore all .{{ext}} files",
"CopyFilePath": "Copy file path",
"CopyRelativeFilePath": "Copy relative file path",
"ShowInExplorer": "Show in Explorer", "ShowInExplorer": "Show in Explorer",
"OpenInExternalEditor": "Open in External editor", "Title": "Git history",
"OpenWithDefaultProgram": "Open with default program" "UnknownDate": "Unknown date",
"WarningMessage": "Note: Checkout and rollback operations will modify workspace files; proceed with caution."
}, },
"Help": { "Help": {
"Alternatives": "Alternatives", "Alternatives": "Alternatives",
@ -405,15 +374,42 @@
"TidGiMiniWindow": "TidGi Mini Window", "TidGiMiniWindow": "TidGi Mini Window",
"View": "View", "View": "View",
"Wiki": "Wiki", "Wiki": "Wiki",
"WikiWorkspaces": "Wiki Workspaces",
"Window": "Window", "Window": "Window",
"Workspaces": "Workspaces", "Workspaces": "Workspaces",
"ZoomIn": "Zoom In", "ZoomIn": "Zoom In",
"ZoomOut": "Zoom Out" "ZoomOut": "Zoom Out"
}, },
"No": "No", "No": "No",
"Notification": {
"AdjustSchedule": "Adjust schedule...",
"AdjustTime": "Adjust time",
"Custom": "Custom...",
"NotificationsNowResumed": "Notifications are now resumed.",
"Pause10Hours": "10 hours",
"Pause12Hours": "12 hours",
"Pause15Minutes": "15 minutes",
"Pause1Hour": "1 hour",
"Pause2Hours": "2 hours",
"Pause30Minutes": "30 minutes",
"Pause45Minutes": "45 minutes",
"Pause4Hours": "4 hours",
"Pause6Hours": "6 hours",
"Pause8Hours": "8 hours",
"PauseBySchedule": "Pause notifications by schedule...",
"PauseNotifications": "Pause notifications",
"PauseUntilNextWeek": "Until next week",
"PauseUntilTomorrow": "Until tomorrow",
"Paused": "Notifications paused",
"PausedUntil": "Notifications paused until {{date}}.",
"Resume": "Resume notifications",
"Resumed": "Notifications resumed"
},
"Open": "Open", "Open": "Open",
"Preference": { "Preference": {
"AIGenerateBackupTitle": "AI Generate Backup Title",
"AIGenerateBackupTitleDescription": "Use AI to automatically generate Git backup titles based on changes, enabled by default",
"AIGenerateBackupTitleTimeout": "AI Generate Backup Title Timeout",
"AIGenerateBackupTitleTimeoutDescription": "Maximum time to wait for AI to generate title, will use default title if timeout",
"AlwaysOnTop": "Always on top", "AlwaysOnTop": "Always on top",
"AlwaysOnTopDetail": "Keep TidGis main window always on top of other windows, and will not be covered by other windows", "AlwaysOnTopDetail": "Keep TidGis 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.",
@ -494,6 +490,7 @@
"SearchEmbeddingStatusIdle": "No embeddings generated", "SearchEmbeddingStatusIdle": "No embeddings generated",
"SearchEmbeddingUpdate": "Update Embeddings", "SearchEmbeddingUpdate": "Update Embeddings",
"SearchNoWorkspaces": "No workspaces found", "SearchNoWorkspaces": "No workspaces found",
"Seconds": "Seconds",
"SelectWorkspace": "Select workspace", "SelectWorkspace": "Select workspace",
"ShareBrowsingData": "Share browsing data (cookies, cache) between workspaces, if this is off, you can login into different 3rd party service in each workspace.", "ShareBrowsingData": "Share browsing data (cookies, cache) between workspaces, if this is off, you can login into different 3rd party service in each workspace.",
"ShowSideBar": "Show SideBar", "ShowSideBar": "Show SideBar",
@ -514,11 +511,6 @@
"SyncIntervalDescription": "After this length of time, it will automatically start backing up to Github, if is a local workspace it will create a local git backup (take effect after restart app)", "SyncIntervalDescription": "After this length of time, it will automatically start backing up to Github, if is a local workspace it will create a local git backup (take effect after restart app)",
"SyncOnlyWhenNoDraft": "Sync only when there are no drafts", "SyncOnlyWhenNoDraft": "Sync only when there are no drafts",
"SyncOnlyWhenNoDraftDescription": "Check if there are drafts or WYSIWYG editing before synchronizing, if so, it will not be synchronized this time, preventing the drafts from being synchronized to your blog. \n(Not working for sync-before-shutdown, for you may want to bring drafts from one computer to another to continue editing)", "SyncOnlyWhenNoDraftDescription": "Check if there are drafts or WYSIWYG editing before synchronizing, if so, it will not be synchronized this time, preventing the drafts from being synchronized to your blog. \n(Not working for sync-before-shutdown, for you may want to bring drafts from one computer to another to continue editing)",
"AIGenerateBackupTitle": "AI Generate Backup Title",
"AIGenerateBackupTitleDescription": "Use AI to automatically generate Git backup titles based on changes, enabled by default",
"AIGenerateBackupTitleTimeout": "AI Generate Backup Title Timeout",
"AIGenerateBackupTitleTimeoutDescription": "Maximum time to wait for AI to generate title, will use default title if timeout",
"Seconds": "Seconds",
"System": "System", "System": "System",
"SystemDefaultTheme": "System Defalut Theme", "SystemDefaultTheme": "System Defalut Theme",
"TestNotification": "Test notifications", "TestNotification": "Test notifications",
@ -589,13 +581,7 @@
"ReloadCurrentWorkspace": "Reload Current Workspace", "ReloadCurrentWorkspace": "Reload Current Workspace",
"RemoveCurrentWorkspace": "Remove Current Workspace", "RemoveCurrentWorkspace": "Remove Current Workspace",
"RemoveWorkspace": "Remove Workspace", "RemoveWorkspace": "Remove Workspace",
"CommitNow": "Commit Now",
"CommitNowQuick": "Quick Commit",
"CommitNowWithAI": "Generate AI Message and Commit",
"RemoveWorkspaceAndDelete": "Remove workspace and delete Wiki folder from the disk", "RemoveWorkspaceAndDelete": "Remove workspace and delete Wiki folder from the disk",
"SyncNow": "Sync Now",
"SyncNowQuick": "Quick Sync",
"SyncNowWithAI": "Generate AI Message and Sync",
"ViewGitHistory": "View Backup History (Git)", "ViewGitHistory": "View Backup History (Git)",
"WakeUpWorkspace": "WakeUp Workspace" "WakeUpWorkspace": "WakeUp Workspace"
}, },

View file

@ -20,6 +20,7 @@
"InvalidTabType": "Type d'onglet non valide. Un onglet de chat est requis.", "InvalidTabType": "Type d'onglet non valide. Un onglet de chat est requis.",
"LoadingChat": "Chargement de la conversation en cours...", "LoadingChat": "Chargement de la conversation en cours...",
"StartConversation": "commencer la conversation", "StartConversation": "commencer la conversation",
"ThinkingProcess": "Réfléchir",
"Untitled": "Sans titre" "Untitled": "Sans titre"
}, },
"Browser": { "Browser": {
@ -44,6 +45,7 @@
} }
}, },
"Common": { "Common": {
"None": "non sélectionné"
}, },
"ContextMenu": { "ContextMenu": {
"AddToCurrentSplitView": "ajouter à la division d'écran actuelle", "AddToCurrentSplitView": "ajouter à la division d'écran actuelle",
@ -105,6 +107,15 @@
"Title": "Définition de l'agent éditeur" "Title": "Définition de l'agent éditeur"
}, },
"ModelFeature": { "ModelFeature": {
"Embedding": "intégration",
"Free": "gratuit",
"ImageGeneration": "génération d'images",
"Language": "langue",
"Reasoning": "raisonnement",
"Speech": "synthèse vocale",
"ToolCalling": "appel d'outil",
"Transcriptions": "reconnaissance vocale",
"Vision": "visuel"
}, },
"ModelSelector": { "ModelSelector": {
"Model": "modèle", "Model": "modèle",
@ -138,6 +149,7 @@
"ConfigureProvider": "Configurer {{provider}}", "ConfigureProvider": "Configurer {{provider}}",
"ConfirmDelete": "Confirmer la suppression", "ConfirmDelete": "Confirmer la suppression",
"ConfirmDeleteAgentDatabase": "Êtes-vous sûr de vouloir supprimer la base de données contenant tous les historiques de conversation avec l'IA ? Cette action est irréversible.", "ConfirmDeleteAgentDatabase": "Êtes-vous sûr de vouloir supprimer la base de données contenant tous les historiques de conversation avec l'IA ? Cette action est irréversible.",
"ConfirmDeleteProvider": "Confirmer la suppression du fournisseur",
"CustomProvider": "Fournisseur personnalisé", "CustomProvider": "Fournisseur personnalisé",
"DefaultAIModelSelection": "Sélection par défaut du modèle AI", "DefaultAIModelSelection": "Sélection par défaut du modèle AI",
"DefaultAIModelSelectionDescription": "Choisissez le fournisseur AI et le modèle par défaut à utiliser lorsqu'aucun n'est spécifiquement défini", "DefaultAIModelSelectionDescription": "Choisissez le fournisseur AI et le modèle par défaut à utiliser lorsqu'aucun n'est spécifiquement défini",
@ -541,7 +553,7 @@
"EditAgentDefinition": "Agent éditorial intelligent", "EditAgentDefinition": "Agent éditorial intelligent",
"NewTab": "Nouvel onglet", "NewTab": "Nouvel onglet",
"NewWeb": "nouvelle page web", "NewWeb": "nouvelle page web",
"SplitView": "" "SplitView": "Affichage divisé"
} }
}, },
"Tool": { "Tool": {

View file

@ -9,8 +9,6 @@
"Choose": "Choisir", "Choose": "Choisir",
"CloneOnlineWiki": "Importer un Wiki en ligne", "CloneOnlineWiki": "Importer un Wiki en ligne",
"CloneWiki": "Importer un Wiki en ligne : ", "CloneWiki": "Importer un Wiki en ligne : ",
"CreateLinkFromSubWikiToMainWikiFailed": "Impossible de lier le dossier \"{{subWikiPath}}\" à \"{{mainWikiTiddlersFolderPath}}\"",
"CreateLinkFromSubWikiToMainWikiSucceed": "Le raccourci vers le sous-wiki est créé avec succès dans le Wiki principal, et le raccourci qui enregistre le fichier dans le Wiki principal enregistrera automatiquement le fichier dans le sous-wiki.",
"CreateNewWiki": "Créer un nouveau Wiki", "CreateNewWiki": "Créer un nouveau Wiki",
"CreatePrivateRepository": "Créer un dépôt privé", "CreatePrivateRepository": "Créer un dépôt privé",
"CreatePublicRepository": "Créer un dépôt public", "CreatePublicRepository": "Créer un dépôt public",
@ -54,7 +52,6 @@
"StartCloningSubWiki": "Commencer à cloner le sous-wiki", "StartCloningSubWiki": "Commencer à cloner le sous-wiki",
"StartCloningWiki": "Commencer à cloner le Wiki", "StartCloningWiki": "Commencer à cloner le Wiki",
"StartCreatingSubWiki": "Commencer à créer le sous-wiki", "StartCreatingSubWiki": "Commencer à créer le sous-wiki",
"StartLinkingSubWikiToMainWiki": "Commencer à lier le sous-wiki au Wiki principal",
"StartUsingTemplateToCreateWiki": "Commencer à créer un wiki avec des modèles", "StartUsingTemplateToCreateWiki": "Commencer à créer un wiki avec des modèles",
"SubWikiCreationCompleted": "Le sous-wiki est créé", "SubWikiCreationCompleted": "Le sous-wiki est créé",
"SubWorkspace": "Espace de travail secondaire", "SubWorkspace": "Espace de travail secondaire",
@ -79,6 +76,7 @@
}, },
"Cancel": "Annuler", "Cancel": "Annuler",
"ClickForDetails": "Cliquez pour plus de détails", "ClickForDetails": "Cliquez pour plus de détails",
"Confirm": "confirmer",
"ContextMenu": { "ContextMenu": {
"About": "À propos", "About": "À propos",
"AddToDictionary": "Ajouter au dictionnaire", "AddToDictionary": "Ajouter au dictionnaire",
@ -108,34 +106,13 @@
"Reload": "Recharger", "Reload": "Recharger",
"RestartService": "Redémarrer le service", "RestartService": "Redémarrer le service",
"RestartServiceComplete": "Redémarrage du service terminé", "RestartServiceComplete": "Redémarrage du service terminé",
"RevertCommit": "Réinitialiser la validation : {{message}}",
"SearchWithGoogle": "Rechercher avec Google", "SearchWithGoogle": "Rechercher avec Google",
"SyncNow": "Synchroniser avec le cloud", "SyncNow": "Synchroniser avec le cloud",
"TidGiSupport": "Support TidGi", "TidGiSupport": "Support TidGi",
"TidGiWebsite": "Site web TidGi" "TidGiWebsite": "Site web TidGi",
}, "UncommittedChanges": "modifications non soumises",
"Notification": { "WithAI": "(Sauvegarde IA - Nommage)"
"AdjustSchedule": "Ajuster le calendrier...",
"AdjustTime": "Ajuster l'heure",
"Custom": "Personnalisé...",
"Pause10Hours": "10 heures",
"Pause12Hours": "12 heures",
"Pause15Minutes": "15 minutes",
"Pause1Hour": "1 heure",
"Pause2Hours": "2 heures",
"Pause30Minutes": "30 minutes",
"Pause45Minutes": "45 minutes",
"Pause4Hours": "4 heures",
"Pause6Hours": "6 heures",
"Pause8Hours": "8 heures",
"PauseBySchedule": "Pause notifications selon le calendrier...",
"PauseNotifications": "Pause notifications",
"Paused": "Notifications en pause",
"PausedUntil": "Notifications en pause jusqu'à {{date}}.",
"PauseUntilNextWeek": "Jusqu'à la semaine prochaine",
"PauseUntilTomorrow": "Jusqu'à demain",
"Resume": "Reprendre les notifications",
"Resumed": "Notifications reprises",
"NotificationsNowResumed": "Les notifications sont maintenant reprises."
}, },
"Delete": "Supprimer", "Delete": "Supprimer",
"Dialog": { "Dialog": {
@ -145,7 +122,7 @@
"FocusedTiddlerNotFoundTitleDetail": "Vous pouvez installer le plugin FocusedTiddler sur CPL.", "FocusedTiddlerNotFoundTitleDetail": "Vous pouvez installer le plugin FocusedTiddler sur CPL.",
"Later": "Plus tard", "Later": "Plus tard",
"MadeWithLove": "<0>Fait avec </0><1>❤</1><2> par </2>", "MadeWithLove": "<0>Fait avec </0><1>❤</1><2> par </2>",
"NeedCorrectTiddlywikiFolderPath": "Le chemin correct doit être passé, et ce chemin ne peut pas être reconnu par TiddlyWiki.", "NeedCorrectTiddlywikiFolderPath": "Le chemin correct doit être passé, et ce chemin ne peut pas être reconnu par TiddlyWiki. Nom : {{name}} , Chemin : {{wikiFolderLocation}}",
"PathPassInCantUse": "Le chemin passé ne peut pas être utilisé", "PathPassInCantUse": "Le chemin passé ne peut pas être utilisé",
"RemoveWorkspace": "Supprimer l'espace de travail", "RemoveWorkspace": "Supprimer l'espace de travail",
"ReportBug": "Signaler un bug", "ReportBug": "Signaler un bug",
@ -260,30 +237,48 @@
}, },
"ErrorMessage": "Message d'erreur", "ErrorMessage": "Message d'erreur",
"GitLog": { "GitLog": {
"Actions": "opération",
"AllBranches": "toutes les branches",
"AndMoreFiles": "Il reste encore {{count}} fichier(s)",
"Author": "auteur", "Author": "auteur",
"CheckoutCommit": "Vérifier cette validation", "BinaryFileCannotDisplay": "Le fichier binaire ne peut pas être affiché sous forme de texte.",
"CommitDetails": "détails de soumission", "Committing": "En cours de soumission...",
"ContentView": "contenu du fichier", "ContentView": "contenu du fichier",
"CopyFilePath": "copier le chemin du fichier",
"CopyHash": "copier soumettre le hachage", "CopyHash": "copier soumettre le hachage",
"CopyRelativeFilePath": "copier le chemin relatif",
"CurrentVersion": "version actuelle", "CurrentVersion": "version actuelle",
"Date": "date", "Date": "date",
"Details": "détails",
"DiffView": "comparaison des différences", "DiffView": "comparaison des différences",
"DiscardChanges": "abandonner les modifications",
"FailedToLoadDiff": "Échec du chargement des différences", "FailedToLoadDiff": "Échec du chargement des différences",
"Files": "fichier", "Files": "fichier",
"FilesChanged": "fichiers modifiés", "FilesChanged": "fichiers modifiés",
"FilesCount": "nombre de fichiers", "FilesChanged_other": "{{count}} fichiers modifiés",
"Hash": "valeur de hachage", "Hash": "valeur de hachage",
"IgnoreExtension": "Ignorer tous les fichiers .{{ext}}",
"IgnoreFile": "Ignorer les fichiers (ajouter à .gitignore)",
"ImageInCommit": "Image à ce commit", "ImageInCommit": "Image à ce commit",
"ImageNotAvailable": "Image non disponible", "ImageNotAvailable": "Image non disponible",
"LoadingFull": "Chargement en cours...",
"Message": "soumettre les informations", "Message": "soumettre les informations",
"NewImage": "Nouvelle image (ajoutée lors de cette soumission)",
"NoCommits": "Aucun enregistrement soumis", "NoCommits": "Aucun enregistrement soumis",
"NoFilesChanged": "Aucun changement de fichier", "NoFilesChanged": "Aucun changement de fichier",
"OpenInExternalEditor": "Ouvrir dans un éditeur externe",
"OpenInGitHub": "Ouvrir dans GitHub", "OpenInGitHub": "Ouvrir dans GitHub",
"RelativeTime": "temps relatif", "OpenWithDefaultProgram": "Ouvrir avec le programme par défaut",
"PreviousVersion": "version avant modification",
"RevertCommit": "Annuler cette validation", "RevertCommit": "Annuler cette validation",
"Reverting": "Retour en arrière...",
"SelectCommit": "Sélectionnez une soumission pour voir les détails", "SelectCommit": "Sélectionnez une soumission pour voir les détails",
"SelectFileToViewDiff": "Sélectionnez un fichier pour voir les différences", "SelectFileToViewDiff": "Sélectionnez un fichier pour voir les différences",
"Title": "Historique Git" "ShowFull": "développer tout",
"ShowInExplorer": "Afficher dans l'Explorateur",
"Title": "Historique Git",
"UnknownDate": "Date inconnue",
"WarningMessage": "Attention : Les opérations de checkout et de rollback modifient les fichiers du répertoire de travail. Veuillez agir avec prudence."
}, },
"Help": { "Help": {
"Alternatives": "Alternatives", "Alternatives": "Alternatives",
@ -385,8 +380,36 @@
"ZoomOut": "Rétrécir" "ZoomOut": "Rétrécir"
}, },
"No": "Non", "No": "Non",
"Notification": {
"AdjustSchedule": "Ajuster le calendrier...",
"AdjustTime": "Ajuster l'heure",
"Custom": "Personnalisé...",
"NotificationsNowResumed": "Les notifications sont maintenant reprises.",
"Pause10Hours": "10 heures",
"Pause12Hours": "12 heures",
"Pause15Minutes": "15 minutes",
"Pause1Hour": "1 heure",
"Pause2Hours": "2 heures",
"Pause30Minutes": "30 minutes",
"Pause45Minutes": "45 minutes",
"Pause4Hours": "4 heures",
"Pause6Hours": "6 heures",
"Pause8Hours": "8 heures",
"PauseBySchedule": "Pause notifications selon le calendrier...",
"PauseNotifications": "Pause notifications",
"PauseUntilNextWeek": "Jusqu'à la semaine prochaine",
"PauseUntilTomorrow": "Jusqu'à demain",
"Paused": "Notifications en pause",
"PausedUntil": "Notifications en pause jusqu'à {{date}}.",
"Resume": "Reprendre les notifications",
"Resumed": "Notifications reprises"
},
"Open": "Ouvrir", "Open": "Ouvrir",
"Preference": { "Preference": {
"AIGenerateBackupTitle": "Sauvegarde générée par IA",
"AIGenerateBackupTitleDescription": "Utiliser l'intelligence artificielle pour générer automatiquement des titres de sauvegarde Git en fonction du contenu modifié, activé par défaut",
"AIGenerateBackupTitleTimeout": "Délai d'expiration de la sauvegarde des titres générés par l'IA",
"AIGenerateBackupTitleTimeoutDescription": "Le temps d'attente maximum pour la génération du titre par l'IA, après quoi le titre par défaut sera utilisé.",
"AlwaysOnTop": "Toujours au-dessus", "AlwaysOnTop": "Toujours au-dessus",
"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.",
@ -399,6 +422,8 @@
"ClearBrowsingDataMessage": "Êtes-vous sûr ? Toutes les données de navigation seront effacées. Cette action ne peut pas être annulée.", "ClearBrowsingDataMessage": "Êtes-vous sûr ? Toutes les données de navigation seront effacées. Cette action ne peut pas être annulée.",
"ConfirmDeleteExternalApiDatabase": "Êtes-vous sûr de vouloir supprimer la base de données contenant les informations de débogage d'API externes ? Cette action est irréversible.", "ConfirmDeleteExternalApiDatabase": "Êtes-vous sûr de vouloir supprimer la base de données contenant les informations de débogage d'API externes ? Cette action est irréversible.",
"DarkTheme": "Thème sombre", "DarkTheme": "Thème sombre",
"DefaultFreeModelSelection": "Modèle gratuit par défaut sélectionné",
"DefaultFreeModelSelectionDescription": "petit modèle gratuit utilisé pour générer des titres de résumé et des titres de sauvegarde",
"DefaultUserName": "Nom d'utilisateur", "DefaultUserName": "Nom d'utilisateur",
"DefaultUserNameDetail": "Le nom d'utilisateur dans le Wiki, cela ne prend effet qu'après redémarrage, cela remplira le champ créateur des tiddlers nouvellement créés ou modifiés. Peut être remplacé par le nom d'utilisateur défini dans les paramètres de l'espace de travail.", "DefaultUserNameDetail": "Le nom d'utilisateur dans le Wiki, cela ne prend effet qu'après redémarrage, cela remplira le champ créateur des tiddlers nouvellement créés ou modifiés. Peut être remplacé par le nom d'utilisateur défini dans les paramètres de l'espace de travail.",
"DeveloperTools": "Outils de développement", "DeveloperTools": "Outils de développement",
@ -451,6 +476,7 @@
"RunOnBackground": "Exécuter en arrière-plan", "RunOnBackground": "Exécuter en arrière-plan",
"RunOnBackgroundDetail": "Lorsque la fenêtre est fermée, continuer à s'exécuter en arrière-plan sans quitter. Restaurer rapidement la fenêtre lors de la réouverture de l'application.", "RunOnBackgroundDetail": "Lorsque la fenêtre est fermée, continuer à s'exécuter en arrière-plan sans quitter. Restaurer rapidement la fenêtre lors de la réouverture de l'application.",
"RunOnBackgroundDetailNotMac": "Recommandé d'activer Attacher à la barre des tâches. Vous pouvez ainsi restaurer la fenêtre en l'utilisant.", "RunOnBackgroundDetailNotMac": "Recommandé d'activer Attacher à la barre des tâches. Vous pouvez ainsi restaurer la fenêtre en l'utilisant.",
"Seconds": "seconde",
"SelectWorkspace": "choisir l'espace de travail", "SelectWorkspace": "choisir l'espace de travail",
"ShareBrowsingData": "Partager les données de navigation (cookies, cache) entre les espaces de travail, si cette option est désactivée, vous pouvez vous connecter à différents services tiers dans chaque espace de travail.", "ShareBrowsingData": "Partager les données de navigation (cookies, cache) entre les espaces de travail, si cette option est désactivée, vous pouvez vous connecter à différents services tiers dans chaque espace de travail.",
"ShowSideBar": "Afficher la barre latérale", "ShowSideBar": "Afficher la barre latérale",
@ -504,6 +530,12 @@
"hardwareAcceleration": "Utiliser l'accélération matérielle lorsque disponible" "hardwareAcceleration": "Utiliser l'accélération matérielle lorsque disponible"
}, },
"Save": "sauvegarder", "Save": "sauvegarder",
"Schema": {
"ProviderModel": {
"FreeModel": "petit modèle gratuit utilisé pour générer des titres de résumé et des titres de sauvegarde",
"FreeModelTitle": "modèle gratuit"
}
},
"Scripting": { "Scripting": {
"ExecutingScript": "Exécution du script" "ExecutingScript": "Exécution du script"
}, },

View file

@ -20,6 +20,7 @@
"InvalidTabType": "無効なタブタイプです。チャットタブが必要です。", "InvalidTabType": "無効なタブタイプです。チャットタブが必要です。",
"LoadingChat": "会話を読み込んでいます...", "LoadingChat": "会話を読み込んでいます...",
"StartConversation": "会話を開始する", "StartConversation": "会話を開始する",
"ThinkingProcess": "考え中",
"Untitled": "無題" "Untitled": "無題"
}, },
"Browser": { "Browser": {
@ -44,6 +45,7 @@
} }
}, },
"Common": { "Common": {
"None": "選択されていません"
}, },
"ContextMenu": { "ContextMenu": {
"AddToCurrentSplitView": "現在の分割画面に追加", "AddToCurrentSplitView": "現在の分割画面に追加",
@ -105,6 +107,15 @@
"Title": "編集エージェントの定義" "Title": "編集エージェントの定義"
}, },
"ModelFeature": { "ModelFeature": {
"Embedding": "埋め込み",
"Free": "無料",
"ImageGeneration": "画像生成",
"Language": "言語",
"Reasoning": "推理",
"Speech": "音声合成",
"ToolCalling": "ツール呼び出し",
"Transcriptions": "音声認識",
"Vision": "視覚"
}, },
"ModelSelector": { "ModelSelector": {
"Model": "モデル", "Model": "モデル",
@ -139,6 +150,7 @@
"ConfirmDelete": "削除を確認", "ConfirmDelete": "削除を確認",
"ConfirmDeleteAgentDatabase": "すべてのAI対話記録を含むデータベースを削除してもよろしいですかこの操作は取り消せません。", "ConfirmDeleteAgentDatabase": "すべてのAI対話記録を含むデータベースを削除してもよろしいですかこの操作は取り消せません。",
"ConfirmDeleteExternalApiDatabase": "外部APIデバッグ情報を含むデータベースを削除してもよろしいですかこの操作は取り消せません。", "ConfirmDeleteExternalApiDatabase": "外部APIデバッグ情報を含むデータベースを削除してもよろしいですかこの操作は取り消せません。",
"ConfirmDeleteProvider": "プロバイダーを削除してよろしいですか",
"CustomProvider": "カスタムプロバイダ", "CustomProvider": "カスタムプロバイダ",
"DefaultAIModelSelection": "デフォルトのAIモデル選択", "DefaultAIModelSelection": "デフォルトのAIモデル選択",
"DefaultAIModelSelectionDescription": "特に設定されていない場合に使用するデフォルトのAIプロバイダーとモデルを選択してください", "DefaultAIModelSelectionDescription": "特に設定されていない場合に使用するデフォルトのAIプロバイダーとモデルを選択してください",
@ -542,7 +554,7 @@
"EditAgentDefinition": "編集エージェント", "EditAgentDefinition": "編集エージェント",
"NewTab": "新しいタブ", "NewTab": "新しいタブ",
"NewWeb": "新しいウェブページを作成", "NewWeb": "新しいウェブページを作成",
"SplitView": "" "SplitView": "分割画面表示"
} }
}, },
"Tool": { "Tool": {

View file

@ -9,8 +9,6 @@
"Choose": "選択", "Choose": "選択",
"CloneOnlineWiki": "オンラインWikiをインポート", "CloneOnlineWiki": "オンラインWikiをインポート",
"CloneWiki": "オンラインWikiをインポート: ", "CloneWiki": "オンラインWikiをインポート: ",
"CreateLinkFromSubWikiToMainWikiFailed": "フォルダ \"{{subWikiPath}}\" を \"{{mainWikiTiddlersFolderPath}}\" にリンクできません",
"CreateLinkFromSubWikiToMainWikiSucceed": "メインWikiにサブWikiのショートカットが正常に作成されました。メインWikiにファイルを保存するショートカットは、自動的にサブWikiにファイルを保存します。",
"CreateNewWiki": "新しいWikiを作成", "CreateNewWiki": "新しいWikiを作成",
"CreatePrivateRepository": "プライベートリポジトリを作成", "CreatePrivateRepository": "プライベートリポジトリを作成",
"CreatePublicRepository": "パブリックリポジトリを作成", "CreatePublicRepository": "パブリックリポジトリを作成",
@ -54,7 +52,6 @@
"StartCloningSubWiki": "サブWikiのクローンを開始", "StartCloningSubWiki": "サブWikiのクローンを開始",
"StartCloningWiki": "Wikiのクローンを開始", "StartCloningWiki": "Wikiのクローンを開始",
"StartCreatingSubWiki": "サブWikiの作成を開始", "StartCreatingSubWiki": "サブWikiの作成を開始",
"StartLinkingSubWikiToMainWiki": "サブWikiをメインWikiにリンクし始める",
"StartUsingTemplateToCreateWiki": "テンプレートを使用してWikiの作成を開始", "StartUsingTemplateToCreateWiki": "テンプレートを使用してWikiの作成を開始",
"SubWikiCreationCompleted": "サブWikiが作成されました", "SubWikiCreationCompleted": "サブWikiが作成されました",
"SubWorkspace": "サブワークスペース", "SubWorkspace": "サブワークスペース",
@ -79,6 +76,7 @@
}, },
"Cancel": "キャンセル", "Cancel": "キャンセル",
"ClickForDetails": "詳細をクリック", "ClickForDetails": "詳細をクリック",
"Confirm": "確認",
"ContextMenu": { "ContextMenu": {
"About": "情報", "About": "情報",
"AddToDictionary": "辞書に追加", "AddToDictionary": "辞書に追加",
@ -108,34 +106,13 @@
"Reload": "リロード", "Reload": "リロード",
"RestartService": "サービスを再起動", "RestartService": "サービスを再起動",
"RestartServiceComplete": "サービスの再起動が完了しました", "RestartServiceComplete": "サービスの再起動が完了しました",
"RevertCommit": "ロールバックコミット:{{message}}",
"SearchWithGoogle": "Googleで検索", "SearchWithGoogle": "Googleで検索",
"SyncNow": "クラウドに同期", "SyncNow": "クラウドに同期",
"TidGiSupport": "TidGiサポート", "TidGiSupport": "TidGiサポート",
"TidGiWebsite": "TidGi公式サイト" "TidGiWebsite": "TidGi公式サイト",
}, "UncommittedChanges": "未提出の変更",
"Notification": { "WithAI": "AIバックアップ命名"
"AdjustSchedule": "スケジュールを調整...",
"AdjustTime": "時間を調整",
"Custom": "カスタム...",
"Pause10Hours": "10時間",
"Pause12Hours": "12時間",
"Pause15Minutes": "15分",
"Pause1Hour": "1時間",
"Pause2Hours": "2時間",
"Pause30Minutes": "30分",
"Pause45Minutes": "45分",
"Pause4Hours": "4時間",
"Pause6Hours": "6時間",
"Pause8Hours": "8時間",
"PauseBySchedule": "スケジュールで通知を一時停止...",
"PauseNotifications": "通知を一時停止",
"Paused": "通知が一時停止されています",
"PausedUntil": "通知は {{date}} まで一時停止されています。",
"PauseUntilNextWeek": "来週まで",
"PauseUntilTomorrow": "明日まで",
"Resume": "通知を再開",
"Resumed": "通知が再開されました",
"NotificationsNowResumed": "通知は現在再開されています。"
}, },
"Delete": "削除", "Delete": "削除",
"Dialog": { "Dialog": {
@ -145,7 +122,7 @@
"FocusedTiddlerNotFoundTitleDetail": "CPL にて FocusedTiddler プラグインをインストールすることができます。", "FocusedTiddlerNotFoundTitleDetail": "CPL にて FocusedTiddler プラグインをインストールすることができます。",
"Later": "後で", "Later": "後で",
"MadeWithLove": "<0>❤</0><1>で作られました</1>", "MadeWithLove": "<0>❤</0><1>で作られました</1>",
"NeedCorrectTiddlywikiFolderPath": "正しいパスを入力する必要があります。このパスはTiddlyWikiで認識できません。", "NeedCorrectTiddlywikiFolderPath": "正しいパスを入力する必要があります。このパスはTiddlyWikiで認識できません。 名前:{{name}} 、パス:{{wikiFolderLocation}}",
"PathPassInCantUse": "入力されたパスは使用できません", "PathPassInCantUse": "入力されたパスは使用できません",
"RemoveWorkspace": "ワークスペースを削除", "RemoveWorkspace": "ワークスペースを削除",
"ReportBug": "バグを報告", "ReportBug": "バグを報告",
@ -260,30 +237,48 @@
}, },
"ErrorMessage": "エラーメッセージ", "ErrorMessage": "エラーメッセージ",
"GitLog": { "GitLog": {
"Actions": "操作",
"AllBranches": "すべてのブランチ",
"AndMoreFiles": "残り {{count}} ファイル",
"Author": "著者", "Author": "著者",
"CheckoutCommit": "このコミットをチェックアウト", "BinaryFileCannotDisplay": "バイナリファイルはテキストとして表示できません",
"CommitDetails": "提出の詳細", "Committing": "送信中...",
"ContentView": "ファイル内容", "ContentView": "ファイル内容",
"CopyFilePath": "ファイルパスをコピー",
"CopyHash": "コピーしてハッシュを提出", "CopyHash": "コピーしてハッシュを提出",
"CopyRelativeFilePath": "相対パスをコピー",
"CurrentVersion": "現在のバージョン", "CurrentVersion": "現在のバージョン",
"Date": "日付", "Date": "日付",
"Details": "詳細",
"DiffView": "差異比較", "DiffView": "差異比較",
"DiscardChanges": "修正を放棄する",
"FailedToLoadDiff": "差分の読み込みに失敗しました", "FailedToLoadDiff": "差分の読み込みに失敗しました",
"Files": "ファイル", "Files": "ファイル",
"FilesChanged": "変更されたファイル", "FilesChanged": "変更されたファイル",
"FilesCount": "ファイル数", "FilesChanged_other": "{{count}} 個のファイルに変更があります",
"Hash": "ハッシュ値", "Hash": "ハッシュ値",
"IgnoreExtension": "すべての .{{ext}} ファイルを無視する",
"IgnoreFile": "ファイルを無視(.gitignoreに追加",
"ImageInCommit": "このコミット時の画像", "ImageInCommit": "このコミット時の画像",
"ImageNotAvailable": "画像が利用できません", "ImageNotAvailable": "画像が利用できません",
"LoadingFull": "読み込み中...",
"Message": "コミットメッセージ", "Message": "コミットメッセージ",
"NewImage": "新しい画像(今回の提出で追加)",
"NoCommits": "提出記録がありません", "NoCommits": "提出記録がありません",
"NoFilesChanged": "ファイルの変更はありません", "NoFilesChanged": "ファイルの変更はありません",
"OpenInExternalEditor": "外部エディタで開く",
"OpenInGitHub": "GitHubで開く", "OpenInGitHub": "GitHubで開く",
"RelativeTime": "相対時間", "OpenWithDefaultProgram": "デフォルトプログラムで開く",
"PreviousVersion": "修正前のバージョン",
"RevertCommit": "このコミットをロールバック", "RevertCommit": "このコミットをロールバック",
"Reverting": "ロールバック中...",
"SelectCommit": "詳細を表示するためにコミットを選択してください", "SelectCommit": "詳細を表示するためにコミットを選択してください",
"SelectFileToViewDiff": "ファイルを選択して差分を表示", "SelectFileToViewDiff": "ファイルを選択して差分を表示",
"Title": "Git 履歴" "ShowFull": "すべて展開する",
"ShowInExplorer": "エクスプローラーで表示",
"Title": "Git 履歴",
"UnknownDate": "日付不明",
"WarningMessage": "注意:チェックアウトとロールバック操作は作業領域のファイルを変更するため、慎重に操作してください。"
}, },
"Help": { "Help": {
"Alternatives": "その他のソース", "Alternatives": "その他のソース",
@ -385,8 +380,36 @@
"ZoomOut": "ズームアウト" "ZoomOut": "ズームアウト"
}, },
"No": "いいえ", "No": "いいえ",
"Notification": {
"AdjustSchedule": "スケジュールを調整...",
"AdjustTime": "時間を調整",
"Custom": "カスタム...",
"NotificationsNowResumed": "通知は現在再開されています。",
"Pause10Hours": "10時間",
"Pause12Hours": "12時間",
"Pause15Minutes": "15分",
"Pause1Hour": "1時間",
"Pause2Hours": "2時間",
"Pause30Minutes": "30分",
"Pause45Minutes": "45分",
"Pause4Hours": "4時間",
"Pause6Hours": "6時間",
"Pause8Hours": "8時間",
"PauseBySchedule": "スケジュールで通知を一時停止...",
"PauseNotifications": "通知を一時停止",
"PauseUntilNextWeek": "来週まで",
"PauseUntilTomorrow": "明日まで",
"Paused": "通知が一時停止されています",
"PausedUntil": "通知は {{date}} まで一時停止されています。",
"Resume": "通知を再開",
"Resumed": "通知が再開されました"
},
"Open": "開く", "Open": "開く",
"Preference": { "Preference": {
"AIGenerateBackupTitle": "AIが生成したバックアップタイトル",
"AIGenerateBackupTitleDescription": "AIを使用して変更内容に基づいて自動的にGitバックアップのタイトルを生成し、デフォルトで有効になっています",
"AIGenerateBackupTitleTimeout": "AI生成バックアップタイトルのタイムアウト時間",
"AIGenerateBackupTitleTimeoutDescription": "AIによるタイトル生成の最大待機時間、タイムアウト時はデフォルトのタイトルを使用します",
"AlwaysOnTop": "常に最前面に表示", "AlwaysOnTop": "常に最前面に表示",
"AlwaysOnTopDetail": "太記のメインウィンドウを常に他のウィンドウの上に表示し、他のウィンドウで覆われないようにする", "AlwaysOnTopDetail": "太記のメインウィンドウを常に他のウィンドウの上に表示し、他のウィンドウで覆われないようにする",
"AntiAntiLeech": "あるウェブサイトはホットリンク防止対策を施しており、特定の画像があなたのWikiに表示されるのをブロックすることがあります。私たちはそのサイトへのリクエストヘッダーをシミュレートすることで、この制限を回避しています。", "AntiAntiLeech": "あるウェブサイトはホットリンク防止対策を施しており、特定の画像があなたのWikiに表示されるのをブロックすることがあります。私たちはそのサイトへのリクエストヘッダーをシミュレートすることで、この制限を回避しています。",
@ -398,6 +421,8 @@
"ClearBrowsingDataDescription": "クッキー、キャッシュなどを消去する", "ClearBrowsingDataDescription": "クッキー、キャッシュなどを消去する",
"ClearBrowsingDataMessage": "本気ですか?すべての閲覧データが消去されます。このアクションは元に戻せません。", "ClearBrowsingDataMessage": "本気ですか?すべての閲覧データが消去されます。このアクションは元に戻せません。",
"DarkTheme": "ダークテーマ", "DarkTheme": "ダークテーマ",
"DefaultFreeModelSelection": "デフォルトの無料モデル選択",
"DefaultFreeModelSelectionDescription": "要約タイトル生成、バックアップタイトル生成に使用する無料の小型モデル",
"DefaultUserName": "デフォルトの編集者名", "DefaultUserName": "デフォルトの編集者名",
"DefaultUserNameDetail": "Wiki でデフォルトで使用される編集者名は、Tiddler の作成または編集時に creator フィールドに入力されます。ワークスペース内で設定された編集者名によって上書きされることがあります。", "DefaultUserNameDetail": "Wiki でデフォルトで使用される編集者名は、Tiddler の作成または編集時に creator フィールドに入力されます。ワークスペース内で設定された編集者名によって上書きされることがあります。",
"DeveloperTools": "開発者ツール", "DeveloperTools": "開発者ツール",
@ -450,6 +475,7 @@
"RunOnBackground": "バックグラウンドで実行を維持する", "RunOnBackground": "バックグラウンドで実行を維持する",
"RunOnBackgroundDetail": "ウィンドウを閉じても終了せず、バックグラウンドで動作を継続します。再度アプリを開くと、すばやくウィンドウが復元されます。", "RunOnBackgroundDetail": "ウィンドウを閉じても終了せず、バックグラウンドで動作を継続します。再度アプリを開くと、すばやくウィンドウが復元されます。",
"RunOnBackgroundDetailNotMac": "太記の小窓を開くことをお勧めします。これにより、メニューバー/タスクバーのアイコンからウィンドウを再び開くことができます。", "RunOnBackgroundDetailNotMac": "太記の小窓を開くことをお勧めします。これにより、メニューバー/タスクバーのアイコンからウィンドウを再び開くことができます。",
"Seconds": "秒",
"SelectWorkspace": "ワークスペースを選択", "SelectWorkspace": "ワークスペースを選択",
"ShareBrowsingData": "ワークスペース間でブラウザデータ(クッキー、キャッシュなど)を共有し、閉じた後は各ワークスペースで異なるサードパーティサービスのアカウントにログインできます。", "ShareBrowsingData": "ワークスペース間でブラウザデータ(クッキー、キャッシュなど)を共有し、閉じた後は各ワークスペースで異なるサードパーティサービスのアカウントにログインできます。",
"ShowSideBar": "サイドバーを表示", "ShowSideBar": "サイドバーを表示",
@ -503,6 +529,12 @@
"hardwareAcceleration": "ハードウェアアクセラレーションを使用する" "hardwareAcceleration": "ハードウェアアクセラレーションを使用する"
}, },
"Save": "保存", "Save": "保存",
"Schema": {
"ProviderModel": {
"FreeModel": "要約タイトルの生成やバックアップタイトルの作成に使用する無料の小型モデル",
"FreeModelTitle": "無料モデル"
}
},
"Scripting": { "Scripting": {
"ExecutingScript": "スクリプトを実行中です" "ExecutingScript": "スクリプトを実行中です"
}, },

View file

@ -20,6 +20,7 @@
"InvalidTabType": "Неверный тип вкладки. Требуется вкладка чата.", "InvalidTabType": "Неверный тип вкладки. Требуется вкладка чата.",
"LoadingChat": "Загрузка диалога...", "LoadingChat": "Загрузка диалога...",
"StartConversation": "начать диалог", "StartConversation": "начать диалог",
"ThinkingProcess": "Размышляю",
"Untitled": "без названия" "Untitled": "без названия"
}, },
"Browser": { "Browser": {
@ -44,6 +45,7 @@
} }
}, },
"Common": { "Common": {
"None": "не выбрано"
}, },
"ContextMenu": { "ContextMenu": {
"AddToCurrentSplitView": "добавить к текущему разделенному экрану", "AddToCurrentSplitView": "добавить к текущему разделенному экрану",
@ -105,6 +107,15 @@
"Title": "редактировать определение интеллектуального агента" "Title": "редактировать определение интеллектуального агента"
}, },
"ModelFeature": { "ModelFeature": {
"Embedding": "встраивание",
"Free": "бесплатно",
"ImageGeneration": "генерация изображений",
"Language": "язык",
"Reasoning": "умозаключение",
"Speech": "синтез речи",
"ToolCalling": "вызов инструмента",
"Transcriptions": "распознавание речи",
"Vision": "визуальный"
}, },
"ModelSelector": { "ModelSelector": {
"Model": "модель", "Model": "модель",
@ -139,6 +150,7 @@
"ConfirmDelete": "Подтвердить удаление", "ConfirmDelete": "Подтвердить удаление",
"ConfirmDeleteAgentDatabase": "Вы уверены, что хотите удалить базу данных со всеми записями диалогов ИИ? Это действие нельзя отменить.", "ConfirmDeleteAgentDatabase": "Вы уверены, что хотите удалить базу данных со всеми записями диалогов ИИ? Это действие нельзя отменить.",
"ConfirmDeleteExternalApiDatabase": "Вы уверены, что хотите удалить базу данных, содержащую отладочную информацию внешнего API? Это действие нельзя отменить.", "ConfirmDeleteExternalApiDatabase": "Вы уверены, что хотите удалить базу данных, содержащую отладочную информацию внешнего API? Это действие нельзя отменить.",
"ConfirmDeleteProvider": "Подтвердите удаление поставщика",
"CustomProvider": "Пользовательский поставщик", "CustomProvider": "Пользовательский поставщик",
"DefaultAIModelSelection": "Выбор AI модели по умолчанию", "DefaultAIModelSelection": "Выбор AI модели по умолчанию",
"DefaultAIModelSelectionDescription": "Выберите поставщика AI и модель, которые будут использоваться по умолчанию", "DefaultAIModelSelectionDescription": "Выберите поставщика AI и модель, которые будут использоваться по умолчанию",
@ -542,7 +554,7 @@
"EditAgentDefinition": "Редакторский интеллектуальный агент", "EditAgentDefinition": "Редакторский интеллектуальный агент",
"NewTab": "Новая вкладка", "NewTab": "Новая вкладка",
"NewWeb": "создать новую веб-страницу", "NewWeb": "создать новую веб-страницу",
"SplitView": "" "SplitView": "разделенный экран"
} }
}, },
"Tool": { "Tool": {

View file

@ -9,8 +9,6 @@
"Choose": "Выбрать", "Choose": "Выбрать",
"CloneOnlineWiki": "Импортировать онлайн Wiki", "CloneOnlineWiki": "Импортировать онлайн Wiki",
"CloneWiki": "Импортировать онлайн Wiki: ", "CloneWiki": "Импортировать онлайн Wiki: ",
"CreateLinkFromSubWikiToMainWikiFailed": "Невозможно создать ссылку из папки \"{{subWikiPath}}\" в \"{{mainWikiTiddlersFolderPath}}\"",
"CreateLinkFromSubWikiToMainWikiSucceed": "Ярлык на под-Wiki успешно создан в основной Wiki, и ярлык, сохраняющий файл в основной Wiki, автоматически сохранит файл в под-Wiki.",
"CreateNewWiki": "Создать новую Wiki", "CreateNewWiki": "Создать новую Wiki",
"CreatePrivateRepository": "Создать частный репозиторий", "CreatePrivateRepository": "Создать частный репозиторий",
"CreatePublicRepository": "Создать публичный репозиторий", "CreatePublicRepository": "Создать публичный репозиторий",
@ -54,7 +52,6 @@
"StartCloningSubWiki": "Начать клонирование под-Wiki", "StartCloningSubWiki": "Начать клонирование под-Wiki",
"StartCloningWiki": "Начать клонирование Wiki", "StartCloningWiki": "Начать клонирование Wiki",
"StartCreatingSubWiki": "Начать создание под-Wiki", "StartCreatingSubWiki": "Начать создание под-Wiki",
"StartLinkingSubWikiToMainWiki": "Начать привязку под-Wiki к основной Wiki",
"StartUsingTemplateToCreateWiki": "Начать создание Wiki с использованием шаблонов", "StartUsingTemplateToCreateWiki": "Начать создание Wiki с использованием шаблонов",
"SubWikiCreationCompleted": "Под-Wiki создана", "SubWikiCreationCompleted": "Под-Wiki создана",
"SubWorkspace": "Подрабочее пространство", "SubWorkspace": "Подрабочее пространство",
@ -79,6 +76,7 @@
}, },
"Cancel": "отменить", "Cancel": "отменить",
"ClickForDetails": "Нажмите, чтобы узнать подробности", "ClickForDetails": "Нажмите, чтобы узнать подробности",
"Confirm": "подтвердить",
"ContextMenu": { "ContextMenu": {
"About": "О программе", "About": "О программе",
"AddToDictionary": "Добавить в словарь", "AddToDictionary": "Добавить в словарь",
@ -108,34 +106,13 @@
"Reload": "Перезагрузить", "Reload": "Перезагрузить",
"RestartService": "Перезапустить сервис", "RestartService": "Перезапустить сервис",
"RestartServiceComplete": "Перезапуск сервиса завершен", "RestartServiceComplete": "Перезапуск сервиса завершен",
"RevertCommit": "Откатить коммит: {{message}}",
"SearchWithGoogle": "Искать в Google", "SearchWithGoogle": "Искать в Google",
"SyncNow": "Синхронизировать с облаком", "SyncNow": "Синхронизировать с облаком",
"TidGiSupport": "Поддержка TidGi", "TidGiSupport": "Поддержка TidGi",
"TidGiWebsite": "Сайт TidGi" "TidGiWebsite": "Сайт TidGi",
}, "UncommittedChanges": "Несохраненные изменения",
"Notification": { "WithAI": "(Резервное копирование ИИ — наименование)"
"AdjustSchedule": "Отрегулировать расписание...",
"AdjustTime": "Отрегулировать время",
"Custom": "Пользовательский...",
"Pause10Hours": "10 часов",
"Pause12Hours": "12 часов",
"Pause15Minutes": "15 минут",
"Pause1Hour": "1 час",
"Pause2Hours": "2 часа",
"Pause30Minutes": "30 минут",
"Pause45Minutes": "45 минут",
"Pause4Hours": "4 часа",
"Pause6Hours": "6 часов",
"Pause8Hours": "8 часов",
"PauseBySchedule": "Приостановить уведомления по расписанию...",
"PauseNotifications": "Приостановить уведомления",
"Paused": "Уведомления приостановлены",
"PausedUntil": "Уведомления приостановлены до {{date}}.",
"PauseUntilNextWeek": "До следующей недели",
"PauseUntilTomorrow": "До завтра",
"Resume": "Возобновить уведомления",
"Resumed": "Уведомления возобновлены",
"NotificationsNowResumed": "Уведомления теперь возобновлены."
}, },
"Delete": "удалить", "Delete": "удалить",
"Dialog": { "Dialog": {
@ -145,7 +122,7 @@
"FocusedTiddlerNotFoundTitleDetail": "Вы можете установить плагин FocusedTiddler в CPL.", "FocusedTiddlerNotFoundTitleDetail": "Вы можете установить плагин FocusedTiddler в CPL.",
"Later": "Позже", "Later": "Позже",
"MadeWithLove": "<0>Есть</0><1> ❤ </1><2>разработчики:</2>", "MadeWithLove": "<0>Есть</0><1> ❤ </1><2>разработчики:</2>",
"NeedCorrectTiddlywikiFolderPath": "Необходимо передать правильный путь, но этот путь не распознается TiddlyWiki.", "NeedCorrectTiddlywikiFolderPath": "Необходимо передать правильный путь, но этот путь не распознается TiddlyWiki. Имя: {{name}} , Путь: {{wikiFolderLocation}}",
"PathPassInCantUse": "Входящий путь недоступен.", "PathPassInCantUse": "Входящий путь недоступен.",
"RemoveWorkspace": "Удалить рабочую область", "RemoveWorkspace": "Удалить рабочую область",
"ReportBug": "Сообщить об ошибке", "ReportBug": "Сообщить об ошибке",
@ -260,30 +237,48 @@
}, },
"ErrorMessage": "сообщение об ошибке", "ErrorMessage": "сообщение об ошибке",
"GitLog": { "GitLog": {
"Actions": "операция",
"AllBranches": "все филиалы",
"AndMoreFiles": "Осталось {{count}} файлов",
"Author": "автор", "Author": "автор",
"CheckoutCommit": "Проверить этот коммит", "BinaryFileCannotDisplay": "Двоичный файл не может быть отображен как текст.",
"CommitDetails": "Детали отправки", "Committing": "Отправка...",
"ContentView": "содержание файла", "ContentView": "содержание файла",
"CopyFilePath": "скопировать путь к файлу",
"CopyHash": "скопировать хэш отправки", "CopyHash": "скопировать хэш отправки",
"CopyRelativeFilePath": "скопировать относительный путь",
"CurrentVersion": "текущая версия", "CurrentVersion": "текущая версия",
"Date": "дата", "Date": "дата",
"Details": "подробности",
"DiffView": "сравнение различий", "DiffView": "сравнение различий",
"DiscardChanges": "отказаться от изменений",
"FailedToLoadDiff": "Не удалось загрузить различия", "FailedToLoadDiff": "Не удалось загрузить различия",
"Files": "файл", "Files": "файл",
"FilesChanged": "измененные файлы", "FilesChanged": "измененные файлы",
"FilesCount": "количество файлов", "FilesChanged_other": "{{count}} файлов изменено",
"Hash": "хэш-значение", "Hash": "хэш-значение",
"IgnoreExtension": "Игнорировать все файлы .{{ext}}",
"IgnoreFile": "Игнорировать файлы (добавить в .gitignore)",
"ImageInCommit": "Изображение в этом коммите", "ImageInCommit": "Изображение в этом коммите",
"ImageNotAvailable": "Изображение недоступно", "ImageNotAvailable": "Изображение недоступно",
"LoadingFull": "Загрузка...",
"Message": "отправить информацию", "Message": "отправить информацию",
"NewImage": "Новые изображения (добавлены в этой отправке)",
"NoCommits": "нет записей о подаче", "NoCommits": "нет записей о подаче",
"NoFilesChanged": "нет изменений файлов", "NoFilesChanged": "нет изменений файлов",
"OpenInExternalEditor": "Открыть во внешнем редакторе",
"OpenInGitHub": "Открыть в GitHub", "OpenInGitHub": "Открыть в GitHub",
"RelativeTime": "относительное время", "OpenWithDefaultProgram": "открыть с помощью программы по умолчанию",
"PreviousVersion": "версия до изменения",
"RevertCommit": "Откатить этот коммит", "RevertCommit": "Откатить этот коммит",
"Reverting": "Откат в процессе...",
"SelectCommit": "Выберите отправку для просмотра деталей", "SelectCommit": "Выберите отправку для просмотра деталей",
"SelectFileToViewDiff": "Выберите файл для просмотра различий", "SelectFileToViewDiff": "Выберите файл для просмотра различий",
"Title": "История Git" "ShowFull": "развернуть все",
"ShowInExplorer": "Показать в проводнике",
"Title": "История Git",
"UnknownDate": "неизвестная дата",
"WarningMessage": "Примечание: операции извлечения и отката изменяют файлы рабочей области, выполняйте их с осторожностью."
}, },
"Help": { "Help": {
"Alternatives": "другие источники", "Alternatives": "другие источники",
@ -385,8 +380,36 @@
"ZoomOut": "Уменьшить" "ZoomOut": "Уменьшить"
}, },
"No": "Нет", "No": "Нет",
"Notification": {
"AdjustSchedule": "Отрегулировать расписание...",
"AdjustTime": "Отрегулировать время",
"Custom": "Пользовательский...",
"NotificationsNowResumed": "Уведомления теперь возобновлены.",
"Pause10Hours": "10 часов",
"Pause12Hours": "12 часов",
"Pause15Minutes": "15 минут",
"Pause1Hour": "1 час",
"Pause2Hours": "2 часа",
"Pause30Minutes": "30 минут",
"Pause45Minutes": "45 минут",
"Pause4Hours": "4 часа",
"Pause6Hours": "6 часов",
"Pause8Hours": "8 часов",
"PauseBySchedule": "Приостановить уведомления по расписанию...",
"PauseNotifications": "Приостановить уведомления",
"PauseUntilNextWeek": "До следующей недели",
"PauseUntilTomorrow": "До завтра",
"Paused": "Уведомления приостановлены",
"PausedUntil": "Уведомления приостановлены до {{date}}.",
"Resume": "Возобновить уведомления",
"Resumed": "Уведомления возобновлены"
},
"Open": "открыть", "Open": "открыть",
"Preference": { "Preference": {
"AIGenerateBackupTitle": "AI создает резервные заголовки",
"AIGenerateBackupTitleDescription": "Использование искусственного интеллекта для автоматического создания заголовков Git-резервных копий на основе изменений, включено по умолчанию.",
"AIGenerateBackupTitleTimeout": "AI создал резервные заголовки с таймаутом",
"AIGenerateBackupTitleTimeoutDescription": "Максимальное время ожидания генерации заголовка ИИ, по истечении которого будет использован заголовок по умолчанию",
"AlwaysOnTop": "Всегда сверху", "AlwaysOnTop": "Всегда сверху",
"AlwaysOnTopDetail": "Детали всегда сверху", "AlwaysOnTopDetail": "Детали всегда сверху",
"AntiAntiLeech": "Анти-анти-слив", "AntiAntiLeech": "Анти-анти-слив",
@ -398,6 +421,8 @@
"ClearBrowsingDataDescription": "Описание очистки данных браузера", "ClearBrowsingDataDescription": "Описание очистки данных браузера",
"ClearBrowsingDataMessage": "Вы уверены? Все данные просмотров будут удалены. Это действие нельзя отменить.", "ClearBrowsingDataMessage": "Вы уверены? Все данные просмотров будут удалены. Это действие нельзя отменить.",
"DarkTheme": "Темная тема", "DarkTheme": "Темная тема",
"DefaultFreeModelSelection": "выбор модели по умолчанию бесплатно",
"DefaultFreeModelSelectionDescription": "бесплатная небольшая модель, используемая для генерации заголовков сводок и резервных заголовков",
"DefaultUserName": "Имя пользователя по умолчанию", "DefaultUserName": "Имя пользователя по умолчанию",
"DefaultUserNameDetail": "Имя пользователя по умолчанию", "DefaultUserNameDetail": "Имя пользователя по умолчанию",
"DeveloperTools": "Инструменты разработчика", "DeveloperTools": "Инструменты разработчика",
@ -450,6 +475,7 @@
"RunOnBackground": "Запуск в фоновом режиме", "RunOnBackground": "Запуск в фоновом режиме",
"RunOnBackgroundDetail": "Детали запуска в фоновом режиме", "RunOnBackgroundDetail": "Детали запуска в фоновом режиме",
"RunOnBackgroundDetailNotMac": "Детали запуска в фоновом режиме (не для Mac)", "RunOnBackgroundDetailNotMac": "Детали запуска в фоновом режиме (не для Mac)",
"Seconds": "секунда",
"SelectWorkspace": "Выбрать рабочую область", "SelectWorkspace": "Выбрать рабочую область",
"ShareBrowsingData": "Делиться данными браузера", "ShareBrowsingData": "Делиться данными браузера",
"ShowSideBar": "Показать боковую панель", "ShowSideBar": "Показать боковую панель",
@ -503,6 +529,12 @@
"hardwareAcceleration": "Аппаратное ускорение" "hardwareAcceleration": "Аппаратное ускорение"
}, },
"Save": "сохранить", "Save": "сохранить",
"Schema": {
"ProviderModel": {
"FreeModel": "бесплатная небольшая модель для генерации заголовков сводок и резервных заголовков",
"FreeModelTitle": "бесплатная модель"
}
},
"Scripting": { "Scripting": {
"ExecutingScript": "Выполнение скрипта" "ExecutingScript": "Выполнение скрипта"
}, },

View file

@ -20,6 +20,7 @@
"InvalidTabType": "无效的标签页类型。需要聊天标签页。", "InvalidTabType": "无效的标签页类型。需要聊天标签页。",
"LoadingChat": "正在加载对话...", "LoadingChat": "正在加载对话...",
"StartConversation": "开始对话", "StartConversation": "开始对话",
"ThinkingProcess": "思考中",
"Untitled": "未命名" "Untitled": "未命名"
}, },
"Browser": { "Browser": {
@ -44,6 +45,7 @@
} }
}, },
"Common": { "Common": {
"None": "未选择"
}, },
"ContextMenu": { "ContextMenu": {
"AddToCurrentSplitView": "添加到当前分屏", "AddToCurrentSplitView": "添加到当前分屏",
@ -150,19 +152,20 @@
"ConfigureModelParameters": "配置参数", "ConfigureModelParameters": "配置参数",
"ConfigureProvider": "配置 {{provider}}", "ConfigureProvider": "配置 {{provider}}",
"ConfirmDeleteAgentDatabase": "确定要删除包含所有智能体对话记录的数据库吗?此操作无法撤销。", "ConfirmDeleteAgentDatabase": "确定要删除包含所有智能体对话记录的数据库吗?此操作无法撤销。",
"ConfirmDeleteProvider": "确认删除提供商",
"CustomProvider": "自定义提供方", "CustomProvider": "自定义提供方",
"DefaultAIModelSelection": "默认人工智能模型选择", "DefaultAIModelSelection": "默认人工智能模型选择",
"DefaultAIModelSelectionDescription": "选择在未具体设置时默认使用人工智能提供商和模型", "DefaultAIModelSelectionDescription": "选择在未具体设置时默认使用人工智能提供商和模型",
"DefaultEmbeddingModelSelection": "默认嵌入模型选择", "DefaultEmbeddingModelSelection": "默认嵌入模型选择",
"DefaultEmbeddingModelSelectionDescription": "选择用于语义搜索和向量操作的默认嵌入模型", "DefaultEmbeddingModelSelectionDescription": "选择用于语义搜索和向量操作的默认嵌入模型",
"DefaultFreeModelSelection": "默认免费模型选择",
"DefaultFreeModelSelectionDescription": "生成总结标题、生成备份标题时使用的免费小模型",
"DefaultImageGenerationModelSelection": "默认图像生成模型选择", "DefaultImageGenerationModelSelection": "默认图像生成模型选择",
"DefaultImageGenerationModelSelectionDescription": "选择用于文字生成图像操作的默认图像生成模型", "DefaultImageGenerationModelSelectionDescription": "选择用于文字生成图像操作的默认图像生成模型",
"DefaultSpeechModelSelection": "默认语音生成模型选择", "DefaultSpeechModelSelection": "默认语音生成模型选择",
"DefaultSpeechModelSelectionDescription": "选择用于文字转语音操作的默认语音生成模型", "DefaultSpeechModelSelectionDescription": "选择用于文字转语音操作的默认语音生成模型",
"DefaultTranscriptionsModelSelection": "默认语音识别模型选择", "DefaultTranscriptionsModelSelection": "默认语音识别模型选择",
"DefaultTranscriptionsModelSelectionDescription": "选择用于语音转文字操作的默认语音识别模型", "DefaultTranscriptionsModelSelectionDescription": "选择用于语音转文字操作的默认语音识别模型",
"DefaultFreeModelSelection": "默认免费模型选择",
"DefaultFreeModelSelectionDescription": "生成总结标题、生成备份标题时使用的免费小模型",
"DeleteAgentDatabase": "删除人工智能对话数据库", "DeleteAgentDatabase": "删除人工智能对话数据库",
"DeleteProvider": "删除提供商", "DeleteProvider": "删除提供商",
"DisabledProviderInfo": "此提供商已禁用,其模型不会在模型选择列表中显示", "DisabledProviderInfo": "此提供商已禁用,其模型不会在模型选择列表中显示",
@ -381,6 +384,8 @@
"Description": "提供商和模型配置", "Description": "提供商和模型配置",
"EmbeddingModel": "用于语义搜索和向量操作的嵌入模型名称", "EmbeddingModel": "用于语义搜索和向量操作的嵌入模型名称",
"EmbeddingModelTitle": "嵌入模型", "EmbeddingModelTitle": "嵌入模型",
"FreeModel": "用于生成总结标题、生成备份标题时使用的免费小模型",
"FreeModelTitle": "免费模型",
"ImageGenerationModel": "用于文字生成图像操作的图像生成模型名称", "ImageGenerationModel": "用于文字生成图像操作的图像生成模型名称",
"ImageGenerationModelTitle": "图像生成模型", "ImageGenerationModelTitle": "图像生成模型",
"Model": "AI 模型名称", "Model": "AI 模型名称",
@ -391,9 +396,7 @@
"SpeechModelTitle": "语音模型", "SpeechModelTitle": "语音模型",
"Title": "提供商模型", "Title": "提供商模型",
"TranscriptionsModel": "用于语音转文字操作的语音识别模型名称", "TranscriptionsModel": "用于语音转文字操作的语音识别模型名称",
"TranscriptionsModelTitle": "语音识别模型", "TranscriptionsModelTitle": "语音识别模型"
"FreeModel": "用于生成总结标题、生成备份标题时使用的免费小模型",
"FreeModelTitle": "免费模型"
}, },
"RAG": { "RAG": {
"Removal": { "Removal": {
@ -542,7 +545,7 @@
"EditAgentDefinition": "编辑智能体", "EditAgentDefinition": "编辑智能体",
"NewTab": "新建标签页", "NewTab": "新建标签页",
"NewWeb": "新建网页", "NewWeb": "新建网页",
"SplitView": "" "SplitView": "分屏展示"
} }
}, },
"Tool": { "Tool": {

View file

@ -9,8 +9,6 @@
"Choose": "选择", "Choose": "选择",
"CloneOnlineWiki": "导入线上知识库", "CloneOnlineWiki": "导入线上知识库",
"CloneWiki": "导入线上知识库: ", "CloneWiki": "导入线上知识库: ",
"CreateLinkFromSubWikiToMainWikiFailed": "无法链接文件夹 \"{{subWikiPath}}\" 到 \"{{mainWikiTiddlersFolderPath}}\"",
"CreateLinkFromSubWikiToMainWikiSucceed": "在主知识库内成功创建子知识库的快捷方式,快捷方式会自动将文件导入子知识库。",
"CreateNewWiki": "创建新知识库", "CreateNewWiki": "创建新知识库",
"CreatePrivateRepository": "创建私有仓库", "CreatePrivateRepository": "创建私有仓库",
"CreatePublicRepository": "创建公开仓库", "CreatePublicRepository": "创建公开仓库",
@ -54,7 +52,6 @@
"StartCloningSubWiki": "开始导入线上子知识库", "StartCloningSubWiki": "开始导入线上子知识库",
"StartCloningWiki": "开始导入线上知识库", "StartCloningWiki": "开始导入线上知识库",
"StartCreatingSubWiki": "开始创建子知识库", "StartCreatingSubWiki": "开始创建子知识库",
"StartLinkingSubWikiToMainWiki": "开始链接子知识库到父知识库",
"StartUsingTemplateToCreateWiki": "开始用模板创建知识库", "StartUsingTemplateToCreateWiki": "开始用模板创建知识库",
"SubWikiCreationCompleted": "子知识库创建完毕", "SubWikiCreationCompleted": "子知识库创建完毕",
"SubWorkspace": "子知识库", "SubWorkspace": "子知识库",
@ -79,13 +76,12 @@
}, },
"Cancel": "取消", "Cancel": "取消",
"ClickForDetails": "点击了解详情", "ClickForDetails": "点击了解详情",
"Confirm": "确认",
"ContextMenu": { "ContextMenu": {
"About": "关于", "About": "关于",
"AddToDictionary": "添加到字典", "AddToDictionary": "添加到字典",
"Back": "后退←", "Back": "后退←",
"BackupNow": "立即本地Git备份", "BackupNow": "立即本地Git备份",
"RevertCommit": "回退提交:{{message}}",
"UncommittedChanges": "未提交的更改",
"Copy": "复制", "Copy": "复制",
"CopyEmailAddress": "复制电子邮件地址", "CopyEmailAddress": "复制电子邮件地址",
"CopyImage": "复制图片", "CopyImage": "复制图片",
@ -110,36 +106,14 @@
"Reload": "刷新", "Reload": "刷新",
"RestartService": "重启服务", "RestartService": "重启服务",
"RestartServiceComplete": "重启服务成功", "RestartServiceComplete": "重启服务成功",
"RevertCommit": "回退提交:{{message}}",
"SearchWithGoogle": "用 Google 搜索", "SearchWithGoogle": "用 Google 搜索",
"SyncNow": "立即同步云端", "SyncNow": "立即同步云端",
"TidGiSupport": "TidGi 用户支持", "TidGiSupport": "TidGi 用户支持",
"TidGiWebsite": "TidGi 官网", "TidGiWebsite": "TidGi 官网",
"UncommittedChanges": "未提交的更改",
"WithAI": "AI备份命名" "WithAI": "AI备份命名"
}, },
"Notification": {
"AdjustSchedule": "调整计划...",
"AdjustTime": "调整时间",
"Custom": "自定义...",
"Pause10Hours": "10 小时",
"Pause12Hours": "12 小时",
"Pause15Minutes": "15 分钟",
"Pause1Hour": "1 小时",
"Pause2Hours": "2 小时",
"Pause30Minutes": "30 分钟",
"Pause45Minutes": "45 分钟",
"Pause4Hours": "4 小时",
"Pause6Hours": "6 小时",
"Pause8Hours": "8 小时",
"PauseBySchedule": "按计划暂停通知...",
"PauseNotifications": "暂停通知",
"Paused": "通知已暂停",
"PausedUntil": "通知暂停至 {{date}}。",
"PauseUntilNextWeek": "至下周",
"PauseUntilTomorrow": "至明天",
"Resume": "恢复通知",
"Resumed": "通知已恢复",
"NotificationsNowResumed": "通知现已恢复。"
},
"Delete": "删除", "Delete": "删除",
"Dialog": { "Dialog": {
"CantFindWorkspaceFolderRemoveWorkspace": "无法找到之前还在该处的工作区知识库文件夹!本应存在于此处的知识库文件夹可能被移走了,或该文件夹内没有知识库!是否移除工作区?", "CantFindWorkspaceFolderRemoveWorkspace": "无法找到之前还在该处的工作区知识库文件夹!本应存在于此处的知识库文件夹可能被移走了,或该文件夹内没有知识库!是否移除工作区?",
@ -148,7 +122,7 @@
"FocusedTiddlerNotFoundTitleDetail": "可以到 CPL 安装 FocusedTiddler 插件", "FocusedTiddlerNotFoundTitleDetail": "可以到 CPL 安装 FocusedTiddler 插件",
"Later": "稍后", "Later": "稍后",
"MadeWithLove": "<0>有</0><1> ❤ </1><2>的开发者:</2>", "MadeWithLove": "<0>有</0><1> ❤ </1><2>的开发者:</2>",
"NeedCorrectTiddlywikiFolderPath": "需要传入正确的路径,而此路径无法被太微识别。", "NeedCorrectTiddlywikiFolderPath": "需要传入正确的路径,而此路径无法被太微识别。工作区名称:{{name}} ,路径:{{wikiFolderLocation}}",
"PathPassInCantUse": "传入的路径无法使用", "PathPassInCantUse": "传入的路径无法使用",
"RemoveWorkspace": "移除工作区", "RemoveWorkspace": "移除工作区",
"ReportBug": "报告错误", "ReportBug": "报告错误",
@ -265,56 +239,46 @@
"GitLog": { "GitLog": {
"Actions": "操作", "Actions": "操作",
"AllBranches": "所有分支", "AllBranches": "所有分支",
"AndMoreFiles": "还有 {{count}} 个文件",
"Author": "作者", "Author": "作者",
"CheckoutCommit": "签出此提交", "BinaryFileCannotDisplay": "二进制文件无法显示为文本",
"CheckoutFailed": "签出失败",
"CheckoutSuccess": "签出成功",
"CommitDetails": "提交详情",
"CommitFailed": "提交失败",
"Committing": "提交中...", "Committing": "提交中...",
"CommitSuccess": "提交成功",
"Reverting": "回退中...",
"ContentView": "文件内容", "ContentView": "文件内容",
"CopyFilePath": "复制文件路径",
"CopyHash": "复制提交哈希", "CopyHash": "复制提交哈希",
"CopyRelativeFilePath": "复制相对路径",
"CurrentVersion": "当前版本", "CurrentVersion": "当前版本",
"Date": "日期", "Date": "日期",
"Details": "详情", "Details": "详情",
"DiffView": "差异对比", "DiffView": "差异对比",
"DiscardChanges": "放弃修改",
"FailedToLoadDiff": "加载差异失败", "FailedToLoadDiff": "加载差异失败",
"Files": "文件", "Files": "文件",
"FilesChanged": "变更了 {{count}} 个文件", "FilesChanged": "变更了 {{count}} 个文件",
"FilesCount": "文件数量", "FilesChanged_other": "{{count}} 个文件有改动",
"Hash": "哈希值", "Hash": "哈希值",
"HashCopied": "哈希已复制到剪贴板", "IgnoreExtension": "忽略所有 .{{ext}} 文件",
"IgnoreFile": "忽略文件(添加到 .gitignore",
"ImageInCommit": "此提交时的图片", "ImageInCommit": "此提交时的图片",
"ImageNotAvailable": "图片不可用", "ImageNotAvailable": "图片不可用",
"LoadingFull": "加载中...", "LoadingFull": "加载中...",
"Message": "提交信息", "Message": "提交信息",
"NewImage": "新图片(本次提交添加)",
"NoCommits": "没有提交记录", "NoCommits": "没有提交记录",
"NoFilesChanged": "没有文件变更", "NoFilesChanged": "没有文件变更",
"ShowFull": "展开全部", "OpenInExternalEditor": "在外部编辑器中打开",
"AndMoreFiles": "还有 {{count}} 个文件",
"UnknownDate": "未知日期",
"OpenInGitHub": "在 GitHub 中打开", "OpenInGitHub": "在 GitHub 中打开",
"RelativeTime": "相对时间", "OpenWithDefaultProgram": "用默认程序打开",
"PreviousVersion": "修改前版本",
"RevertCommit": "回退此提交", "RevertCommit": "回退此提交",
"RevertFailed": "回退失败", "Reverting": "回退中...",
"RevertSuccess": "回退成功",
"SelectCommit": "选择一个提交来查看详情", "SelectCommit": "选择一个提交来查看详情",
"SelectFileToViewDiff": "选择一个文件以查看差异", "SelectFileToViewDiff": "选择一个文件以查看差异",
"Title": "Git 历史记录", "ShowFull": "展开全部",
"WarningMessage": "注意:签出和回退操作会修改工作区文件,请谨慎操作。",
"PreviousVersion": "修改前版本",
"NewImage": "新图片(本次提交添加)",
"BinaryFileCannotDisplay": "二进制文件无法显示为文本",
"DiscardChanges": "放弃修改",
"IgnoreFile": "忽略文件(添加到 .gitignore",
"IgnoreExtension": "忽略所有 .{{ext}} 文件",
"CopyFilePath": "复制文件路径",
"CopyRelativeFilePath": "复制相对路径",
"ShowInExplorer": "在资源管理器中显示", "ShowInExplorer": "在资源管理器中显示",
"OpenInExternalEditor": "在外部编辑器中打开", "Title": "Git 历史记录",
"OpenWithDefaultProgram": "用默认程序打开" "UnknownDate": "未知日期",
"WarningMessage": "注意:签出和回退操作会修改工作区文件,请谨慎操作。"
}, },
"Help": { "Help": {
"Alternatives": "其它源", "Alternatives": "其它源",
@ -410,15 +374,42 @@
"TidGiMiniWindow": "太记小窗", "TidGiMiniWindow": "太记小窗",
"View": "查看", "View": "查看",
"Wiki": "知识库", "Wiki": "知识库",
"WikiWorkspaces": "知识库工作区",
"Window": "窗口", "Window": "窗口",
"Workspaces": "工作区列表", "Workspaces": "工作区列表",
"ZoomIn": "放大", "ZoomIn": "放大",
"ZoomOut": "缩小" "ZoomOut": "缩小"
}, },
"No": "不了", "No": "不了",
"Notification": {
"AdjustSchedule": "调整计划...",
"AdjustTime": "调整时间",
"Custom": "自定义...",
"NotificationsNowResumed": "通知现已恢复。",
"Pause10Hours": "10 小时",
"Pause12Hours": "12 小时",
"Pause15Minutes": "15 分钟",
"Pause1Hour": "1 小时",
"Pause2Hours": "2 小时",
"Pause30Minutes": "30 分钟",
"Pause45Minutes": "45 分钟",
"Pause4Hours": "4 小时",
"Pause6Hours": "6 小时",
"Pause8Hours": "8 小时",
"PauseBySchedule": "按计划暂停通知...",
"PauseNotifications": "暂停通知",
"PauseUntilNextWeek": "至下周",
"PauseUntilTomorrow": "至明天",
"Paused": "通知已暂停",
"PausedUntil": "通知暂停至 {{date}}。",
"Resume": "恢复通知",
"Resumed": "通知已恢复"
},
"Open": "打开", "Open": "打开",
"Preference": { "Preference": {
"AIGenerateBackupTitle": "AI 生成备份标题",
"AIGenerateBackupTitleDescription": "使用人工智能根据变更内容自动生成 Git 备份标题,默认开启",
"AIGenerateBackupTitleTimeout": "AI 生成备份标题超时时间",
"AIGenerateBackupTitleTimeoutDescription": "等待 AI 生成标题的最长时间,超时后将使用默认标题",
"AlwaysOnTop": "保持窗口在其他窗口上方", "AlwaysOnTop": "保持窗口在其他窗口上方",
"AlwaysOnTopDetail": "让太记的主窗口永远保持在其它窗口上方,不会被其他窗口覆盖", "AlwaysOnTopDetail": "让太记的主窗口永远保持在其它窗口上方,不会被其他窗口覆盖",
"AntiAntiLeech": "有的网站做了防盗链,会阻止某些图片在你的知识库上显示,我们通过模拟访问该网站的请求头来绕过这种限制。", "AntiAntiLeech": "有的网站做了防盗链,会阻止某些图片在你的知识库上显示,我们通过模拟访问该网站的请求头来绕过这种限制。",
@ -500,6 +491,7 @@
"SearchEmbeddingStatusIdle": "未生成嵌入", "SearchEmbeddingStatusIdle": "未生成嵌入",
"SearchEmbeddingUpdate": "更新嵌入", "SearchEmbeddingUpdate": "更新嵌入",
"SearchNoWorkspaces": "未找到工作区", "SearchNoWorkspaces": "未找到工作区",
"Seconds": "秒",
"SelectWorkspace": "选择工作区", "SelectWorkspace": "选择工作区",
"ShareBrowsingData": "在工作区之间共享浏览器数据cookies、缓存等关闭后可以每个工作区登不同的第三方服务账号。", "ShareBrowsingData": "在工作区之间共享浏览器数据cookies、缓存等关闭后可以每个工作区登不同的第三方服务账号。",
"ShowSideBar": "显示侧边栏", "ShowSideBar": "显示侧边栏",
@ -520,11 +512,6 @@
"SyncIntervalDescription": "每经过这段长度的时间后,就会自动开始备份到 Github如果工作区是本地工作区则会创建本地备份重启后生效", "SyncIntervalDescription": "每经过这段长度的时间后,就会自动开始备份到 Github如果工作区是本地工作区则会创建本地备份重启后生效",
"SyncOnlyWhenNoDraft": "在没有草稿时才同步", "SyncOnlyWhenNoDraft": "在没有草稿时才同步",
"SyncOnlyWhenNoDraftDescription": "在同步前检查有没有草稿或处于所见即所得编辑状态的条目,如果有则本次不同步,防止将草稿同步到你的博客里。(对关机前自动同步无效,毕竟你很可能希望将草稿从一台电脑上带到另一台电脑上继续编辑)", "SyncOnlyWhenNoDraftDescription": "在同步前检查有没有草稿或处于所见即所得编辑状态的条目,如果有则本次不同步,防止将草稿同步到你的博客里。(对关机前自动同步无效,毕竟你很可能希望将草稿从一台电脑上带到另一台电脑上继续编辑)",
"AIGenerateBackupTitle": "AI 生成备份标题",
"AIGenerateBackupTitleDescription": "使用人工智能根据变更内容自动生成 Git 备份标题,默认开启",
"AIGenerateBackupTitleTimeout": "AI 生成备份标题超时时间",
"AIGenerateBackupTitleTimeoutDescription": "等待 AI 生成标题的最长时间,超时后将使用默认标题",
"Seconds": "秒",
"System": "系统", "System": "系统",
"SystemDefaultTheme": "系统默认主题色", "SystemDefaultTheme": "系统默认主题色",
"TestNotification": "测试通知功能", "TestNotification": "测试通知功能",

View file

@ -20,6 +20,7 @@
"InvalidTabType": "無效的標籤頁類型。需要聊天標籤頁。", "InvalidTabType": "無效的標籤頁類型。需要聊天標籤頁。",
"LoadingChat": "正在載入對話...", "LoadingChat": "正在載入對話...",
"StartConversation": "開始對話", "StartConversation": "開始對話",
"ThinkingProcess": "思考中",
"Untitled": "未命名" "Untitled": "未命名"
}, },
"Browser": { "Browser": {
@ -44,6 +45,7 @@
} }
}, },
"Common": { "Common": {
"None": "未選擇"
}, },
"ContextMenu": { "ContextMenu": {
"AddToCurrentSplitView": "添加到當前分屏", "AddToCurrentSplitView": "添加到當前分屏",
@ -109,6 +111,15 @@
"Title": "編輯智慧體定義" "Title": "編輯智慧體定義"
}, },
"ModelFeature": { "ModelFeature": {
"Embedding": "嵌入",
"Free": "免費",
"ImageGeneration": "圖像生成",
"Language": "語言",
"Reasoning": "推理",
"Speech": "語音合成",
"ToolCalling": "工具調用",
"Transcriptions": "語音辨識",
"Vision": "視覺"
}, },
"ModelSelector": { "ModelSelector": {
"Model": "模型", "Model": "模型",
@ -141,6 +152,7 @@
"ConfigureModelParameters": "配置參數", "ConfigureModelParameters": "配置參數",
"ConfigureProvider": "配置 {{provider}}", "ConfigureProvider": "配置 {{provider}}",
"ConfirmDeleteAgentDatabase": "確定要刪除包含所有智慧體對話記錄的資料庫嗎?此操作無法撤銷。", "ConfirmDeleteAgentDatabase": "確定要刪除包含所有智慧體對話記錄的資料庫嗎?此操作無法撤銷。",
"ConfirmDeleteProvider": "確認刪除提供商",
"CustomProvider": "自訂提供方", "CustomProvider": "自訂提供方",
"DefaultAIModelSelection": "默認人工智慧模型選擇", "DefaultAIModelSelection": "默認人工智慧模型選擇",
"DefaultAIModelSelectionDescription": "選擇在未具體設置時預設使用人工智慧提供商和模型", "DefaultAIModelSelectionDescription": "選擇在未具體設置時預設使用人工智慧提供商和模型",
@ -529,7 +541,7 @@
"EditAgentDefinition": "編輯智慧體", "EditAgentDefinition": "編輯智慧體",
"NewTab": "新建標籤頁", "NewTab": "新建標籤頁",
"NewWeb": "新建網頁", "NewWeb": "新建網頁",
"SplitView": "" "SplitView": "分屏展示"
} }
}, },
"Tool": { "Tool": {

View file

@ -9,8 +9,6 @@
"Choose": "選擇", "Choose": "選擇",
"CloneOnlineWiki": "導入線上知識庫", "CloneOnlineWiki": "導入線上知識庫",
"CloneWiki": "導入線上知識庫: ", "CloneWiki": "導入線上知識庫: ",
"CreateLinkFromSubWikiToMainWikiFailed": "無法連結文件夾 \"{{subWikiPath}}\" 到 \"{{mainWikiTiddlersFolderPath}}\"",
"CreateLinkFromSubWikiToMainWikiSucceed": "在主知識庫內成功創建子知識庫的捷徑,捷徑會自動將文件導入子知識庫。",
"CreateNewWiki": "創建新知識庫", "CreateNewWiki": "創建新知識庫",
"CreatePrivateRepository": "創建私有倉庫", "CreatePrivateRepository": "創建私有倉庫",
"CreatePublicRepository": "創建公開倉庫", "CreatePublicRepository": "創建公開倉庫",
@ -54,7 +52,6 @@
"StartCloningSubWiki": "開始導入線上子知識庫", "StartCloningSubWiki": "開始導入線上子知識庫",
"StartCloningWiki": "開始導入線上知識庫", "StartCloningWiki": "開始導入線上知識庫",
"StartCreatingSubWiki": "開始創建子知識庫", "StartCreatingSubWiki": "開始創建子知識庫",
"StartLinkingSubWikiToMainWiki": "開始連結子知識庫到父知識庫",
"StartUsingTemplateToCreateWiki": "開始用模板創建知識庫", "StartUsingTemplateToCreateWiki": "開始用模板創建知識庫",
"SubWikiCreationCompleted": "子知識庫創建完畢", "SubWikiCreationCompleted": "子知識庫創建完畢",
"SubWorkspace": "子知識庫", "SubWorkspace": "子知識庫",
@ -79,6 +76,7 @@
}, },
"Cancel": "取消", "Cancel": "取消",
"ClickForDetails": "點擊了解詳情", "ClickForDetails": "點擊了解詳情",
"Confirm": "確認",
"ContextMenu": { "ContextMenu": {
"About": "關於", "About": "關於",
"AddToDictionary": "添加到字典", "AddToDictionary": "添加到字典",
@ -108,34 +106,13 @@
"Reload": "刷新", "Reload": "刷新",
"RestartService": "重啟服務", "RestartService": "重啟服務",
"RestartServiceComplete": "重啟服務成功", "RestartServiceComplete": "重啟服務成功",
"RevertCommit": "回退提交:{{message}}",
"SearchWithGoogle": "用 Google 搜索", "SearchWithGoogle": "用 Google 搜索",
"SyncNow": "立即同步雲端", "SyncNow": "立即同步雲端",
"TidGiSupport": "TidGi 用戶支持", "TidGiSupport": "TidGi 用戶支持",
"TidGiWebsite": "TidGi 官網" "TidGiWebsite": "TidGi 官網",
}, "UncommittedChanges": "未提交的變更",
"Notification": { "WithAI": "AI備份命名"
"AdjustSchedule": "調整計畫...",
"AdjustTime": "調整時間",
"Custom": "自訂...",
"Pause10Hours": "10 小時",
"Pause12Hours": "12 小時",
"Pause15Minutes": "15 分鐘",
"Pause1Hour": "1 小時",
"Pause2Hours": "2 小時",
"Pause30Minutes": "30 分鐘",
"Pause45Minutes": "45 分鐘",
"Pause4Hours": "4 小時",
"Pause6Hours": "6 小時",
"Pause8Hours": "8 小時",
"PauseBySchedule": "按計畫暫停通知...",
"PauseNotifications": "暫停通知",
"Paused": "通知已暫停",
"PausedUntil": "通知暫停至 {{date}}。",
"PauseUntilNextWeek": "至下週",
"PauseUntilTomorrow": "至明天",
"Resume": "恢復通知",
"Resumed": "通知已恢復",
"NotificationsNowResumed": "通知現已恢復。"
}, },
"Delete": "刪除", "Delete": "刪除",
"Dialog": { "Dialog": {
@ -145,7 +122,7 @@
"FocusedTiddlerNotFoundTitleDetail": "可以到 CPL 安裝 FocusedTiddler 插件", "FocusedTiddlerNotFoundTitleDetail": "可以到 CPL 安裝 FocusedTiddler 插件",
"Later": "稍後", "Later": "稍後",
"MadeWithLove": "<0>有</0><1> ❤ </1><2>的開發者:</2>", "MadeWithLove": "<0>有</0><1> ❤ </1><2>的開發者:</2>",
"NeedCorrectTiddlywikiFolderPath": "需要傳入正確的路徑,而此路徑無法被太微識別。", "NeedCorrectTiddlywikiFolderPath": "需要傳入正確的路徑,而此路徑無法被太微識別。工作區名稱:{{name}} ,路徑:{{wikiFolderLocation}}",
"PathPassInCantUse": "傳入的路徑無法使用", "PathPassInCantUse": "傳入的路徑無法使用",
"RemoveWorkspace": "移除工作區", "RemoveWorkspace": "移除工作區",
"ReportBug": "報告錯誤", "ReportBug": "報告錯誤",
@ -260,32 +237,48 @@
}, },
"ErrorMessage": "報錯資訊", "ErrorMessage": "報錯資訊",
"GitLog": { "GitLog": {
"Actions": "操作",
"AllBranches": "所有分支",
"AndMoreFiles": "還有 {{count}} 個文件",
"Author": "作者", "Author": "作者",
"CheckoutCommit": "簽出此提交", "BinaryFileCannotDisplay": "二進位檔案無法顯示為文字",
"CommitDetails": "提交詳情", "Committing": "提交中...",
"ContentView": "檔案內容", "ContentView": "檔案內容",
"CopyFilePath": "複製檔案路徑",
"CopyHash": "複製提交哈希", "CopyHash": "複製提交哈希",
"CopyRelativeFilePath": "複製相對路徑",
"CurrentVersion": "目前版本", "CurrentVersion": "目前版本",
"Date": "日期", "Date": "日期",
"Details": "詳情",
"DiffView": "差異對比", "DiffView": "差異對比",
"DiscardChanges": "放棄修改",
"FailedToLoadDiff": "載入差異失敗", "FailedToLoadDiff": "載入差異失敗",
"Files": "檔案", "Files": "檔案",
"FilesChanged": "變更了 {{count}} 個文件", "FilesChanged": "變更了 {{count}} 個文件",
"FilesCount": "文件數量", "FilesChanged_other": "{{count}} 個檔案有變更",
"Hash": "雜湊值", "Hash": "雜湊值",
"IgnoreExtension": "忽略所有 .{{ext}} 檔案",
"IgnoreFile": "忽略檔案(新增至 .gitignore",
"ImageInCommit": "此提交時的圖片", "ImageInCommit": "此提交時的圖片",
"ImageNotAvailable": "圖片不可用", "ImageNotAvailable": "圖片不可用",
"LoadingFull": "載入中...",
"Message": "提交資訊", "Message": "提交資訊",
"NewImage": "新圖片(本次提交新增)",
"NoCommits": "沒有提交記錄", "NoCommits": "沒有提交記錄",
"NoFilesChanged": "沒有文件變更", "NoFilesChanged": "沒有文件變更",
"AndMoreFiles": "還有 {{count}} 個文件", "OpenInExternalEditor": "在外部編輯器中開啟",
"UnknownDate": "未知日期",
"OpenInGitHub": "在 GitHub 中開啟", "OpenInGitHub": "在 GitHub 中開啟",
"RelativeTime": "相對時間", "OpenWithDefaultProgram": "用預設程式開啟",
"PreviousVersion": "修改前版本",
"RevertCommit": "回退此提交", "RevertCommit": "回退此提交",
"Reverting": "回退中...",
"SelectCommit": "選擇一個提交來查看詳情", "SelectCommit": "選擇一個提交來查看詳情",
"SelectFileToViewDiff": "選擇一個文件以查看差異", "SelectFileToViewDiff": "選擇一個文件以查看差異",
"Title": "Git 歷史記錄" "ShowFull": "展開全部",
"ShowInExplorer": "在資源管理器中顯示",
"Title": "Git 歷史記錄",
"UnknownDate": "未知日期",
"WarningMessage": "注意:簽出和回退操作會修改工作區文件,請謹慎操作。"
}, },
"Help": { "Help": {
"Alternatives": "其它源", "Alternatives": "其它源",
@ -387,8 +380,36 @@
"ZoomOut": "縮小" "ZoomOut": "縮小"
}, },
"No": "不了", "No": "不了",
"Notification": {
"AdjustSchedule": "調整計畫...",
"AdjustTime": "調整時間",
"Custom": "自訂...",
"NotificationsNowResumed": "通知現已恢復。",
"Pause10Hours": "10 小時",
"Pause12Hours": "12 小時",
"Pause15Minutes": "15 分鐘",
"Pause1Hour": "1 小時",
"Pause2Hours": "2 小時",
"Pause30Minutes": "30 分鐘",
"Pause45Minutes": "45 分鐘",
"Pause4Hours": "4 小時",
"Pause6Hours": "6 小時",
"Pause8Hours": "8 小時",
"PauseBySchedule": "按計畫暫停通知...",
"PauseNotifications": "暫停通知",
"PauseUntilNextWeek": "至下週",
"PauseUntilTomorrow": "至明天",
"Paused": "通知已暫停",
"PausedUntil": "通知暫停至 {{date}}。",
"Resume": "恢復通知",
"Resumed": "通知已恢復"
},
"Open": "打開", "Open": "打開",
"Preference": { "Preference": {
"AIGenerateBackupTitle": "AI 生成備份標題",
"AIGenerateBackupTitleDescription": "使用人工智慧根據變更內容自動生成 Git 備份標題,預設開啟",
"AIGenerateBackupTitleTimeout": "AI 生成備份標題逾時時間",
"AIGenerateBackupTitleTimeoutDescription": "等待 AI 生成標題的最長時間,逾時後將使用預設標題",
"AlwaysOnTop": "保持窗口在其他窗口上方", "AlwaysOnTop": "保持窗口在其他窗口上方",
"AlwaysOnTopDetail": "讓太記的主窗口永遠保持在其它窗口上方,不會被其他窗口覆蓋", "AlwaysOnTopDetail": "讓太記的主窗口永遠保持在其它窗口上方,不會被其他窗口覆蓋",
"AntiAntiLeech": "有的網站做了防盜鏈,會阻止某些圖片在你的知識庫上顯示,我們透過模擬訪問該網站的請求頭來繞過這種限制。", "AntiAntiLeech": "有的網站做了防盜鏈,會阻止某些圖片在你的知識庫上顯示,我們透過模擬訪問該網站的請求頭來繞過這種限制。",
@ -402,6 +423,8 @@
"ConfirmDelete": "確認刪除", "ConfirmDelete": "確認刪除",
"ConfirmDeleteExternalApiDatabase": "確定要刪除包含外部 API Debug 資訊的資料庫嗎?此操作無法撤銷。", "ConfirmDeleteExternalApiDatabase": "確定要刪除包含外部 API Debug 資訊的資料庫嗎?此操作無法撤銷。",
"DarkTheme": "黑暗主題", "DarkTheme": "黑暗主題",
"DefaultFreeModelSelection": "預設免費模型選擇",
"DefaultFreeModelSelectionDescription": "產生總結標題、產生備份標題時使用的免費小模型",
"DefaultUserName": "默認編輯者名", "DefaultUserName": "默認編輯者名",
"DefaultUserNameDetail": "在知識庫中預設使用的編輯者名,將在創建或編輯條目時填入 creator 欄位。可以被工作區內設置的編輯者名覆蓋。", "DefaultUserNameDetail": "在知識庫中預設使用的編輯者名,將在創建或編輯條目時填入 creator 欄位。可以被工作區內設置的編輯者名覆蓋。",
"DeleteExternalApiDatabase": "刪除外部 API 資料庫", "DeleteExternalApiDatabase": "刪除外部 API 資料庫",
@ -470,6 +493,7 @@
"SearchEmbeddingStatusIdle": "未生成嵌入", "SearchEmbeddingStatusIdle": "未生成嵌入",
"SearchEmbeddingUpdate": "更新嵌入", "SearchEmbeddingUpdate": "更新嵌入",
"SearchNoWorkspaces": "未找到工作區", "SearchNoWorkspaces": "未找到工作區",
"Seconds": "秒",
"SelectWorkspace": "選擇工作區", "SelectWorkspace": "選擇工作區",
"ShareBrowsingData": "在工作區之間共享瀏覽器數據cookies、快取等關閉後可以每個工作區登不同的第三方服務帳號。", "ShareBrowsingData": "在工作區之間共享瀏覽器數據cookies、快取等關閉後可以每個工作區登不同的第三方服務帳號。",
"ShowSideBar": "顯示側邊欄", "ShowSideBar": "顯示側邊欄",
@ -523,6 +547,12 @@
"hardwareAcceleration": "使用硬體加速" "hardwareAcceleration": "使用硬體加速"
}, },
"Save": "保存", "Save": "保存",
"Schema": {
"ProviderModel": {
"FreeModel": "用於生成總結標題、生成備份標題時使用的免費小模型",
"FreeModelTitle": "免費模型"
}
},
"Scripting": { "Scripting": {
"ExecutingScript": "正在執行腳本" "ExecutingScript": "正在執行腳本"
}, },

View file

@ -67,7 +67,7 @@
"default-gateway": "6.0.3", "default-gateway": "6.0.3",
"dugite": "3.0.0-rc12", "dugite": "3.0.0-rc12",
"electron-dl": "^4.0.0", "electron-dl": "^4.0.0",
"electron-ipc-cat": "2.2.1", "electron-ipc-cat": "2.2.3",
"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",
@ -138,6 +138,7 @@
"devDependencies": { "devDependencies": {
"@cucumber/cucumber": "^12.2.0", "@cucumber/cucumber": "^12.2.0",
"@electron-forge/cli": "7.10.2", "@electron-forge/cli": "7.10.2",
"@electron-forge/maker-msix": "^7.10.2",
"@electron-forge/plugin-auto-unpack-natives": "7.10.2", "@electron-forge/plugin-auto-unpack-natives": "7.10.2",
"@electron-forge/plugin-vite": "7.10.2", "@electron-forge/plugin-vite": "7.10.2",
"@electron/rebuild": "^4.0.1", "@electron/rebuild": "^4.0.1",

58
pnpm-lock.yaml generated
View file

@ -117,8 +117,8 @@ importers:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0 version: 4.0.0
electron-ipc-cat: electron-ipc-cat:
specifier: 2.2.1 specifier: 2.2.3
version: 2.2.1(electron@38.3.0)(rxjs@7.8.2) version: 2.2.3(electron@38.3.0)(rxjs@7.8.2)
electron-settings: electron-settings:
specifier: 5.0.0 specifier: 5.0.0
version: 5.0.0(electron@38.3.0) version: 5.0.0(electron@38.3.0)
@ -297,6 +297,9 @@ importers:
'@electron-forge/cli': '@electron-forge/cli':
specifier: 7.10.2 specifier: 7.10.2
version: 7.10.2(@swc/core@1.12.0)(bluebird@3.7.2)(encoding@0.1.13)(esbuild@0.25.11) version: 7.10.2(@swc/core@1.12.0)(bluebird@3.7.2)(encoding@0.1.13)(esbuild@0.25.11)
'@electron-forge/maker-msix':
specifier: ^7.10.2
version: 7.10.2(bluebird@3.7.2)
'@electron-forge/plugin-auto-unpack-natives': '@electron-forge/plugin-auto-unpack-natives':
specifier: 7.10.2 specifier: 7.10.2
version: 7.10.2(bluebird@3.7.2) version: 7.10.2(bluebird@3.7.2)
@ -1031,6 +1034,10 @@ packages:
resolution: {integrity: sha512-LldkYGkIhri99+HqetGjNzi2cdXy32o5uLlr7fDLoiegm8WAkvvWxFTLdSDS1RP94f6PVOKR9KHqPauu5GaIYw==} resolution: {integrity: sha512-LldkYGkIhri99+HqetGjNzi2cdXy32o5uLlr7fDLoiegm8WAkvvWxFTLdSDS1RP94f6PVOKR9KHqPauu5GaIYw==}
engines: {node: '>= 16.4.0'} engines: {node: '>= 16.4.0'}
'@electron-forge/maker-msix@7.10.2':
resolution: {integrity: sha512-3c2OA1XQ1M0VBDsDb531xqXcu3llXvVpJcKvSL7qzGKbSFOJu3iNZuvpDNGsHpGvbSz5Ea7mUG/Zep6TahiexQ==}
engines: {node: '>= 16.4.0'}
'@electron-forge/maker-rpm@7.10.2': '@electron-forge/maker-rpm@7.10.2':
resolution: {integrity: sha512-LQoeYzbY/z1yuBBA+bNutCJmhCA4NcXUbFO4OTqsIX8B6y1zNTYZT4JEuhoK7eBsP4/Rz6u/JnNp0XOyjftOUQ==} resolution: {integrity: sha512-LQoeYzbY/z1yuBBA+bNutCJmhCA4NcXUbFO4OTqsIX8B6y1zNTYZT4JEuhoK7eBsP4/Rz6u/JnNp0XOyjftOUQ==}
engines: {node: '>= 16.4.0'} engines: {node: '>= 16.4.0'}
@ -3190,6 +3197,10 @@ packages:
resolution: {integrity: sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==} resolution: {integrity: sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==}
engines: {node: '>=18'} engines: {node: '>=18'}
chalk@3.0.0:
resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==}
engines: {node: '>=8'}
chalk@4.1.2: chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -3649,8 +3660,8 @@ packages:
engines: {node: '>= 10.0'} engines: {node: '>= 10.0'}
hasBin: true hasBin: true
electron-ipc-cat@2.2.1: electron-ipc-cat@2.2.3:
resolution: {integrity: sha512-ybHpgKP42VPwAMsOmmwNc1umT1kzAn2VJcP+XC8awY5N8K0DXnidndLe1gwgXfkouHLbRxzybwCA5vYoSfrHvw==} resolution: {integrity: sha512-IbOiQtlu8umssyl8Gu54XHVmdQJQkv7HqMTKWNfLCMEAFFxoL74NxVLB5rI/5QIeZkIfccBAxtIA2GqJLEwZGw==}
peerDependencies: peerDependencies:
electron: '>= 13.0.0' electron: '>= 13.0.0'
rxjs: '>= 7.5.0' rxjs: '>= 7.5.0'
@ -3676,6 +3687,9 @@ packages:
resolution: {integrity: sha512-1mNTwCfkolXl3kMf50yW3vE2lZj0y92P/HYWFBrb+v2S/pCka5mdwN3cagKm458A7NjndSwijynXgcLWRodsVg==} resolution: {integrity: sha512-1mNTwCfkolXl3kMf50yW3vE2lZj0y92P/HYWFBrb+v2S/pCka5mdwN3cagKm458A7NjndSwijynXgcLWRodsVg==}
engines: {node: '>=8.0.0'} engines: {node: '>=8.0.0'}
electron-windows-msix@2.0.4:
resolution: {integrity: sha512-rYRrCWT7Hey52995JNgtmXKwG0saTTVPRd4KymeejOdIgNrSP5hHC5nSmVyClOICGbc5/vL4U5A2Am6L/5t0lw==}
electron-winstaller@5.4.0: electron-winstaller@5.4.0:
resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
engines: {node: '>=8.0.0'} engines: {node: '>=8.0.0'}
@ -7404,6 +7418,9 @@ packages:
utf-8-validate: utf-8-validate:
optional: true optional: true
xml-escape@1.1.0:
resolution: {integrity: sha512-B/T4sDK8Z6aUh/qNr7mjKAwwncIljFuUP+DO/D5hloYFj+90O88z8Wf7oSucZTHxBAsC1/CTP4rtx/x1Uf72Mg==}
xml-name-validator@5.0.0: xml-name-validator@5.0.0:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -8270,6 +8287,19 @@ snapshots:
- supports-color - supports-color
optional: true optional: true
'@electron-forge/maker-msix@7.10.2(bluebird@3.7.2)':
dependencies:
'@electron-forge/core-utils': 7.10.2(bluebird@3.7.2)
'@electron-forge/maker-base': 7.10.2(bluebird@3.7.2)
'@electron-forge/shared-types': 7.10.2(bluebird@3.7.2)
cross-spawn: 7.0.6
electron-windows-msix: 2.0.4
fs-extra: 10.1.0
parse-author: 2.0.0
transitivePeerDependencies:
- bluebird
- supports-color
'@electron-forge/maker-rpm@7.10.2(bluebird@3.7.2)': '@electron-forge/maker-rpm@7.10.2(bluebird@3.7.2)':
dependencies: dependencies:
'@electron-forge/maker-base': 7.10.2(bluebird@3.7.2) '@electron-forge/maker-base': 7.10.2(bluebird@3.7.2)
@ -10856,6 +10886,11 @@ snapshots:
chai@6.2.0: {} chai@6.2.0: {}
chalk@3.0.0:
dependencies:
ansi-styles: 4.3.0
supports-color: 7.2.0
chalk@4.1.2: chalk@4.1.2:
dependencies: dependencies:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
@ -11337,7 +11372,7 @@ snapshots:
- supports-color - supports-color
optional: true optional: true
electron-ipc-cat@2.2.1(electron@38.3.0)(rxjs@7.8.2): electron-ipc-cat@2.2.3(electron@38.3.0)(rxjs@7.8.2):
dependencies: dependencies:
electron: 38.3.0 electron: 38.3.0
memize: 2.1.1 memize: 2.1.1
@ -11373,6 +11408,17 @@ snapshots:
jsonfile: 4.0.0 jsonfile: 4.0.0
mkdirp: 0.5.6 mkdirp: 0.5.6
electron-windows-msix@2.0.4:
dependencies:
'@electron/windows-sign': 1.2.2
'@malept/cross-spawn-promise': 2.0.0
chalk: 3.0.0
debug: 4.4.3(supports-color@8.1.1)
fs-extra: 11.3.2
xml-escape: 1.1.0
transitivePeerDependencies:
- supports-color
electron-winstaller@5.4.0: electron-winstaller@5.4.0:
dependencies: dependencies:
'@electron/asar': 3.4.1 '@electron/asar': 3.4.1
@ -15540,6 +15586,8 @@ snapshots:
ws@8.18.3: {} ws@8.18.3: {}
xml-escape@1.1.0: {}
xml-name-validator@5.0.0: {} xml-name-validator@5.0.0: {}
xml-parse-from-string@1.0.1: {} xml-parse-from-string@1.0.1: {}

View file

@ -1,8 +0,0 @@
import { LOCALIZATION_FOLDER } from '@/constants/paths';
import fs from 'fs-extra';
import path from 'path';
export const supportedLanguagesMap = fs.readJsonSync(path.join(LOCALIZATION_FOLDER, 'supportedLanguages.json')) as Record<string, string>;
export const tiddlywikiLanguagesMap = fs.readJsonSync(path.join(LOCALIZATION_FOLDER, 'tiddlywikiLanguages.json')) as Record<string, string | undefined>;
export const supportedLanguagesKNames = Object.keys(supportedLanguagesMap);

View file

@ -15,6 +15,11 @@ import { developmentWikiFolderName, localizationFolderName, testWikiFolderName }
* *
* Key challenge: In unit tests, Electron sets process.resourcesPath to its internal directory, * Key challenge: In unit tests, Electron sets process.resourcesPath to its internal directory,
* which is wrong. We detect this by checking if the path contains 'electron'. * which is wrong. We detect this by checking if the path contains 'electron'.
*
* WARNING: process.resourcesPath changes during app initialization!
* When starting via protocol (tidgi://), this path may not be correct initially.
* Always access language maps via ContextService after app initialization.
* See issue #625 for details.
*/ */
// Detect if we're in packaged app (not dev, not unit tests with electron's internal path) // Detect if we're in packaged app (not dev, not unit tests with electron's internal path)

View file

@ -0,0 +1,9 @@
import { isTest } from './environment';
/**
* Protocol scheme used for deep linking
* Test mode uses a different protocol to avoid conflicts with production
*
* Note: This file is for main process only, not for renderer/shared code
*/
export const TIDGI_PROTOCOL_SCHEME = isTest ? 'tidgi-test' : 'tidgi';

View file

@ -9,6 +9,7 @@ import inspector from 'node:inspector';
import { MainChannel } from '@/constants/channels'; import { MainChannel } from '@/constants/channels';
import { isDevelopmentOrTest, isTest } from '@/constants/environment'; import { isDevelopmentOrTest, isTest } from '@/constants/environment';
import { TIDGI_PROTOCOL_SCHEME } from '@/constants/protocol';
import { container } from '@services/container'; import { container } from '@services/container';
import { initRendererI18NHandler } from '@services/libs/i18n'; import { initRendererI18NHandler } from '@services/libs/i18n';
import { destroyLogger, logger } from '@services/libs/log'; import { destroyLogger, logger } from '@services/libs/log';
@ -19,6 +20,7 @@ import serviceIdentifier from '@services/serviceIdentifier';
import { WindowNames } from '@services/windows/WindowProperties'; import { WindowNames } from '@services/windows/WindowProperties';
import type { IAgentDefinitionService } from '@services/agentDefinition/interface'; import type { IAgentDefinitionService } from '@services/agentDefinition/interface';
import type { IContextService } from '@services/context/interface';
import type { IDatabaseService } from '@services/database/interface'; import type { IDatabaseService } from '@services/database/interface';
import type { IDeepLinkService } from '@services/deepLink/interface'; import type { IDeepLinkService } from '@services/deepLink/interface';
import type { IExternalAPIService } from '@services/externalAPI/interface'; import type { IExternalAPIService } from '@services/externalAPI/interface';
@ -52,10 +54,11 @@ if (process.env.DEBUG_MAIN === 'true') {
EventEmitter.defaultMaxListeners = 150; EventEmitter.defaultMaxListeners = 150;
app.commandLine.appendSwitch('--disable-web-security'); app.commandLine.appendSwitch('--disable-web-security');
app.commandLine.appendSwitch('--unsafely-disable-devtools-self-xss-warnings'); app.commandLine.appendSwitch('--unsafely-disable-devtools-self-xss-warnings');
// Use different protocol scheme for test mode to avoid conflicts
protocol.registerSchemesAsPrivileged([ protocol.registerSchemesAsPrivileged([
{ scheme: 'http', privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true } }, { scheme: 'http', privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true } },
{ scheme: 'https', privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true } }, { scheme: 'https', privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true } },
{ scheme: 'tidgi', privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true } }, { scheme: TIDGI_PROTOCOL_SCHEME, privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true } },
{ scheme: 'open', privileges: { bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true } }, { scheme: 'open', privileges: { bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true } },
{ scheme: 'file', privileges: { bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true } }, { scheme: 'file', privileges: { bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true } },
{ scheme: 'mailto', privileges: { standard: true } }, { scheme: 'mailto', privileges: { standard: true } },
@ -63,6 +66,7 @@ protocol.registerSchemesAsPrivileged([
bindServiceAndProxy(); bindServiceAndProxy();
// Get services - DO NOT use them until commonInit() is called // Get services - DO NOT use them until commonInit() is called
const contextService = container.get<IContextService>(serviceIdentifier.Context);
const databaseService = container.get<IDatabaseService>(serviceIdentifier.Database); const databaseService = container.get<IDatabaseService>(serviceIdentifier.Database);
const preferenceService = container.get<IPreferenceService>(serviceIdentifier.Preference); const preferenceService = container.get<IPreferenceService>(serviceIdentifier.Preference);
const updaterService = container.get<IUpdaterService>(serviceIdentifier.Updater); const updaterService = container.get<IUpdaterService>(serviceIdentifier.Updater);
@ -93,9 +97,10 @@ const commonInit = async (): Promise<void> => {
await app.whenReady(); await app.whenReady();
await initDevelopmentExtension(); await initDevelopmentExtension();
// Initialize database FIRST - all other services depend on it // Initialize context service - loads language maps after app is ready. This ensures LOCALIZATION_FOLDER path is correct (process.resourcesPath is stable)
await contextService.initialize();
// Initialize database - all other services depend on it
await databaseService.initializeForApp(); await databaseService.initializeForApp();
// Initialize i18n early so error messages can be translated // Initialize i18n early so error messages can be translated
await initRendererI18NHandler(); await initRendererI18NHandler();
@ -120,7 +125,8 @@ const commonInit = async (): Promise<void> => {
// if user want a tidgi mini window, 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'); // Use different protocol for test mode to avoid conflicts with production
deepLinkService.initializeDeepLink(TIDGI_PROTOCOL_SCHEME);
await windowService.open(WindowNames.main); await windowService.open(WindowNames.main);
@ -139,6 +145,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();
// Process any pending deep link after workspaces are initialized
await deepLinkService.processPendingDeepLink();
const tidgiMiniWindow = await preferenceService.get('tidgiMiniWindow'); const tidgiMiniWindow = await preferenceService.get('tidgiMiniWindow');
if (tidgiMiniWindow) { if (tidgiMiniWindow) {
await windowService.openTidgiMiniWindow(true, false); await windowService.openTidgiMiniWindow(true, false);
@ -192,7 +202,7 @@ app.on('ready', async () => {
await commonInit(); await commonInit();
try { try {
// buildLanguageMenu needs menuService which is initialized in commonInit // buildLanguageMenu needs menuService which is initialized in commonInit
buildLanguageMenu(); await buildLanguageMenu();
if (await preferenceService.get('syncBeforeShutdown')) { if (await preferenceService.get('syncBeforeShutdown')) {
wikiGitWorkspaceService.registerSyncBeforeShutdown(); wikiGitWorkspaceService.registerSyncBeforeShutdown();
} }

View file

@ -26,7 +26,7 @@ async function executeJavaScriptInBrowserView(): Promise<void> {
const workspaceID = viewMetaData.workspace?.id; const workspaceID = viewMetaData.workspace?.id;
// Log when view is fully loaded for E2E tests // Log when view is fully loaded for E2E tests
void native.logFor(workspaceName, 'info', `[test-id-VIEW_LOADED] Browser view preload script executed and ready for workspace: ${workspaceName}`); void native.logFor(workspaceName, 'debug', `[test-id-VIEW_LOADED] Browser view preload script executed and ready for workspace: ${workspaceName}`);
try { try {
await webFrame.executeJavaScript(` await webFrame.executeJavaScript(`

View file

@ -1,11 +1,13 @@
import { isElectronDevelopment } from '@/constants/isElectronDevelopment'; import { isElectronDevelopment } from '@/constants/isElectronDevelopment';
import { LOCALIZATION_FOLDER } from '@/constants/paths';
import { app, net } from 'electron'; import { app, net } from 'electron';
import fs from 'fs-extra';
import { injectable } from 'inversify'; import { injectable } from 'inversify';
import os from 'os'; import os from 'os';
import path from 'path';
import process from 'process'; import process from 'process';
import * as appPaths from '@/constants/appPaths'; import * as appPaths from '@/constants/appPaths';
import { supportedLanguagesMap, tiddlywikiLanguagesMap } from '@/constants/languages';
import * as paths from '@/constants/paths'; import * as paths from '@/constants/paths';
import { getMainWindowEntry } from '@services/windows/viteEntry'; import { getMainWindowEntry } from '@services/windows/viteEntry';
import type { IConstants, IContext, IContextService, IPaths } from './interface'; import type { IConstants, IContext, IContextService, IPaths } from './interface';
@ -21,19 +23,49 @@ export class ContextService implements IContextService {
appName: app.name, appName: app.name,
oSVersion: os.release(), oSVersion: os.release(),
environmentVersions: process.versions, environmentVersions: process.versions,
tiddlywikiLanguagesMap,
supportedLanguagesMap,
}; };
private readonly context: IContext; private readonly context: IContext;
private initialized = false;
constructor() { constructor() {
this.pathConstants.MAIN_WINDOW_WEBPACK_ENTRY = getMainWindowEntry(); this.pathConstants.MAIN_WINDOW_WEBPACK_ENTRY = getMainWindowEntry();
this.context = { this.context = {
...this.pathConstants, ...this.pathConstants,
...this.constants, ...this.constants,
supportedLanguagesMap: {},
tiddlywikiLanguagesMap: {},
}; };
} }
/**
* Initialize language maps after app is ready
* Must be called before any code tries to access language maps
* This ensures LOCALIZATION_FOLDER path is correct (process.resourcesPath is stable)
*/
public async initialize(): Promise<void> {
if (this.initialized) {
return;
}
try {
const supportedLanguagesPath = path.join(LOCALIZATION_FOLDER, 'supportedLanguages.json');
const tiddlywikiLanguagesPath = path.join(LOCALIZATION_FOLDER, 'tiddlywikiLanguages.json');
const [supportedLanguagesMap, tiddlywikiLanguagesMap] = await Promise.all([
fs.readJson(supportedLanguagesPath) as Promise<Record<string, string>>,
fs.readJson(tiddlywikiLanguagesPath) as Promise<Record<string, string | undefined>>,
]);
this.context.supportedLanguagesMap = supportedLanguagesMap ?? {};
this.context.tiddlywikiLanguagesMap = tiddlywikiLanguagesMap ?? {};
this.initialized = true;
} catch (error) {
console.error('Failed to load language maps:', error);
// Keep empty objects as fallback
}
}
public async get<K extends keyof IContext>(key: K): Promise<IContext[K]> { public async get<K extends keyof IContext>(key: K): Promise<IContext[K]> {
if (key in this.context) { if (key in this.context) {
return this.context[key]; return this.context[key];

View file

@ -28,23 +28,30 @@ export interface IConstants {
isDevelopment: boolean; isDevelopment: boolean;
oSVersion: string; oSVersion: string;
platform: string; platform: string;
}
export interface IContext extends IPaths, IConstants {
supportedLanguagesMap: Record<string, string>; supportedLanguagesMap: Record<string, string>;
tiddlywikiLanguagesMap: Record<string, string | undefined>; tiddlywikiLanguagesMap: Record<string, string | undefined>;
} }
export interface IContext extends IPaths, IConstants {}
/** /**
* Manage constant value like `isDevelopment` and many else, so you can know about about running environment in main and renderer process easily. * Manage constant value like `isDevelopment` and many else, so you can know about about running environment in main and renderer process easily.
*/ */
export interface IContextService { export interface IContextService {
get<K extends keyof IContext>(key: K): Promise<IContext[K]>; get<K extends keyof IContext>(key: K): Promise<IContext[K]>;
/**
* Initialize language maps after app is ready.
* Must be called early in app initialization before any code tries to access language maps.
*/
initialize(): Promise<void>;
isOnline(): Promise<boolean>; isOnline(): Promise<boolean>;
} }
export const ContextServiceIPCDescriptor = { export const ContextServiceIPCDescriptor = {
channel: ContextChannel.name, channel: ContextChannel.name,
properties: { properties: {
get: ProxyPropertyType.Function, get: ProxyPropertyType.Function,
initialize: ProxyPropertyType.Function,
isOnline: ProxyPropertyType.Function, isOnline: ProxyPropertyType.Function,
}, },
}; };

View file

@ -43,21 +43,12 @@ export class DatabaseService implements IDatabaseService {
private storeSettingsToFileLock = false; private storeSettingsToFileLock = false;
async initializeForApp(): Promise<void> { async initializeForApp(): Promise<void> {
logger.info('starting', { logger.debug('starting', {
function: 'DatabaseService.initializeForApp', function: 'DatabaseService.initializeForApp',
}); });
// Initialize settings folder and load settings // Initialize settings folder and load settings
ensureSettingFolderExist(); ensureSettingFolderExist();
this.settingFileContent = settings.getSync() as unknown as ISettingFile; this.settingFileContent = settings.getSync() as unknown as ISettingFile;
logger.info('loaded settings', {
hasContent: !!this.settingFileContent,
keys: this.settingFileContent ? Object.keys(this.settingFileContent).length : 0,
hasPreferences: !!this.settingFileContent?.preferences,
tidgiMiniWindow: this.settingFileContent?.preferences?.tidgiMiniWindow,
settingsFilePath: settings.file(),
function: 'DatabaseService.initializeForApp',
});
// Initialize settings backup stream // Initialize settings backup stream
try { try {
this.settingBackupStream = rotateFs.createStream(`settings.json.bak`, { this.settingBackupStream = rotateFs.createStream(`settings.json.bak`, {
@ -72,7 +63,6 @@ export class DatabaseService implements IDatabaseService {
// Ensure database folder exists // Ensure database folder exists
await fs.ensureDir(CACHE_DATABASE_FOLDER); await fs.ensureDir(CACHE_DATABASE_FOLDER);
// Register default app database schema // Register default app database schema
this.registerSchema('app', { this.registerSchema('app', {
entities: [], // Put app-level entities here entities: [], // Put app-level entities here
@ -80,22 +70,16 @@ export class DatabaseService implements IDatabaseService {
synchronize: false, synchronize: false,
migrationsRun: true, migrationsRun: true,
}); });
// Register wiki database schema example
this.registerSchema('wiki', { this.registerSchema('wiki', {
entities: [WikiTiddler], // Wiki related entities entities: [WikiTiddler], // Wiki related entities
synchronize: true, synchronize: true,
migrationsRun: false, migrationsRun: false,
}); });
// Register wiki-embedding database schema
this.registerSchema('wikiEmbedding', { this.registerSchema('wikiEmbedding', {
entities: [WikiEmbeddingEntity, WikiEmbeddingStatusEntity], entities: [WikiEmbeddingEntity, WikiEmbeddingStatusEntity],
synchronize: true, synchronize: true,
migrationsRun: false, migrationsRun: false,
}); });
// Register agent database schema
this.registerSchema('agent', { this.registerSchema('agent', {
entities: [ entities: [
AgentDefinitionEntity, AgentDefinitionEntity,
@ -106,8 +90,6 @@ export class DatabaseService implements IDatabaseService {
synchronize: true, synchronize: true,
migrationsRun: false, migrationsRun: false,
}); });
// Register external API log database schema
this.registerSchema('externalApi', { this.registerSchema('externalApi', {
entities: [ExternalAPILogEntity], entities: [ExternalAPILogEntity],
synchronize: true, synchronize: true,

View file

@ -1,3 +1,4 @@
import { TIDGI_PROTOCOL_SCHEME } from '@/constants/protocol';
import { logger } from '@services/libs/log'; import { logger } from '@services/libs/log';
import serviceIdentifier from '@services/serviceIdentifier'; import serviceIdentifier from '@services/serviceIdentifier';
import type { IWorkspaceService } from '@services/workspaces/interface'; import type { IWorkspaceService } from '@services/workspaces/interface';
@ -8,6 +9,8 @@ import type { IDeepLinkService } from './interface';
@injectable() @injectable()
export class DeepLinkService implements IDeepLinkService { export class DeepLinkService implements IDeepLinkService {
private pendingDeepLink: string | undefined;
constructor( constructor(
@inject(serviceIdentifier.Workspace) private readonly workspaceService: IWorkspaceService, @inject(serviceIdentifier.Workspace) private readonly workspaceService: IWorkspaceService,
) {} ) {}
@ -67,6 +70,9 @@ export class DeepLinkService implements IDeepLinkService {
} }
workspace = await this.workspaceService.getByWikiName(workspaceName); workspace = await this.workspaceService.getByWikiName(workspaceName);
if (workspace === undefined) { if (workspace === undefined) {
// Workspace doesn't exist yet, save for later processing
logger.info(`Workspace not found, saving deep link for later`, { requestUrl, function: 'deepLinkHandler' });
this.pendingDeepLink = requestUrl;
return; return;
} }
} }
@ -93,6 +99,18 @@ export class DeepLinkService implements IDeepLinkService {
} }
}; };
/**
* Process any pending deep link after workspaces are initialized
*/
public async processPendingDeepLink(): Promise<void> {
if (this.pendingDeepLink) {
const url = this.pendingDeepLink;
this.pendingDeepLink = undefined;
logger.info(`Processing pending deep link`, { url, function: 'processPendingDeepLink' });
await this.deepLinkHandler(url);
}
}
public initializeDeepLink(protocol: string) { public initializeDeepLink(protocol: string) {
if (process.defaultApp) { if (process.defaultApp) {
if (process.argv.length >= 2) { if (process.argv.length >= 2) {
@ -120,12 +138,31 @@ export class DeepLinkService implements IDeepLinkService {
const gotTheLock = app.requestSingleInstanceLock(); const gotTheLock = app.requestSingleInstanceLock();
if (gotTheLock) { if (gotTheLock) {
// Handle second instance (when app is already running)
app.on('second-instance', (_event, commandLine) => { app.on('second-instance', (_event, commandLine) => {
const url = commandLine.pop(); const url = commandLine.pop();
if (url !== undefined && url !== '') { if (url !== undefined && url !== '') {
void this.deepLinkHandler(url); void this.deepLinkHandler(url);
} }
}); });
// Handle first instance startup with URL parameter
// On Windows/Linux, protocol URLs are passed as command line arguments
if (process.argv.length >= 2) {
// Find the protocol URL in command line arguments
const protocolUrl = process.argv.find(argument => argument.startsWith(`${TIDGI_PROTOCOL_SCHEME}://`));
if (protocolUrl) {
logger.info(`Processing initial deep link from command line`, { protocolUrl, function: 'setupWindowsLinuxHandler' });
// Process after app is ready
if (app.isReady()) {
void this.deepLinkHandler(protocolUrl);
} else {
app.once('ready', () => {
void this.deepLinkHandler(protocolUrl);
});
}
}
}
} else { } else {
app.quit(); app.quit();
} }

View file

@ -6,11 +6,17 @@ export interface IDeepLinkService {
* @param protocol The protocol to be used for deep linking. * @param protocol The protocol to be used for deep linking.
*/ */
initializeDeepLink(protocol: string): void; initializeDeepLink(protocol: string): void;
/**
* Process any pending deep link after workspaces are initialized.
* Should be called after all workspaces are ready.
*/
processPendingDeepLink(): Promise<void>;
} }
export const DeepLinkServiceIPCDescriptor = { export const DeepLinkServiceIPCDescriptor = {
channel: 'DeepLinkChannel', channel: 'DeepLinkChannel',
properties: { properties: {
initializeDeepLink: ProxyPropertyType.Function, initializeDeepLink: ProxyPropertyType.Function,
processPendingDeepLink: ProxyPropertyType.Function,
}, },
}; };

View file

@ -1,209 +0,0 @@
{
"providers": [
{
"provider": "openai",
"providerClass": "openai",
"isPreset": true,
"enabled": false,
"models": [
{
"name": "gpt-4o",
"caption": "GPT-4o",
"features": ["language", "reasoning", "toolCalling", "vision"]
},
{
"name": "gpt-4-turbo",
"caption": "GPT-4 Turbo",
"features": ["language", "reasoning", "toolCalling"]
},
{
"name": "gpt-3.5-turbo",
"caption": "GPT-3.5 Turbo",
"features": ["language"]
},
{
"name": "text-embedding-3-small",
"caption": "Text Embedding 3 Small",
"features": ["embedding"]
}
]
},
{
"provider": "siliconflow",
"providerClass": "openAICompatible",
"isPreset": true,
"enabled": true,
"baseURL": "https://api.siliconflow.cn/v1",
"models": [
{
"name": "Qwen/Qwen2.5-7B-Instruct",
"caption": "通义千问 2.5 7B",
"features": ["language", "reasoning", "free"]
},
{
"name": "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B",
"caption": "DeepSeek-R1",
"features": ["language", "reasoning"]
},
{
"name": "BAAI/bge-m3",
"caption": "bge-m3",
"features": ["embedding"]
},
{
"name": "IndexTeam/IndexTTS-2",
"caption": "IndexTTS-2",
"features": ["speech"]
},
{
"name": "TeleAI/TeleSpeechASR",
"caption": "TeleSpeechASR",
"features": ["transcriptions"]
}
]
},
{
"provider": "ollama",
"providerClass": "ollama",
"isPreset": true,
"showBaseURLField": true,
"enabled": false,
"baseURL": "http://localhost:11434",
"models": [
{
"name": "llama3",
"caption": "Llama 3",
"features": ["language"]
},
{
"name": "phi3",
"caption": "Phi-3",
"features": ["language"]
},
{
"name": "mistral",
"caption": "Mistral",
"features": ["language"]
},
{
"name": "gemma",
"caption": "Gemma",
"features": ["language"]
}
]
},
{
"provider": "deepseek",
"providerClass": "deepseek",
"isPreset": true,
"enabled": false,
"models": [
{
"name": "deepseek-chat",
"caption": "DeepSeek Chat",
"features": ["language", "reasoning"]
},
{
"name": "deepseek-coder",
"caption": "DeepSeek Coder",
"features": ["language", "reasoning"]
}
]
},
{
"provider": "anthropic",
"providerClass": "anthropic",
"isPreset": true,
"enabled": false,
"models": [
{
"name": "claude-3-opus-20240229",
"caption": "Claude 3 Opus",
"features": ["language", "reasoning", "vision", "toolCalling"]
},
{
"name": "claude-3-sonnet-20240229",
"caption": "Claude 3 Sonnet",
"features": ["language", "reasoning", "vision"]
},
{
"name": "claude-3-haiku-20240307",
"caption": "Claude 3 Haiku",
"features": ["language", "reasoning", "vision"]
}
]
},
{
"provider": "comfyui",
"providerClass": "comfyui",
"isPreset": true,
"enabled": false,
"baseURL": "http://localhost:8188",
"models": [
{
"name": "flux",
"caption": "Flux",
"features": ["imageGeneration"]
}
]
}
],
"defaultConfig": {
"api": {
"provider": "siliconflow",
"model": "Qwen/Qwen2.5-7B-Instruct"
},
"modelParameters": {
"temperature": 0.7,
"systemPrompt": "You are a helpful assistant.",
"topP": 0.95
}
},
"modelFeatures": [
{
"value": "language",
"label": "Language",
"i18nKey": "ModelFeature.Language"
},
{
"value": "reasoning",
"label": "Reasoning",
"i18nKey": "ModelFeature.Reasoning"
},
{
"value": "toolCalling",
"label": "Tool Calling",
"i18nKey": "ModelFeature.ToolCalling"
},
{
"value": "vision",
"label": "Vision",
"i18nKey": "ModelFeature.Vision"
},
{
"value": "imageGeneration",
"label": "Image Generation",
"i18nKey": "ModelFeature.ImageGeneration"
},
{
"value": "embedding",
"label": "Embedding",
"i18nKey": "ModelFeature.Embedding"
},
{
"value": "speech",
"label": "Speech",
"i18nKey": "ModelFeature.Speech"
},
{
"value": "transcriptions",
"label": "Transcriptions",
"i18nKey": "ModelFeature.Transcriptions"
},
{
"value": "free",
"label": "Free",
"i18nKey": "ModelFeature.Free"
}
]
}

View file

@ -0,0 +1,211 @@
import { t } from '@services/libs/i18n/placeholder';
export default {
providers: [
{
provider: 'openai',
providerClass: 'openai',
isPreset: true,
enabled: false,
models: [
{
name: 'gpt-4o',
caption: 'GPT-4o',
features: ['language', 'reasoning', 'toolCalling', 'vision'],
},
{
name: 'gpt-4-turbo',
caption: 'GPT-4 Turbo',
features: ['language', 'reasoning', 'toolCalling'],
},
{
name: 'gpt-3.5-turbo',
caption: 'GPT-3.5 Turbo',
features: ['language'],
},
{
name: 'text-embedding-3-small',
caption: 'Text Embedding 3 Small',
features: ['embedding'],
},
],
},
{
provider: 'siliconflow',
providerClass: 'openAICompatible',
isPreset: true,
enabled: true,
baseURL: 'https://api.siliconflow.cn/v1',
models: [
{
name: 'Qwen/Qwen2.5-7B-Instruct',
caption: '通义千问 2.5 7B',
features: ['language', 'reasoning', 'free'],
},
{
name: 'deepseek-ai/DeepSeek-R1-Distill-Qwen-7B',
caption: 'DeepSeek-R1',
features: ['language', 'reasoning'],
},
{
name: 'BAAI/bge-m3',
caption: 'bge-m3',
features: ['embedding'],
},
{
name: 'IndexTeam/IndexTTS-2',
caption: 'IndexTTS-2',
features: ['speech'],
},
{
name: 'TeleAI/TeleSpeechASR',
caption: 'TeleSpeechASR',
features: ['transcriptions'],
},
],
},
{
provider: 'ollama',
providerClass: 'ollama',
isPreset: true,
showBaseURLField: true,
enabled: false,
baseURL: 'http://localhost:11434',
models: [
{
name: 'llama3',
caption: 'Llama 3',
features: ['language'],
},
{
name: 'phi3',
caption: 'Phi-3',
features: ['language'],
},
{
name: 'mistral',
caption: 'Mistral',
features: ['language'],
},
{
name: 'gemma',
caption: 'Gemma',
features: ['language'],
},
],
},
{
provider: 'deepseek',
providerClass: 'deepseek',
isPreset: true,
enabled: false,
models: [
{
name: 'deepseek-chat',
caption: 'DeepSeek Chat',
features: ['language', 'reasoning'],
},
{
name: 'deepseek-coder',
caption: 'DeepSeek Coder',
features: ['language', 'reasoning'],
},
],
},
{
provider: 'anthropic',
providerClass: 'anthropic',
isPreset: true,
enabled: false,
models: [
{
name: 'claude-3-opus-20240229',
caption: 'Claude 3 Opus',
features: ['language', 'reasoning', 'vision', 'toolCalling'],
},
{
name: 'claude-3-sonnet-20240229',
caption: 'Claude 3 Sonnet',
features: ['language', 'reasoning', 'vision'],
},
{
name: 'claude-3-haiku-20240307',
caption: 'Claude 3 Haiku',
features: ['language', 'reasoning', 'vision'],
},
],
},
{
provider: 'comfyui',
providerClass: 'comfyui',
isPreset: true,
enabled: false,
baseURL: 'http://localhost:8188',
models: [
{
name: 'flux',
caption: 'Flux',
features: ['imageGeneration'],
},
],
},
],
defaultConfig: {
api: {
provider: 'siliconflow',
model: 'Qwen/Qwen2.5-7B-Instruct',
},
modelParameters: {
temperature: 0.7,
systemPrompt: 'You are a helpful assistant.',
topP: 0.95,
},
},
modelFeatures: [
{
value: 'language',
label: 'Language',
i18nKey: t('ModelFeature.Language'),
},
{
value: 'reasoning',
label: 'Reasoning',
i18nKey: t('ModelFeature.Reasoning'),
},
{
value: 'toolCalling',
label: 'Tool Calling',
i18nKey: t('ModelFeature.ToolCalling'),
},
{
value: 'vision',
label: 'Vision',
i18nKey: t('ModelFeature.Vision'),
},
{
value: 'imageGeneration',
label: 'Image Generation',
i18nKey: t('ModelFeature.ImageGeneration'),
},
{
value: 'embedding',
label: 'Embedding',
i18nKey: t('ModelFeature.Embedding'),
},
{
value: 'speech',
label: 'Speech',
i18nKey: t('ModelFeature.Speech'),
},
{
value: 'transcriptions',
label: 'Transcriptions',
i18nKey: t('ModelFeature.Transcriptions'),
},
{
value: 'free',
label: 'Free',
i18nKey: t('ModelFeature.Free'),
},
],
};

View file

@ -539,3 +539,221 @@ export async function amendCommitMessage(repoPath: string, newMessage: string):
throw new Error(`Failed to amend commit message: ${result.stderr}`); throw new Error(`Failed to amend commit message: ${result.stderr}`);
} }
} }
/**
* Get deleted tiddler titles from git history since a specific date
* This looks for deleted .tid and .meta files and extracts their title field
* @param repoPath - Path to the git repository
* @param sinceDate - Date to check for deletions after this time
* @returns Array of deleted tiddler titles
*/
export async function getDeletedTiddlersSinceDate(repoPath: string, sinceDate: Date): Promise<string[]> {
try {
// Format date for git log (ISO format)
const sinceISOString = sinceDate.toISOString();
// Get list of deleted files since sinceDate
// Using git log with --diff-filter=D to show only deletions
const logResult = await gitExec(
['log', `--since=${sinceISOString}`, '--diff-filter=D', '--name-only', '--pretty=format:'],
repoPath,
);
if (logResult.exitCode !== 0) {
throw new Error(`Failed to get deleted files: ${logResult.stderr}`);
}
const deletedFiles = logResult.stdout
.trim()
.split('\n')
.filter((line: string) => line.length > 0)
.filter((file: string) => file.endsWith('.tid') || file.endsWith('.meta'));
if (deletedFiles.length === 0) {
return [];
}
// For each deleted file, get its content from git history to extract the title
// Parallelize git operations for efficiency (avoid serial git exec calls)
const deletedTitlePromises = deletedFiles.map(async (file) => {
try {
// Get the last commit that had this file (before deletion)
const revListResult = await gitExec(
['rev-list', '-n', '1', 'HEAD', '--', file],
repoPath,
);
if (revListResult.exitCode !== 0 || !revListResult.stdout.trim()) {
return null;
}
const lastCommitHash = revListResult.stdout.trim();
// Get the file content from that commit
const showResult = await gitExec(
['show', `${lastCommitHash}:${file}`],
repoPath,
);
if (showResult.exitCode !== 0) {
return null;
}
return extractTitleFromTiddlerContent(showResult.stdout);
} catch (error) {
console.error(`Error processing deleted file ${file}:`, error);
return null;
}
});
const deletedTitles = await Promise.all(deletedTitlePromises);
// Remove nulls and duplicates
return [...new Set(deletedTitles.filter((title): title is string => title !== null))];
} catch (error) {
console.error('Error getting deleted tiddlers:', error);
return [];
}
}
/**
* Get tiddler content at a specific point in time from git history
* This is used for 3-way merge to get the base version
* @param repoPath - Path to the git repository
* @param tiddlerTitle - Title of the tiddler
* @param beforeDate - Get the version that existed before this date
* @returns Tiddler fields including text, or null if not found
*/
export async function getTiddlerAtTime(
repoPath: string,
tiddlerTitle: string,
beforeDate: Date,
): Promise<{ fields: Record<string, unknown>; text: string } | null> {
try {
// Find commits that modified any file before the specified date
const beforeISOString = beforeDate.toISOString();
// First, find all .tid and .meta files that might contain this tiddler
// We need to search for files because the title might not match the filename
const logResult = await gitExec(
['log', `--before=${beforeISOString}`, '--name-only', '--pretty=format:%H', '--', '*.tid', '*.meta'],
repoPath,
);
if (logResult.exitCode !== 0) {
return null;
}
const lines = logResult.stdout.trim().split('\n');
// Parse output: commit hash followed by file names
let currentCommit: string | null = null;
const filesToCheck: Array<{ commit: string; file: string }> = [];
for (const line of lines) {
if (line.length === 40 && /^[0-9a-f]+$/.test(line)) {
// This is a commit hash
currentCommit = line;
} else if (line.trim().length > 0 && currentCommit) {
// This is a file name
const file = line.trim();
if (file.endsWith('.tid') || file.endsWith('.meta')) {
filesToCheck.push({ commit: currentCommit, file });
}
}
}
// Check each file to find the one with matching title
// Use Promise.all to check files in parallel instead of sequentially
const searchPromises = filesToCheck.map(async ({ commit, file }) => {
try {
const showResult = await gitExec(
['show', `${commit}:${file}`],
repoPath,
);
if (showResult.exitCode === 0) {
const content = showResult.stdout;
const parsedTiddler = parseTiddlerContent(content);
if (parsedTiddler.fields.title === tiddlerTitle) {
return parsedTiddler; // Match found
}
}
} catch {
// Continue checking other files
}
return null; // No match in this file
});
// Return the first match found, or null if none match
const results = await Promise.all(searchPromises);
return results.find((result): result is { fields: Record<string, unknown>; text: string } => result !== null) ?? null;
} catch (error) {
console.error('Error getting tiddler at time:', error);
return null;
}
}
/**
* Parse tiddler content (tid or meta file) into fields and text
*/
function parseTiddlerContent(content: string): { fields: Record<string, unknown>; text: string } {
const lines = content.split('\n');
const fields: Record<string, unknown> = {};
let textStartIndex = 0;
// Parse headers
for (let index = 0; index < lines.length; index++) {
const line = lines[index];
// Empty line marks end of headers
if (line.trim() === '') {
textStartIndex = index + 1;
break;
}
// Parse field: value format
const colonIndex = line.indexOf(':');
if (colonIndex > 0) {
const fieldName = line.slice(0, colonIndex).trim();
const fieldValue = line.slice(colonIndex + 1).trim();
fields[fieldName] = fieldValue;
}
}
// Get text content (everything after the empty line)
const text = lines.slice(textStartIndex).join('\n');
return { fields, text };
}
/**
* Extract title field from tiddler content (tid or meta file)
* Tiddler files have format:
* ```
* title: My Tiddler Title
* tags: [[Tag1]] [[Tag2]]
* ...
*
* Tiddler text content...
* ```
*/
function extractTitleFromTiddlerContent(content: string): string | null {
const lines = content.split('\n');
for (const line of lines) {
// Look for "title:" field (case-insensitive)
const titleMatch = line.match(/^title:\s*(.+)$/i);
if (titleMatch) {
return titleMatch[1].trim();
}
// Stop at empty line (end of headers)
if (line.trim() === '') {
break;
}
}
return null;
}

View file

@ -38,6 +38,7 @@ export class Git implements IGitService {
@inject(serviceIdentifier.Preference) private readonly preferenceService: IPreferenceService, @inject(serviceIdentifier.Preference) private readonly preferenceService: IPreferenceService,
@inject(serviceIdentifier.Authentication) private readonly authService: IAuthenticationService, @inject(serviceIdentifier.Authentication) private readonly authService: IAuthenticationService,
@inject(serviceIdentifier.NativeService) private readonly nativeService: INativeService, @inject(serviceIdentifier.NativeService) private readonly nativeService: INativeService,
@inject(serviceIdentifier.Window) private readonly windowService: IWindowService,
) {} ) {}
private notifyGitStateChange(wikiFolderLocation: string, type: IGitStateChange['type']): void { private notifyGitStateChange(wikiFolderLocation: string, type: IGitStateChange['type']): void {
@ -48,6 +49,26 @@ export class Git implements IGitService {
}); });
} }
/**
* Public method to notify file system changes
* Called by watch-fs plugin when files are modified
*/
public notifyFileChange(wikiFolderLocation: string, options?: { onlyWhenGitLogOpened?: boolean }): void {
const { onlyWhenGitLogOpened = true } = options ?? {};
// If we should only notify when git log is open, check if the window exists
if (onlyWhenGitLogOpened) {
const gitLogWindow = this.windowService.get(WindowNames.gitHistory);
// If no git log window is open, skip notification
if (!gitLogWindow) {
return;
}
}
this.notifyGitStateChange(wikiFolderLocation, 'file-change');
}
public async initialize(): Promise<void> { public async initialize(): Promise<void> {
await this.initWorker(); await this.initWorker();
// Register menu items after initialization // Register menu items after initialization
@ -206,7 +227,7 @@ export class Git implements IGitService {
.subscribe(this.getWorkerMessageObserver(wikiFolderPath, resolve, reject)); .subscribe(this.getWorkerMessageObserver(wikiFolderPath, resolve, reject));
}); });
// Log for e2e test detection - indicates initial git setup and commits are complete // Log for e2e test detection - indicates initial git setup and commits are complete
logger.info(`[test-id-git-init-complete]`, { wikiFolderPath }); logger.debug(`[test-id-git-init-complete]`, { wikiFolderPath });
} }
public async commitAndSync(workspace: IWorkspace, configs: ICommitAndSyncConfigs): Promise<boolean> { public async commitAndSync(workspace: IWorkspace, configs: ICommitAndSyncConfigs): Promise<boolean> {
@ -230,19 +251,19 @@ export class Git implements IGitService {
// Generate AI commit message if not provided and settings allow // Generate AI commit message if not provided and settings allow
let finalConfigs = configs; let finalConfigs = configs;
if (!configs.commitMessage) { if (!configs.commitMessage) {
logger.info('No commit message provided, attempting to generate AI commit message'); logger.debug('No commit message provided, attempting to generate AI commit message');
const { generateAICommitMessage } = await import('./aiCommitMessage'); const { generateAICommitMessage } = await import('./aiCommitMessage');
const aiCommitMessage = await generateAICommitMessage(workspace.wikiFolderLocation); const aiCommitMessage = await generateAICommitMessage(workspace.wikiFolderLocation);
if (aiCommitMessage) { if (aiCommitMessage) {
finalConfigs = { ...configs, commitMessage: aiCommitMessage }; finalConfigs = { ...configs, commitMessage: aiCommitMessage };
logger.info('Using AI-generated commit message', { commitMessage: aiCommitMessage }); logger.debug('Using AI-generated commit message', { commitMessage: aiCommitMessage });
} else { } else {
// If AI generation fails or times out, use default message // If AI generation fails or times out, use default message
logger.info('AI commit message generation returned undefined, using default message'); logger.debug('AI commit message generation returned undefined, using default message');
finalConfigs = { ...configs, commitMessage: i18n.t('LOG.CommitBackupMessage') }; finalConfigs = { ...configs, commitMessage: i18n.t('LOG.CommitBackupMessage') };
} }
} else { } else {
logger.info('Commit message already provided, skipping AI generation', { commitMessage: configs.commitMessage }); logger.debug('Commit message already provided, skipping AI generation', { commitMessage: configs.commitMessage });
} }
const observable = this.gitWorker?.commitAndSyncWiki(workspace, finalConfigs, getErrorMessageI18NDict()); const observable = this.gitWorker?.commitAndSyncWiki(workspace, finalConfigs, getErrorMessageI18NDict());
@ -252,7 +273,7 @@ export class Git implements IGitService {
const changeType = configs.commitOnly ? 'commit' : 'sync'; const changeType = configs.commitOnly ? 'commit' : 'sync';
this.notifyGitStateChange(workspace.wikiFolderLocation, changeType); this.notifyGitStateChange(workspace.wikiFolderLocation, changeType);
// Log for e2e test detection // Log for e2e test detection
logger.info(`[test-id-git-${changeType}-complete]`, { wikiFolderLocation: workspace.wikiFolderLocation }); logger.debug(`[test-id-git-${changeType}-complete]`, { wikiFolderLocation: workspace.wikiFolderLocation });
return hasChanges; return hasChanges;
} catch (error: unknown) { } catch (error: unknown) {
const error_ = error as Error; const error_ = error as Error;
@ -363,7 +384,7 @@ export class Git implements IGitService {
// Notify git state change // Notify git state change
this.notifyGitStateChange(wikiFolderPath, 'checkout'); this.notifyGitStateChange(wikiFolderPath, 'checkout');
// Log for e2e test detection // Log for e2e test detection
logger.info(`[test-id-git-checkout-complete]`, { wikiFolderPath, commitHash }); logger.debug(`[test-id-git-checkout-complete]`, { wikiFolderPath, commitHash });
} }
public async revertCommit(wikiFolderPath: string, commitHash: string, commitMessage?: string): Promise<void> { public async revertCommit(wikiFolderPath: string, commitHash: string, commitMessage?: string): Promise<void> {
@ -372,7 +393,7 @@ export class Git implements IGitService {
// Notify git state change // Notify git state change
this.notifyGitStateChange(wikiFolderPath, 'revert'); this.notifyGitStateChange(wikiFolderPath, 'revert');
// Log for e2e test detection // Log for e2e test detection
logger.info(`[test-id-git-revert-complete]`, { wikiFolderPath, commitHash }); logger.debug(`[test-id-git-revert-complete]`, { wikiFolderPath, commitHash });
} catch (error) { } catch (error) {
logger.error('revertCommit failed', { error, wikiFolderPath, commitHash, commitMessage }); logger.error('revertCommit failed', { error, wikiFolderPath, commitHash, commitMessage });
throw error; throw error;
@ -404,4 +425,12 @@ export class Git implements IGitService {
return false; return false;
} }
} }
public async getDeletedTiddlersSinceDate(wikiFolderPath: string, sinceDate: Date): Promise<string[]> {
return await gitOperations.getDeletedTiddlersSinceDate(wikiFolderPath, sinceDate);
}
public async getTiddlerAtTime(wikiFolderPath: string, tiddlerTitle: string, beforeDate: Date): Promise<{ fields: Record<string, unknown>; text: string } | null> {
return await gitOperations.getTiddlerAtTime(wikiFolderPath, tiddlerTitle, beforeDate);
}
} }

View file

@ -74,7 +74,7 @@ export interface IGitStateChange {
/** The workspace folder that changed */ /** The workspace folder that changed */
wikiFolderLocation: string; wikiFolderLocation: string;
/** Type of change */ /** Type of change */
type: 'commit' | 'sync' | 'pull' | 'checkout' | 'revert' | 'discard'; type: 'commit' | 'sync' | 'pull' | 'checkout' | 'revert' | 'discard' | 'file-change';
} }
/** /**
@ -159,6 +159,31 @@ export interface IGitService {
* Check if AI-generated backup title feature is enabled and configured * Check if AI-generated backup title feature is enabled and configured
*/ */
isAIGenerateBackupTitleEnabled(): Promise<boolean>; isAIGenerateBackupTitleEnabled(): Promise<boolean>;
/**
* Get deleted tiddler titles from git history since a specific date
* This looks for deleted .tid and .meta files and extracts their title field
* @param wikiFolderPath - Path to the wiki folder
* @param sinceDate - Date to check for deletions after this time
* @returns Array of deleted tiddler titles
*/
getDeletedTiddlersSinceDate(wikiFolderPath: string, sinceDate: Date): Promise<string[]>;
/**
* Get tiddler content at a specific point in time from git history
* This is used for 3-way merge to get the base version
* @param wikiFolderPath - Path to the wiki folder
* @param tiddlerTitle - Title of the tiddler
* @param beforeDate - Get the version that existed before this date
* @returns Tiddler fields including text, or null if not found
*/
getTiddlerAtTime(wikiFolderPath: string, tiddlerTitle: string, beforeDate: Date): Promise<{ fields: Record<string, unknown>; text: string } | null>;
/**
* Notify that file system changes have been detected
* This is called by watch-fs plugin to trigger git log refresh
* @param wikiFolderLocation - The workspace folder where files changed
* @param options - Notification options
* @param options.onlyWhenGitLogOpened - Only notify if git log window is open (default: true)
*/
notifyFileChange(wikiFolderLocation: string, options?: { onlyWhenGitLogOpened?: boolean }): void;
} }
export const GitServiceIPCDescriptor = { export const GitServiceIPCDescriptor = {
channel: GitChannel.name, channel: GitChannel.name,
@ -170,15 +195,18 @@ export const GitServiceIPCDescriptor = {
discardFileChanges: ProxyPropertyType.Function, discardFileChanges: ProxyPropertyType.Function,
forcePull: ProxyPropertyType.Function, forcePull: ProxyPropertyType.Function,
getCommitFiles: ProxyPropertyType.Function, getCommitFiles: ProxyPropertyType.Function,
getDeletedTiddlersSinceDate: ProxyPropertyType.Function,
getFileBinaryContent: ProxyPropertyType.Function, getFileBinaryContent: ProxyPropertyType.Function,
getFileContent: ProxyPropertyType.Function, getFileContent: ProxyPropertyType.Function,
getFileDiff: ProxyPropertyType.Function, getFileDiff: ProxyPropertyType.Function,
getGitLog: ProxyPropertyType.Function, getGitLog: ProxyPropertyType.Function,
getImageComparison: ProxyPropertyType.Function, getImageComparison: ProxyPropertyType.Function,
getModifiedFileList: ProxyPropertyType.Function, getModifiedFileList: ProxyPropertyType.Function,
getTiddlerAtTime: ProxyPropertyType.Function,
getWorkspacesRemote: ProxyPropertyType.Function, getWorkspacesRemote: ProxyPropertyType.Function,
gitStateChange$: ProxyPropertyType.Value$, gitStateChange$: ProxyPropertyType.Value$,
initWikiGit: ProxyPropertyType.Function, initWikiGit: ProxyPropertyType.Function,
notifyFileChange: ProxyPropertyType.Function,
revertCommit: ProxyPropertyType.Function, revertCommit: ProxyPropertyType.Function,
syncOrForcePull: ProxyPropertyType.Function, syncOrForcePull: ProxyPropertyType.Function,
isAIGenerateBackupTitleEnabled: ProxyPropertyType.Function, isAIGenerateBackupTitleEnabled: ProxyPropertyType.Function,

View file

@ -1,6 +1,6 @@
import { I18NChannels } from '@/constants/channels'; import { I18NChannels } from '@/constants/channels';
import { supportedLanguagesMap, tiddlywikiLanguagesMap } from '@/constants/languages';
import { container } from '@services/container'; import { container } from '@services/container';
import type { IContextService } from '@services/context/interface';
import type { IMenuService } from '@services/menu/interface'; import type { IMenuService } from '@services/menu/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';
@ -10,12 +10,17 @@ import { logger } from '../log';
import { i18n } from '.'; import { i18n } from '.';
export async function requestChangeLanguage(newLanguage: string): Promise<void> { export async function requestChangeLanguage(newLanguage: string): Promise<void> {
const contextService = container.get<IContextService>(serviceIdentifier.Context);
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 wikiService = container.get<IWikiService>(serviceIdentifier.Wiki); const wikiService = container.get<IWikiService>(serviceIdentifier.Wiki);
const menuService = container.get<IMenuService>(serviceIdentifier.MenuService); const menuService = container.get<IMenuService>(serviceIdentifier.MenuService);
const viewCount = await viewService.getViewCount(); const viewCount = await viewService.getViewCount();
// Get language maps from context service
const supportedLanguagesMap = await contextService.get('supportedLanguagesMap');
const tiddlywikiLanguagesMap = await contextService.get('tiddlywikiLanguagesMap');
await i18n.changeLanguage(newLanguage); await i18n.changeLanguage(newLanguage);
viewService.forEachView((_view) => { viewService.forEachView((_view) => {
_view.webContents.send(I18NChannels.changeLanguageRequest, { _view.webContents.send(I18NChannels.changeLanguageRequest, {

View file

@ -1,17 +1,22 @@
import { container } from '@services/container'; import { container } from '@services/container';
import serviceIdentifier from '@services/serviceIdentifier'; import serviceIdentifier from '@services/serviceIdentifier';
import { supportedLanguagesKNames, supportedLanguagesMap } from '@/constants/languages'; import type { IContextService } from '@services/context/interface';
import type { DeferredMenuItemConstructorOptions, IMenuService } from '@services/menu/interface'; import type { DeferredMenuItemConstructorOptions, IMenuService } from '@services/menu/interface';
import type { IPreferenceService } from '@services/preferences/interface'; import type { IPreferenceService } from '@services/preferences/interface';
/** /**
* Register languages into language menu, call this function after container init * Register languages into language menu, call this function after container init
*/ */
export function buildLanguageMenu(): void { export async function buildLanguageMenu(): Promise<void> {
const contextService = container.get<IContextService>(serviceIdentifier.Context);
const preferenceService = container.get<IPreferenceService>(serviceIdentifier.Preference); const preferenceService = container.get<IPreferenceService>(serviceIdentifier.Preference);
const menuService = container.get<IMenuService>(serviceIdentifier.MenuService); const menuService = container.get<IMenuService>(serviceIdentifier.MenuService);
// Load language maps from context service
const supportedLanguagesMap = await contextService.get('supportedLanguagesMap');
const supportedLanguagesKNames = Object.keys(supportedLanguagesMap);
const subMenu: DeferredMenuItemConstructorOptions[] = []; const subMenu: DeferredMenuItemConstructorOptions[] = [];
for (const language of supportedLanguagesKNames) { for (const language of supportedLanguagesKNames) {
subMenu.push({ subMenu.push({
@ -22,5 +27,5 @@ export function buildLanguageMenu(): void {
}); });
} }
void menuService.insertMenu('Language', subMenu); await menuService.insertMenu('Language', subMenu);
} }

View file

@ -127,6 +127,18 @@ const gitGUIApp: IDarwinExternalEditor[] = [
name: 'GitHub Desktop', name: 'GitHub Desktop',
bundleIdentifiers: ['com.github.GitHubClient'], bundleIdentifiers: ['com.github.GitHubClient'],
}, },
{
name: 'GitKraken',
bundleIdentifiers: ['com.axosoft.gitkraken'],
},
{
name: 'SourceTree',
bundleIdentifiers: ['com.torusknot.SourceTreeNotMAS'],
},
{
name: 'Tower',
bundleIdentifiers: ['com.fournova.Tower2', 'com.fournova.Tower3'],
},
]; ];
async function findApplication(editor: IDarwinExternalEditor): Promise<string | null> { async function findApplication(editor: IDarwinExternalEditor): Promise<string | null> {

View file

@ -65,6 +65,14 @@ const gitGUIApp: ILinuxExternalEditor[] = [
name: 'GitHub Desktop', name: 'GitHub Desktop',
paths: ['/snap/bin/desktop', '/usr/bin/desktop'], paths: ['/snap/bin/desktop', '/usr/bin/desktop'],
}, },
{
name: 'GitKraken',
paths: ['/usr/bin/gitkraken', '/snap/bin/gitkraken'],
},
{
name: 'GitAhead',
paths: ['/usr/bin/gitahead'],
},
]; ];
async function getAvailablePath(paths: string[]): Promise<string | null> { async function getAvailablePath(paths: string[]): Promise<string | null> {

View file

@ -330,6 +330,27 @@ const gitGUIApp: WindowsExternalEditor[] = [
displayNamePrefix: 'GitHub', displayNamePrefix: 'GitHub',
publisher: 'GitHub, Inc.', publisher: 'GitHub, Inc.',
}, },
{
name: 'GitKraken',
registryKeys: [CurrentUserUninstallKey('gitkraken'), LocalMachineUninstallKey('gitkraken')],
executableShimPaths: [['gitkraken.exe']],
displayNamePrefix: 'GitKraken',
publisher: 'Axosoft, LLC',
},
{
name: 'SourceTree',
registryKeys: [CurrentUserUninstallKey('SourceTree'), LocalMachineUninstallKey('SourceTree')],
executableShimPaths: [['SourceTree.exe']],
displayNamePrefix: 'SourceTree',
publisher: 'Atlassian',
},
{
name: 'TortoiseGit',
registryKeys: [LocalMachineUninstallKey('TortoiseGit'), Wow64LocalMachineUninstallKey('TortoiseGit')],
executableShimPaths: [['bin', 'TortoiseGitProc.exe']],
displayNamePrefix: 'TortoiseGit',
publisher: 'TortoiseGit',
},
]; ];
function getKeyOrEmpty(keys: readonly RegistryValue[], key: string): string { function getKeyOrEmpty(keys: readonly RegistryValue[], key: string): string {

View file

@ -169,9 +169,7 @@ export class Wiki implements IWikiService {
logger.debug(`wikiWorker initialized`, { function: 'Wiki.startWiki' }); logger.debug(`wikiWorker initialized`, { function: 'Wiki.startWiki' });
this.wikiWorkers[workspaceID] = { proxy: worker, nativeWorker: wikiWorker, detachWorker }; this.wikiWorkers[workspaceID] = { proxy: worker, nativeWorker: wikiWorker, detachWorker };
this.wikiWorkerStartedEventTarget.dispatchEvent(new Event(wikiWorkerStartedEventName(workspaceID))); this.wikiWorkerStartedEventTarget.dispatchEvent(new Event(wikiWorkerStartedEventName(workspaceID)));
void worker.notifyServicesReady();
// Notify worker that services are ready to use
worker.notifyServicesReady();
const loggerMeta = { worker: 'NodeJSWiki', homePath: wikiFolderLocation, workspaceID }; const loggerMeta = { worker: 'NodeJSWiki', homePath: wikiFolderLocation, workspaceID };
@ -546,11 +544,11 @@ export class Wiki implements IWikiService {
if (!isWikiWorkspace(workspace)) { if (!isWikiWorkspace(workspace)) {
return true; // dedicated workspaces always "exist" return true; // dedicated workspaces always "exist"
} }
const { wikiFolderLocation, id: workspaceID } = workspace; const { wikiFolderLocation, id: workspaceID, name } = workspace;
const { shouldBeMainWiki, showDialog } = options; const { shouldBeMainWiki, showDialog } = options;
try { try {
if (typeof wikiFolderLocation !== 'string' || wikiFolderLocation.length === 0 || !path.isAbsolute(wikiFolderLocation)) { if (typeof wikiFolderLocation !== 'string' || wikiFolderLocation.length === 0 || !path.isAbsolute(wikiFolderLocation)) {
const errorMessage = i18n.t('Dialog.NeedCorrectTiddlywikiFolderPath') + wikiFolderLocation; const errorMessage = i18n.t('Dialog.NeedCorrectTiddlywikiFolderPath', { name, wikiFolderLocation });
logger.error(errorMessage); logger.error(errorMessage);
const windowService = container.get<IWindowService>(serviceIdentifier.Window); const windowService = container.get<IWindowService>(serviceIdentifier.Window);
const mainWindow = windowService.get(WindowNames.main); const mainWindow = windowService.get(WindowNames.main);
@ -673,7 +671,7 @@ export class Wiki implements IWikiService {
const workspaceViewService = container.get<IWorkspaceViewService>(serviceIdentifier.WorkspaceView); const workspaceViewService = container.get<IWorkspaceViewService>(serviceIdentifier.WorkspaceView);
await workspaceViewService.restartWorkspaceViewService(mainWikiID); await workspaceViewService.restartWorkspaceViewService(mainWikiID);
// Log that main wiki restart is complete after creating sub-wiki // Log that main wiki restart is complete after creating sub-wiki
logger.info('[test-id-MAIN_WIKI_RESTARTED_AFTER_SUBWIKI] Main wiki restarted after sub-wiki creation', { mainWikiID, subWikiID: id }); logger.debug('[test-id-MAIN_WIKI_RESTARTED_AFTER_SUBWIKI] Main wiki restarted after sub-wiki creation', { mainWikiID, subWikiID: id });
} }
} else { } else {
try { try {
@ -777,10 +775,9 @@ export class Wiki implements IWikiService {
public async getTiddlerFilePath(title: string, workspaceID?: string): Promise<string | undefined> { public async getTiddlerFilePath(title: string, workspaceID?: string): Promise<string | undefined> {
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace); const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
const activeWorkspace = await workspaceService.getActiveWorkspace(); const wikiWorker = this.getWorker(workspaceID ?? (await workspaceService.getActiveWorkspace())?.id ?? '');
const wikiWorker = this.getWorker(workspaceID ?? activeWorkspace?.id ?? '');
if (wikiWorker !== undefined) { if (wikiWorker !== undefined) {
const tiddlerFileMetadata = wikiWorker.getTiddlerFileMetadata(title); const tiddlerFileMetadata = await wikiWorker.getTiddlerFileMetadata(title);
if (tiddlerFileMetadata?.filepath !== undefined) { if (tiddlerFileMetadata?.filepath !== undefined) {
return tiddlerFileMetadata.filepath; return tiddlerFileMetadata.filepath;
} }

View file

@ -11,7 +11,7 @@ function getInfoTiddlerFields(updateInfoTiddlersCallback: (infos: Array<{ text:
// Basics // Basics
if (!$tw.browser || typeof window === 'undefined') return infoTiddlerFields; if (!$tw.browser || typeof window === 'undefined') return infoTiddlerFields;
const isInTidGi = typeof document !== 'undefined' && document.location.protocol.startsWith('tidgi'); const isInTidGi = typeof document !== 'undefined' && document.location.protocol.startsWith('tidgi');
const workspace = (window.meta() as WindowMeta[WindowNames.view] | undefined)?.workspace; const workspace = (typeof window.meta === 'function' ? window.meta() as WindowMeta[WindowNames.view] : undefined)?.workspace;
const workspaceID = workspace?.id; const workspaceID = workspace?.id;
infoTiddlerFields.push({ title: '$:/info/tidgi', text: mapBoolean(isInTidGi) }); infoTiddlerFields.push({ title: '$:/info/tidgi', text: mapBoolean(isInTidGi) });
if (isInTidGi && workspaceID) { if (isInTidGi && workspaceID) {

View file

@ -39,7 +39,7 @@ class TidGiIPCSyncAdaptor {
this.isLoggedIn = false; this.isLoggedIn = false;
this.isReadOnly = false; this.isReadOnly = false;
this.logoutIsAvailable = true; this.logoutIsAvailable = true;
const workspaceID = (window.meta() as WindowMeta[WindowNames.view]).workspace?.id; const workspaceID = (typeof window.meta === 'function' ? window.meta() as WindowMeta[WindowNames.view] : undefined)?.workspace?.id;
if (workspaceID === undefined) { if (workspaceID === undefined) {
throw new Error('TidGiIPCSyncAdaptor: workspaceID is undefined. Cannot initialize sync adaptor without a valid workspace ID.'); throw new Error('TidGiIPCSyncAdaptor: workspaceID is undefined. Cannot initialize sync adaptor without a valid workspace ID.');
} }
@ -319,7 +319,7 @@ class TidGiIPCSyncAdaptor {
if ($tw.browser && typeof window !== 'undefined') { if ($tw.browser && typeof window !== 'undefined') {
const isInTidGi = typeof document !== 'undefined' && document.location.protocol.startsWith('tidgi'); const isInTidGi = typeof document !== 'undefined' && document.location.protocol.startsWith('tidgi');
const servicesExposed = Boolean(window.service.wiki); const servicesExposed = Boolean(window.service.wiki);
const hasWorkspaceIDinMeta = Boolean((window.meta() as WindowMeta[WindowNames.view] | undefined)?.workspace?.id); const hasWorkspaceIDinMeta = Boolean((typeof window.meta === 'function' ? window.meta() as WindowMeta[WindowNames.view] : undefined)?.workspace?.id);
if (isInTidGi && servicesExposed && hasWorkspaceIDinMeta) { if (isInTidGi && servicesExposed && hasWorkspaceIDinMeta) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
exports.adaptorClass = TidGiIPCSyncAdaptor; exports.adaptorClass = TidGiIPCSyncAdaptor;

View file

@ -1,4 +1,4 @@
import { workspace } from '@services/wiki/wikiWorker/services'; import { git, workspace } from '@services/wiki/wikiWorker/services';
import fs from 'fs'; import fs from 'fs';
import nsfw from 'nsfw'; import nsfw from 'nsfw';
import path from 'path'; import path from 'path';
@ -23,6 +23,14 @@ const FILE_DELETION_DELAY_MS = 100;
*/ */
const FILE_INCLUSION_DELAY_MS = 150; const FILE_INCLUSION_DELAY_MS = 150;
/**
* Delay before notifying git service about file changes.
* This prevents excessive git status checks when multiple files change rapidly.
* The delay aggregates multiple file changes into a single notification.
* Increased to 1000ms to prevent refresh storms during git operations like discard.
*/
const GIT_NOTIFICATION_DELAY_MS = 1000;
/** /**
* Enhanced filesystem adaptor that extends FileSystemAdaptor with file watching capabilities. * Enhanced filesystem adaptor that extends FileSystemAdaptor with file watching capabilities.
* *
@ -70,6 +78,11 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
* Cleared during cleanup to prevent orphaned timeouts. * Cleared during cleanup to prevent orphaned timeouts.
*/ */
private pendingInclusions: Map<string, NodeJS.Timeout> = new Map(); private pendingInclusions: Map<string, NodeJS.Timeout> = new Map();
/**
* Timer for debouncing git notifications.
* Aggregates multiple file changes into a single git status check.
*/
private gitNotificationTimer: NodeJS.Timeout | undefined;
constructor(options: { boot?: typeof $tw.boot; wiki: Wiki }) { constructor(options: { boot?: typeof $tw.boot; wiki: Wiki }) {
super(options); super(options);
@ -225,7 +238,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
// Initialize sub-wiki watchers // Initialize sub-wiki watchers
await this.initializeSubWikiWatchers(); await this.initializeSubWikiWatchers();
// Log stabilization marker for tests // Log stabilization marker for tests
this.logger.log('[test-id-WATCH_FS_STABILIZED] Watcher has stabilized'); this.logger.log('[test-id-WATCH_FS_STABILIZED] Watcher has stabilized', { level: 'debug' });
} catch (error) { } catch (error) {
this.logger.alert('[WATCH_FS_ERROR] Failed to initialize file watching:', error); this.logger.alert('[WATCH_FS_ERROR] Failed to initialize file watching:', error);
} }
@ -329,6 +342,9 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
this.logger.log(`[WATCH_FS_INCLUDE] Including file: ${absoluteFilePath}`); this.logger.log(`[WATCH_FS_INCLUDE] Including file: ${absoluteFilePath}`);
this.inverseFilesIndex.includeFile(absoluteFilePath); this.inverseFilesIndex.includeFile(absoluteFilePath);
this.pendingInclusions.delete(absoluteFilePath); this.pendingInclusions.delete(absoluteFilePath);
// Notify git service when file is included after being saved
// This ensures that frontend-initiated changes also trigger git log refresh
this.scheduleGitNotification();
}, FILE_INCLUSION_DELAY_MS); }, FILE_INCLUSION_DELAY_MS);
this.pendingInclusions.set(absoluteFilePath, timer); this.pendingInclusions.set(absoluteFilePath, timer);
@ -372,6 +388,8 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
* Handle NSFW file system change events * Handle NSFW file system change events
*/ */
private handleNsfwEvents(events: nsfw.FileChangeEvent[]): void { private handleNsfwEvents(events: nsfw.FileChangeEvent[]): void {
let hasFileChanges = false;
for (const event of events) { for (const event of events) {
const { action, directory } = event; const { action, directory } = event;
@ -397,6 +415,9 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
continue; continue;
} }
// Mark that we have file changes to notify git
hasFileChanges = true;
// Determine which wiki this file belongs to and compute relative path accordingly // Determine which wiki this file belongs to and compute relative path accordingly
const subWikiInfo = this.inverseFilesIndex.getSubWikiForFile(fileAbsolutePath); const subWikiInfo = this.inverseFilesIndex.getSubWikiForFile(fileAbsolutePath);
const basePath = subWikiInfo ? subWikiInfo.path : this.watchPathBase; const basePath = subWikiInfo ? subWikiInfo.path : this.watchPathBase;
@ -463,6 +484,40 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
} }
} }
} }
// Notify git service about file changes with debounce
if (hasFileChanges) {
this.scheduleGitNotification();
}
}
/**
* Schedule a debounced notification to git service about file changes.
* Multiple file changes are aggregated into a single notification.
*/
private scheduleGitNotification(): void {
// Clear existing timer
if (this.gitNotificationTimer) {
clearTimeout(this.gitNotificationTimer);
}
// Schedule new notification
this.gitNotificationTimer = setTimeout(() => {
// Notify git service about file changes
// Pass the wiki folder (parent of tiddlers folder) as wikiFolderLocation
// watchPathBase is wiki/tiddlers, but wikiFolderLocation should be wiki
const wikiFolderLocation = path.dirname(this.watchPathBase);
try {
(git.notifyFileChange as ((path: string, options?: { onlyWhenGitLogOpened?: boolean }) => void))(
wikiFolderLocation,
{ onlyWhenGitLogOpened: true },
);
this.logger.log(`[WATCH_FS_GIT_NOTIFY] Notified git service about file changes in ${wikiFolderLocation}`);
} catch (error) {
this.logger.alert('[WATCH_FS_GIT_NOTIFY_ERROR] Failed to notify git service:', error);
}
this.gitNotificationTimer = undefined;
}, GIT_NOTIFICATION_DELAY_MS);
} }
/** /**
@ -536,9 +591,9 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
// Log appropriate event // Log appropriate event
if (isNewFile) { if (isNewFile) {
this.logger.log(`[test-id-WATCH_FS_TIDDLER_ADDED] ${tiddlerTitle}`); this.logger.log(`[test-id-WATCH_FS_TIDDLER_ADDED] ${tiddlerTitle}`, { level: 'debug' });
} else { } else {
this.logger.log(`[test-id-WATCH_FS_TIDDLER_UPDATED] ${tiddlerTitle}`); this.logger.log(`[test-id-WATCH_FS_TIDDLER_UPDATED] ${tiddlerTitle}`, { level: 'debug' });
} }
} }
} }
@ -580,7 +635,7 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
// Delete the tiddler from wiki to trigger change event // Delete the tiddler from wiki to trigger change event
$tw.syncadaptor!.wiki.deleteTiddler(tiddlerTitle); $tw.syncadaptor!.wiki.deleteTiddler(tiddlerTitle);
this.logger.log(`[test-id-WATCH_FS_TIDDLER_DELETED] ${tiddlerTitle}`); this.logger.log(`[test-id-WATCH_FS_TIDDLER_DELETED] ${tiddlerTitle}`, { level: 'debug' });
// Delete system tiddler empty file if exists // Delete system tiddler empty file if exists
try { try {
@ -603,6 +658,12 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
* Cleanup method to properly close watcher when wiki is shutting down * Cleanup method to properly close watcher when wiki is shutting down
*/ */
public async cleanup(): Promise<void> { public async cleanup(): Promise<void> {
// Clear git notification timer
if (this.gitNotificationTimer) {
clearTimeout(this.gitNotificationTimer);
this.gitNotificationTimer = undefined;
}
// Clear all pending deletion timers // Clear all pending deletion timers
for (const timer of this.pendingDeletions.values()) { for (const timer of this.pendingDeletions.values()) {
clearTimeout(timer); clearTimeout(timer);

View file

@ -107,9 +107,10 @@ async function beforeExit(): Promise<void> {
} }
} }
// All exposed methods should be async.
const wikiWorker = { const wikiWorker = {
startNodeJSWiki, startNodeJSWiki,
getTiddlerFileMetadata: (tiddlerTitle: string) => getWikiInstance()?.boot.files[tiddlerTitle], getTiddlerFileMetadata: async (tiddlerTitle: string) => getWikiInstance()?.boot.files[tiddlerTitle],
executeZxScript, executeZxScript,
extractWikiHTML, extractWikiHTML,
packetHTMLFromWikiFolder, packetHTMLFromWikiFolder,

View file

@ -258,7 +258,7 @@ export class IpcServerRoutes {
// Log SSE ready every time a new observer subscribes (including after worker restart) // Log SSE ready every time a new observer subscribes (including after worker restart)
// Include timestamp to make each log entry unique for test detection // Include timestamp to make each log entry unique for test detection
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
console.log(`[test-id-SSE_READY] Wiki change observer registered and ready at ${timestamp}`); console.debug(`[test-id-SSE_READY] Wiki change observer registered and ready at ${timestamp}`);
}; };
void getWikiChangeObserverInWorkerIIFE(); void getWikiChangeObserverInWorkerIIFE();
}); });

View file

@ -25,7 +25,7 @@ export function onWorkerServicesReady(callback: () => void): void {
* Mark worker services as ready and execute all pending callbacks * Mark worker services as ready and execute all pending callbacks
* This should be called by main process after attachWorker is complete * This should be called by main process after attachWorker is complete
*/ */
export function notifyServicesReady(): void { export async function notifyServicesReady(): Promise<void> {
console.log('[servicesReady] Worker services marked as ready'); console.log('[servicesReady] Worker services marked as ready');
servicesReady = true; servicesReady = true;
onServicesReadyCallbacks.forEach(callback => { onServicesReadyCallbacks.forEach(callback => {

View file

@ -157,6 +157,12 @@ export class Window implements IWindowService {
// Don't bring up window when running e2e test, otherwise it will annoy the developer who is doing other things. // Don't bring up window when running e2e test, otherwise it will annoy the developer who is doing other things.
existedWindow.show(); existedWindow.show();
} }
// Realign workspace view when reopening window to ensure browser view is properly positioned
// This fixes issue #626: white screen after hiding and reopening window
const WindowWithBrowserView = [WindowNames.main, WindowNames.tidgiMiniWindow];
if (WindowWithBrowserView.includes(windowName)) {
await container.get<IWorkspaceViewService>(serviceIdentifier.WorkspaceView).realignActiveWorkspace();
}
if (returnWindow === true) { if (returnWindow === true) {
return existedWindow; return existedWindow;
} }

View file

@ -3,10 +3,10 @@ import { app, dialog, session } from 'electron';
import { inject, injectable } from 'inversify'; import { inject, injectable } from 'inversify';
import { WikiChannel } from '@/constants/channels'; import { WikiChannel } from '@/constants/channels';
import { tiddlywikiLanguagesMap } from '@/constants/languages';
import { WikiCreationMethod } from '@/constants/wikiCreation'; import { WikiCreationMethod } from '@/constants/wikiCreation';
import type { IAuthenticationService } from '@services/auth/interface'; import type { IAuthenticationService } from '@services/auth/interface';
import { container } from '@services/container'; import { container } from '@services/container';
import type { IContextService } from '@services/context/interface';
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';
@ -179,6 +179,8 @@ export class WorkspaceView implements IWorkspaceViewService {
if (view !== undefined) { if (view !== undefined) {
// if is newly created wiki, we set the language as user preference // if is newly created wiki, we set the language as user preference
const currentLanguage = await this.preferenceService.get('language'); const currentLanguage = await this.preferenceService.get('language');
const contextService = container.get<IContextService>(serviceIdentifier.Context);
const tiddlywikiLanguagesMap = await contextService.get('tiddlywikiLanguagesMap');
const tiddlywikiLanguageName = tiddlywikiLanguagesMap[currentLanguage]; const tiddlywikiLanguageName = tiddlywikiLanguagesMap[currentLanguage];
if (tiddlywikiLanguageName === undefined) { if (tiddlywikiLanguageName === undefined) {
const errorMessage = `When creating new wiki, and switch to language "${currentLanguage}", there is no corresponding tiddlywiki language registered`; const errorMessage = `When creating new wiki, and switch to language "${currentLanguage}", there is no corresponding tiddlywiki language registered`;

View file

@ -293,7 +293,7 @@ export function CommitDetailsPanel(
data-testid='commit-now-button' data-testid='commit-now-button'
startIcon={isCommitting ? <CircularProgress size={16} color='inherit' /> : undefined} startIcon={isCommitting ? <CircularProgress size={16} color='inherit' /> : undefined}
> >
{isCommitting ? t('GitLog.Committing') : t('GitLog.CommitNow')} {isCommitting ? t('GitLog.Committing') : t('ContextMenu.BackupNow')}
</Button> </Button>
<Divider sx={{ my: 1 }} /> <Divider sx={{ my: 1 }} />
</> </>

View file

@ -135,9 +135,10 @@ const ImagePreview = styled('img')`
interface IFileDiffPanelProps { interface IFileDiffPanelProps {
commitHash: string; commitHash: string;
filePath: string | null; filePath: string | null;
onDiscardSuccess?: () => void;
} }
export function FileDiffPanel({ commitHash, filePath }: IFileDiffPanelProps): React.JSX.Element { export function FileDiffPanel({ commitHash, filePath, onDiscardSuccess }: IFileDiffPanelProps): React.JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
const [diff, setDiff] = useState<string>(''); const [diff, setDiff] = useState<string>('');
const [fileContent, setFileContent] = useState<string>(''); const [fileContent, setFileContent] = useState<string>('');
@ -191,7 +192,8 @@ export function FileDiffPanel({ commitHash, filePath }: IFileDiffPanelProps): Re
try { try {
await window.service.git.discardFileChanges(workspace.wikiFolderLocation, filePath); await window.service.git.discardFileChanges(workspace.wikiFolderLocation, filePath);
// TODO: Show success message and refresh file list // Clear selection and trigger refresh
onDiscardSuccess?.();
} catch (error) { } catch (error) {
console.error('Failed to discard changes:', error); console.error('Failed to discard changes:', error);
// TODO: Show error message // TODO: Show error message

View file

@ -226,8 +226,9 @@ export default function GitHistory(): React.JSX.Element {
}, [lastChangeType, entries, setSelectedCommit]); }, [lastChangeType, entries, setSelectedCommit]);
// Maintain selection across refreshes by hash // Maintain selection across refreshes by hash
// Skip if we should select first (manual commit) or if a commit just happened (auto-selection in progress)
useEffect(() => { useEffect(() => {
if (selectedCommit && entries.length > 0) { if (selectedCommit && entries.length > 0 && !shouldSelectFirst && lastChangeType !== 'commit') {
// Try to find the same commit in the new entries // Try to find the same commit in the new entries
const stillExists = entries.find((entry) => entry.hash === selectedCommit.hash); const stillExists = entries.find((entry) => entry.hash === selectedCommit.hash);
// Only update if data actually changed (compare by serialization) // Only update if data actually changed (compare by serialization)
@ -236,7 +237,7 @@ export default function GitHistory(): React.JSX.Element {
setSelectedCommit(stillExists); setSelectedCommit(stillExists);
} }
} }
}, [entries, selectedCommit]); }, [entries, selectedCommit, shouldSelectFirst, lastChangeType]);
const handleCommitSuccess = () => { const handleCommitSuccess = () => {
// Trigger selection of first commit after data refreshes // Trigger selection of first commit after data refreshes
@ -381,6 +382,11 @@ export default function GitHistory(): React.JSX.Element {
<FileDiffPanel <FileDiffPanel
commitHash={selectedCommit?.hash || ''} commitHash={selectedCommit?.hash || ''}
filePath={selectedFile} filePath={selectedFile}
onDiscardSuccess={() => {
setSelectedFile(null);
// Trigger git log refresh after discard
setShouldSelectFirst(true);
}}
/> />
</DiffPanelWrapper> </DiffPanelWrapper>
</ContentWrapper> </ContentWrapper>

View file

@ -1,6 +1,6 @@
import type { IWorkspace } from '@services/workspaces/interface'; import type { IWorkspace } from '@services/workspaces/interface';
import useObservable from 'beautiful-react-hooks/useObservable'; import useObservable from 'beautiful-react-hooks/useObservable';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
import type { GitLogEntry } from './types'; import type { GitLogEntry } from './types';
@ -22,6 +22,9 @@ export function useGitLogData(): IGitLogData {
const [workspaceInfo, setWorkspaceInfo] = useState<IWorkspace | null>(null); const [workspaceInfo, setWorkspaceInfo] = useState<IWorkspace | null>(null);
const [refreshTrigger, setRefreshTrigger] = useState(0); const [refreshTrigger, setRefreshTrigger] = useState(0);
const [lastChangeType, setLastChangeType] = useState<string | null>(null); const [lastChangeType, setLastChangeType] = useState<string | null>(null);
const lastLoggedEntriesCount = useRef<number>(0);
const lastRefreshTime = useRef<number>(0);
const lastChangeTimestamp = useRef<number>(0);
// Get workspace info once // Get workspace info once
useEffect(() => { useEffect(() => {
@ -68,10 +71,30 @@ export function useGitLogData(): IGitLogData {
); );
useObservable(gitStateChange$, (change) => { useObservable(gitStateChange$, (change) => {
// Store the type of change so we can auto-select first commit after a manual commit // Debounce git state changes to prevent excessive refreshes
setLastChangeType(change?.type ?? null); // Git operations (like discard) may trigger multiple file system events
// Trigger refresh when git state changes const now = Date.now();
setRefreshTrigger((previous) => previous + 1); const timeSinceLastRefresh = now - lastRefreshTime.current;
// Check if this is the same change event (within 100ms window)
// This prevents duplicate events from triggering multiple refreshes
if (change?.timestamp === lastChangeTimestamp.current) {
return;
}
// For file-change events, use longer debounce (1000ms) to avoid watch-fs storm
// For other git operations (commit, discard, etc), use shorter debounce (300ms)
const debounceTime = change?.type === 'file-change' ? 1000 : 300;
// Allow refresh if enough time has passed since last refresh
if (timeSinceLastRefresh >= debounceTime) {
lastRefreshTime.current = now;
lastChangeTimestamp.current = change?.timestamp ?? 0;
// Store the type of change so we can auto-select first commit after a manual commit
setLastChangeType(change?.type ?? null);
// Trigger refresh when git state changes
setRefreshTrigger((previous) => previous + 1);
}
}); });
// Load git log data // Load git log data
@ -116,11 +139,12 @@ export function useGitLogData(): IGitLogData {
requestAnimationFrame(() => { requestAnimationFrame(() => {
setEntries(entriesWithFiles); setEntries(entriesWithFiles);
setCurrentBranch(result.currentBranch); setCurrentBranch(result.currentBranch);
// Log for E2E test timing - indicates UI has been updated with new commits });
void window.service.native.log('info', '[test-id-git-log-refreshed]', {
commitCount: entriesWithFiles.length, // Log for E2E test timing - only log once per load, not in requestAnimationFrame
wikiFolderLocation: workspaceInfo.wikiFolderLocation, void window.service.native.log('debug', '[test-id-git-log-refreshed]', {
}); commitCount: entriesWithFiles.length,
wikiFolderLocation: workspaceInfo.wikiFolderLocation,
}); });
} catch (error_) { } catch (error_) {
const error = error_ as Error; const error = error_ as Error;
@ -140,13 +164,17 @@ export function useGitLogData(): IGitLogData {
// Log when entries are updated and rendered to DOM // Log when entries are updated and rendered to DOM
useEffect(() => { useEffect(() => {
if (entries.length > 0 && workspaceInfo && 'wikiFolderLocation' in workspaceInfo) { if (entries.length > 0 && workspaceInfo && 'wikiFolderLocation' in workspaceInfo) {
// Use setTimeout to ensure DOM has been updated after state changes // Only log if the entries count actually changed (to avoid logging on every re-render)
setTimeout(() => { if (lastLoggedEntriesCount.current !== entries.length) {
void window.service.native.log('info', '[test-id-git-log-data-rendered]', { lastLoggedEntriesCount.current = entries.length;
commitCount: entries.length, // Use setTimeout to ensure DOM has been updated after state changes
wikiFolderLocation: workspaceInfo.wikiFolderLocation, setTimeout(() => {
}); void window.service.native.log('debug', '[test-id-git-log-data-rendered]', {
}, 100); commitCount: entries.length,
wikiFolderLocation: workspaceInfo.wikiFolderLocation,
});
}, 100);
}
} }
}, [entries, workspaceInfo]); }, [entries, workspaceInfo]);

View file

@ -15,7 +15,7 @@ import {
TextField, TextField,
Typography, Typography,
} from '@mui/material'; } from '@mui/material';
import defaultProvidersConfig from '@services/externalAPI/defaultProviders.json'; import defaultProvidersConfig from '@services/externalAPI/defaultProviders';
import { ModelFeature, ModelInfo } from '@services/externalAPI/interface'; import { ModelFeature, ModelInfo } from '@services/externalAPI/interface';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';

View file

@ -5,7 +5,7 @@ import { Dispatch, SetStateAction, SyntheticEvent, useEffect, useMemo, useState
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ListItemText } from '@/components/ListItem'; import { ListItemText } from '@/components/ListItem';
import defaultProvidersConfig from '@services/externalAPI/defaultProviders.json'; import defaultProvidersConfig from '@services/externalAPI/defaultProviders';
import { AIProviderConfig, ModelFeature, ModelInfo } from '@services/externalAPI/interface'; import { AIProviderConfig, ModelFeature, ModelInfo } from '@services/externalAPI/interface';
import { ListItemVertical } from '../../../PreferenceComponents'; import { ListItemVertical } from '../../../PreferenceComponents';
import { NewModelDialog } from './NewModelDialog'; import { NewModelDialog } from './NewModelDialog';

@ -1 +1 @@
Subproject commit e7d18ab75be6ff8c2a068c13110e4a72fe3ac088 Subproject commit bb58ff74794ceaeb84d69a359da9361e2e1642ef