feat: compress index.json output for reduced file size#9
Conversation
- Remove AnimatePresence and motion.div wrapper from main content area - Replace with standard div for instant view switching - Preserve sidebar margin transition effect This change improves perceived responsiveness when navigating between views in the sidebar navigation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR introduces multi-source package management (local folders, GitHub Releases, HTTP indices) with an abstract PackageSource layer, adds license management with persistence and sync-to-version capabilities, and implements comprehensive Redux state management, UI components, and IPC integration across the full stack. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User (UI)
participant Redux as Redux Store
participant Saga as Package Source Saga
participant IPC as Electron IPC
participant Main as Main Process
participant Source as Package Source
User->>Redux: Dispatch VALIDATE_CONFIG
Redux->>Saga: Trigger Validation
Saga->>IPC: Call validateConfig(config)
IPC->>Main: IPC Handler
Main->>Source: Create source & validate
Source->>Source: Check URL/Path/Auth
Source-->>Main: Validation Result
Main-->>IPC: Return result
IPC-->>Saga: Validation complete
Saga->>Redux: Set validation status
Redux-->>User: Display result
User->>Redux: Dispatch SCAN_FOLDER
Redux->>Saga: Trigger scan
Saga->>IPC: Call scanFolder(path)
IPC->>Main: IPC Handler
Main->>Source: List available versions
Source->>Source: Scan directory/API
Source-->>Main: Version list
Main-->>IPC: Return versions
IPC-->>Saga: Scan complete
Saga->>Redux: Set available versions
Redux-->>User: Display versions
sequenceDiagram
participant User as User (License UI)
participant Redux as Redux Store
participant Saga as License Saga
participant IPC as Electron IPC
participant Main as Main Process
participant Manager as License Manager
participant Config as ConfigManager
User->>Redux: Dispatch FETCH_LICENSE
Redux->>Saga: Load license
Saga->>IPC: Call license.get()
IPC->>Main: IPC Handler
Main->>Manager: getLicense()
Manager->>Config: Retrieve license
Config-->>Manager: License data
Manager-->>Main: Return license
Main-->>IPC: Send to renderer
IPC-->>Saga: License received
Saga->>Redux: Set license & clear loading
Redux-->>User: Display license
User->>Redux: Input new license key
User->>Redux: Dispatch SAVE_LICENSE
Redux->>Saga: Save license
Saga->>IPC: Call license.save(key)
IPC->>Main: IPC Handler
Main->>Manager: saveLicense(key)
Manager->>Manager: syncToAppSettings()
Manager->>Config: setLicense(key)
Config-->>Manager: Persisted
Manager-->>Main: Sync status
Main-->>IPC: Return success
IPC-->>Saga: Saved & synced
Saga->>Redux: Set license, show toast
Redux-->>User: Display success
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 20
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/renderer/components/VersionManagementPage.tsx (1)
66-83:⚠️ Potential issue | 🟡 MinorAdd missing
onVersionListChangedmethod to theWindow.electronAPIinterface.The
window.electronAPI.onVersionListChangedmethod is used at line 129 but is not declared in the localWindow.electronAPIinterface. Add the following line to the interface (afteronActiveVersionChanged):onVersionListChanged: (callback: () => void) => void;The method is implemented in
src/preload/index.ts:132and needs to be declared in the local type definition to avoid TypeScript errors.
🤖 Fix all issues with AI agents
In `@src/main/config.ts`:
- Around line 94-100: In setLicense(licenseKey: string) remove the unused local
variable current (the assignment from this.getLicense()) since it’s not
referenced; update the setLicense method to simply call this.set('license', {
licenseKey, isConfigured: !!licenseKey, updatedAt: new Date().toISOString() })
without declaring current so no unused variable remains.
In `@src/main/main.ts`:
- Around line 1444-1448: The VersionManager is constructed synchronously but
kicks off async work in initializePackageSource(), causing callers (e.g., the
immediate IIFE that uses versionManager) to race with initialization; change the
pattern so construction is synchronous-only and expose an explicit async
initializer (e.g., VersionManager.initializePackageSource or a static async
factory like VersionManager.create(...)) and then await that initializer right
after creating packageSourceConfigManager and before any use of versionManager
(replace the current new VersionManager(...) call with await
VersionManager.create(...) or call await
versionManager.initializePackageSource()). Ensure initializePackageSource() is
moved out of the constructor and returns a Promise so callers can reliably await
completion.
In `@src/main/package-source-config-manager.ts`:
- Around line 278-283: The code in the addSource call inside
package-source-config-manager.ts hardcodes a developer-specific path for
defaultSource; update that call (the defaultSource initialization that invokes
this.addSource) to read the path from an environment/config variable (e.g.
process.env.DEFAULT_LOCAL_PACKAGE_PATH) with a sensible fallback (or omit
fallback and fail-fast) instead of using "/home/newbe36524/…"; ensure you only
change the path property passed to this.addSource and keep the same type/name
values so defaultSource creation logic remains intact.
In `@src/main/package-sources/github-release-source.ts`:
- Around line 329-334: The regex in extractVersionFromFilename only allows
single-digit version parts, so update the pattern(s) to use [0-9]+ for each
numeric component (e.g., change [0-9]\.[0-9]\.[0-9] to [0-9]+(?:\.[0-9]+){2} or
similar) to support multi-digit segments and keep the rest of the capture group
intact; apply the same change to the other occurrence around lines 339-344 so
both extractors accept versions like 10.20.30 while preserving the existing
optional prerelease capture and platform suffix handling in
extractVersionFromFilename.
- Around line 388-406: The current compareVersions implementation in
compareVersions(v1: string, v2: string) strips prerelease info and misorders
semver pre-releases; replace its body to use the semver library by calling
semver.compare(v1, v2) (ensure semver is imported at the top of the file),
removing the manual parseVersion logic so prerelease and build metadata are
handled correctly.
In `@src/main/package-sources/http-index-source.ts`:
- Around line 341-353: The version part of extractPlatformFromFilename only
allows single-digit components; update the filename regex in
extractPlatformFromFilename to accept multi-digit version components (use one or
more digits for each version segment and allow one or two dot-separated
segments), keep the optional prerelease suffix handling (e.g., -beta.1), and
preserve the platform capture and optional -nort suffix logic so filenames like
hagicode-0.10.0-beta.1-linux-x64-nort.zip and hagicode-1.2.10-windows-x64.zip
are matched and normalized (win -> windows).
In `@src/main/package-sources/local-folder-source.ts`:
- Around line 188-214: The three regexes in extractPlatformFromFilename (the
patterns used in the first match, the fallback-old-format match, and the final
no-arch match) currently constrain each version component to a single digit
([0-9]\.[0-9]\.[0-9]) and therefore fail for multi-digit versions; update each
pattern to allow multi-digit version components (e.g., replace
[0-9]\.[0-9]\.[0-9] with \d+\.\d+\.\d+ or [0-9]+\.[0-9]+\.[0-9]+), remembering
to escape backslashes appropriately in the TypeScript string literals, so all
three regex declarations inside extractPlatformFromFilename accept versions like
10.11.123.
- Around line 179-183: In extractVersionFromFilename, the version regex only
allows single-digit components; update the pattern used in filename.match(...)
to accept multi-digit semver components (e.g., use \d+ or [0-9]+ for each
numeric segment) while preserving the optional pre-release part and the platform
group (linux|windows|osx) and the .zip suffix so filenames like
hagicode-0.10.0-linux.zip and hagicode-12.0.0-windows.zip match and return the
captured version instead of 'unknown'.
In `@src/main/package-sources/package-source.ts`:
- Around line 36-39: The HttpIndexConfig interface currently only defines
indexUrl but StoredPackageSourceConfig (in package-source-config-manager.ts)
includes baseUrl and httpAuthToken; update HttpIndexConfig to include baseUrl?:
string and httpAuthToken?: string (or the same required/optional semantics as
StoredPackageSourceConfig) so runtime config matches stored config and no fields
are lost during conversion between StoredPackageSourceConfig and HttpIndexConfig
(ensure any conversion functions that create HttpIndexConfig from
StoredPackageSourceConfig also copy these fields).
In `@src/main/version-manager.ts`:
- Around line 65-79: The constructor currently calls initializePackageSource()
without awaiting it, causing a race on currentPackageSource for methods like
listVersions() and installVersion(); remove the async call from the constructor
and add a static async factory (e.g., VersionManager.create(dependencyManager,
packageSourceConfigManager)) that constructs the VersionManager, then awaits
instance.initializePackageSource() before returning the instance; update any
call sites to use VersionManager.create(...) instead of new VersionManager(...).
Ensure existing ensurePackageSource() stays as a fallback but the factory
guarantees initialization so listVersions(), installVersion(), and other methods
can rely on currentPackageSource being set.
- Around line 101-121: The storedToConfig function currently substitutes empty
strings for missing required fields which can hide errors; update storedToConfig
to validate required properties and throw a descriptive Error when a required
field is missing or empty (e.g., for 'local-folder' ensure stored.path is
defined and non-empty, for 'github-release' ensure stored.owner and stored.repo
are defined and non-empty while token can remain optional, and for 'http-index'
ensure stored.indexUrl is defined and non-empty), using trimmed checks to catch
whitespace-only values and include the offending type/name in the thrown message
to fail fast and avoid silent downstream failures.
In `@src/preload/index.ts`:
- Around line 54-70: The Window.electronAPI type is missing several grouped IPC
objects and listeners; update the central electron API declaration (the
Window.electronAPI interface) to include a packageSource object with the eight
methods getConfig, getAllConfigs, setConfig, switchSource, validateConfig,
scanFolder, fetchGithub, fetchHttpIndex and the onConfigChange(callback)
listener, a license object with get, save and onSyncStatus methods/listener, the
onVersionListChanged listener, and any missing manifest dependency APIs used
across the renderer (e.g., the methods referenced in webServiceSaga.ts); add
these symbols to the shared type declaration so all components import the same
complete electronAPI shape rather than declaring local/fragmented types.
In `@src/renderer/components/PackageSourceSelector.tsx`:
- Around line 102-118: Replace hard-coded Chinese strings used as UI labels in
PackageSourceSelector (e.g., the name property values for the local folder,
GitHub release and HTTP index branches and the Save button text referenced
around lines ~289-291) with translation keys or locale‑agnostic IDs, then render
them via the app's i18n function (or a localized label resolver) before display;
specifically update the objects created in the ternary branches where name:
'本地文件夹' / name: 'HagiCode Releases' / name: 'HTTP 索引源' to use translation keys
(e.g., t('packageSource.localFolder')) and change the Save button text to call
t('common.save') or the equivalent i18n helper used in this codebase so all
labels are localized at render time.
- Around line 56-63: The sync effect leaves a stale folder value because it only
calls dispatch(setFolderPath(...)) when currentConfig.path is truthy; update the
logic in the PackageSourceSelector sync to always set folder state for the
'local-folder' branch (e.g. dispatch(setFolderPath(currentConfig.path || ''))
inside the if (currentConfig.type === 'local-folder') block) and optionally
clear the folder state in the other branches (call dispatch(setFolderPath(''))
when type !== 'local-folder') so the input cannot retain a previous value.
- Around line 99-105: The ternary precedence in PackageSourceSelector.tsx causes
folderPath to be ignored: change the expression used for the path property so it
evaluates folderPath first and only falls back to the dev path when folderPath
is falsy; specifically update the path assignment (the object created when value
=== 'local-folder') to use a clear conditional like folderPath ? folderPath :
(process.env.NODE_ENV === 'development' ? '<devPath>' : '') so the path property
truly uses folderPath when present.
In `@src/renderer/i18n/locales/zh-CN/components.json`:
- Line 265: The translation entry "nortOnlyNotice" appears to contain a possible
typo in its value ("nort"); verify the intended term by checking the matching
key in other locale files and any UI usage of the key "nortOnlyNotice", then
correct the Chinese string to the accurate term and ensure the same key/value
spelling is used consistently across all locale files and in code references
(search for "nortOnlyNotice" to update any mismatches).
In `@src/renderer/store/sagas/licenseSaga.ts`:
- Around line 55-63: Replace the hard-coded mixed CN/EN toast messages in
licenseSaga.ts with i18n translation lookups: call the app's translation
function (e.g., i18n.t or the translator used elsewhere) for both the toast
title and description where toast.success and toast.error are invoked (including
the other occurrences noted at lines ~70-72), and also use translated text when
dispatching setError(result.error || '...') so fallback messages come from
translation keys rather than literal strings; update the
toast.success/toast.error calls and the setError fallback to use the appropriate
translation keys (e.g., 'license.updated.success' / 'license.updated.failure')
so messages follow the app i18n pattern.
In `@src/renderer/store/sagas/packageSourceSaga.ts`:
- Around line 32-48: The loading flag races because loadSourceConfig and
loadAllSourceConfigs both dispatch setLoading(true/false); change coordination
so initializePackageSourceSaga doesn't get a false from the first completer:
either (A) add a counter-based loading API (e.g., dispatch startLoading()
increments a reducer counter and finishLoading() decrements it, keep setLoading
usage replaced by startLoading()/finishLoading() inside loadSourceConfig and
loadAllSourceConfigs), or (B) add two distinct flags (e.g., setConfigLoading and
setAllConfigsLoading) and have loadSourceConfig call
setConfigLoading(true/false) and loadAllSourceConfigs call
setAllConfigsLoading(true/false); update the reducer to derive overall loading
if needed and update initializePackageSourceSaga to remove its own setLoading
toggles so loading remains true until both sagas finish. Ensure the change
references functions loadSourceConfig, loadAllSourceConfigs,
initializePackageSourceSaga, and the existing setLoading action so you update
all call sites consistently.
In `@src/renderer/store/slices/packageSourceSlice.ts`:
- Around line 52-54: The hardcoded developer-specific path assigned to
folderPath in packageSourceSlice (the ternary using process.env.NODE_ENV ===
'development') is duplicated elsewhere; extract that literal into a shared
constant or environment variable and import/use it in both packageSourceSlice
and package-source-config-manager instead of repeating the string. Create a
single exported symbol (e.g., RELEASE_PACKAGES_PATH or RELEASE_PACKAGES_DIR) in
a shared config/module and replace the inline string in the folderPath ternary
and the other occurrence so both modules read the same value (or read from
process.env.RELEASE_PACKAGES_DIR) to avoid duplication and maintenance drift.
- Line 3: Update the import so PackageSourceConfig is imported from the correct
module: replace the current combined import that pulls PackageSourceConfig from
package-source-config-manager with two imports — import type {
PackageSourceConfig } from '../../../main/package-sources/package-source' and
import type { StoredPackageSourceConfig } from
'../../../main/package-source-config-manager' — ensuring the symbols
PackageSourceConfig and StoredPackageSourceConfig are referenced from their
proper files (package-source.ts for PackageSourceConfig,
package-source-config-manager for StoredPackageSourceConfig).
🧹 Nitpick comments (40)
.github/workflows/sync-azure-storage.yml (1)
181-187: Good defensive validation; consider using English for comment consistency.The
jq emptyvalidation is a good safety check before uploading. However, the comment on line 181 is in Chinese while the rest of the workflow uses English comments.Suggested fix for comment consistency
- # 验证生成的 JSON 格式有效 + # Validate the generated JSON format echo "Validating index.json..."openspec/changes/archive/2026-02-08-license-management-feature/license-management-feature/proposal.md (1)
29-44: Consider adding language specifier to fenced code blocks.The ASCII art diagrams at lines 29-44 and 48-73 trigger markdownlint warnings (MD040). While these are visual diagrams rather than code, adding a
textorplaintextlanguage specifier would suppress the warnings and improve markdown consistency.📝 Example fix for line 29
-``` +```text ┌─────────────────────────────────────────┐openspec/changes/archive/2026-02-08-multi-source-package-management-enhancement/multi-source-package-management-enhancement/tasks.md (1)
206-211: Inconsistency: HTTP package source is mentioned as future work but appears to be implemented.Line 208 suggests "考虑添加 HTTP 包源类型作为未来的扩展" (consider adding HTTP package source as future extension), but based on the PR summary and related spec file (
spec.mdfor HTTP index package source), the HTTP index source is being implemented in this PR.Consider updating this section to reflect the current implementation status or remove it from the "技术债务和改进建议" list.
openspec/changes/archive/2026-02-08-multi-source-package-management-enhancement/multi-source-package-management-enhancement/design.md (3)
104-111: Replace developer-specific hardcoded path with a generic example.The default configuration example contains a developer-specific path (
/home/newbe36524/repos/...) which could confuse other developers or accidentally end up in production configuration. Consider using a placeholder or generic example path.📝 Suggested change
const defaultConfig: PackageSourceConfig = { type: 'local-folder', localFolderPath: process.env.NODE_ENV === 'development' - ? '/home/newbe36524/repos/newbe36524/pcode/Release/release-packages/' + ? '/path/to/your/local/release-packages/' // Configure via environment or settings : path.join(app.getPath('userData'), 'packages') };
633-644: Same hardcoded path appears in migration example.The migration code also references the developer-specific path. Update for consistency with the above suggestion.
📝 Suggested change
async function migrateConfig() { - const oldPath = '/home/newbe36524/repos/newbe36524/pcode/Release/release-packages/'; + const oldPath = process.env.OLD_PACKAGE_PATH || '/path/to/old/release-packages/'; const newConfig: PackageSourceConfig = { type: 'local-folder', localFolderPath: oldPath }; await configManager.setPackageSourceConfig(newConfig); }
119-152: Consider adding language specifier to ASCII art blocks.Similar to the proposal document, the UI mockups at lines 119-152, 156-200, and 302-348 use fenced code blocks without language specifiers, triggering MD040 warnings. Adding
textwould improve consistency.openspec/changes/remove-view-transition-animation/tasks.md (3)
9-122: Tasks should be marked as complete if implementation is done.Based on the commit history ("Remove view transition fade animation"), this feature appears to have been implemented. Per coding guidelines, all completed tasks should be updated to
- [x]after implementation work is confirmed.Please update the task checkboxes to reflect the actual implementation status.
As per coding guidelines: "Update every task in tasks.md to
- [x]after all implementation work is complete and confirmed"
102-111: Add language identifier to fenced code block.The commit message example block should specify a language for proper rendering and linting compliance.
📝 Proposed fix
- [ ] 创建提交: - ``` + ```bash git commit -m "feat: remove view transition fade animation
162-196: Add language identifier to ASCII diagram code block.Consider using
textorplaintextas the language identifier for the dependency diagram to satisfy markdown linting.📝 Proposed fix
-``` +```text ┌─────────────────┐ │ 1. 代码准备 │src/types/license.ts (1)
17-24: DuplicateLicenseSyncStatusinterface definition.This interface is also defined in
src/main/license-manager.ts(lines 12-19). To avoid drift and maintain DRY principles, consider having a single source of truth for this type and importing it where needed.♻️ Suggested approach
Either:
- Remove the duplicate from
src/main/license-manager.tsand import from this shared types file, or- Keep the definition in
src/main/license-manager.tsand import it here for re-exportOption 1 is recommended since
src/types/license.tsappears to be the canonical location for shared license types.src/renderer/store/index.ts (1)
13-14: Unused imports:initializePackageSourceSagaandinitializeLicenseSaga.These are imported but never used. The code dispatches raw action types instead (lines 60-61, 65).
Either remove the unused imports or use the imported action creators for consistency with other sagas (e.g.,
initializeI18nSagaon line 48).♻️ Option 1: Remove unused imports
-import { packageSourceSaga, initializePackageSourceSaga } from './sagas/packageSourceSaga'; -import { licenseSaga, initializeLicenseSaga } from './sagas/licenseSaga'; +import { packageSourceSaga } from './sagas/packageSourceSaga'; +import { licenseSaga } from './sagas/licenseSaga';src/renderer/components/FolderConfigForm.tsx (2)
7-18: Remove unused imports.
Select,SelectContent,SelectItem,SelectTrigger,SelectValue, andsetSelectedSourceTypeare imported but never used in this component.♻️ Proposed fix
import { Label } from './ui/label'; import { Input } from './ui/input'; import { Button } from './ui/button'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from './ui/select'; import type { RootState } from '../store'; import { setFolderPath, - setSelectedSourceType, } from '../store/slices/packageSourceSlice';
40-44: Consider surfacing the error to the user.The error is only logged to console. If the directory picker fails, the user receives no feedback. Consider dispatching an error action or using a toast notification.
src/main/package-sources/github-release-source.ts (1)
94-96: Unreachable code: axios throws on non-2xx status by default.This status check will never be reached because axios throws an error for non-2xx responses. The error handling in lines 157-185 correctly catches these cases.
♻️ Remove unreachable code
const response = await axios.get<GitHubRelease[]>( `https://api.github.com/repos/${this.config.owner}/${this.config.repo}/releases`, { headers, params: { // Get all releases including drafts and pre-releases per_page: 100, // Maximum per page } } ); - if (response.status !== 200) { - throw new Error(`GitHub API returned status ${response.status}`); - } - const releases = response.data;src/renderer/components/SourceConfigForm.tsx (2)
14-18: Unused import:setValidationError.This action creator is imported but never used in the component.
♻️ Proposed fix
import { setSelectedSourceType, - setValidationError, clearErrors, } from '../store/slices/packageSourceSlice';
73-73: Consider using theLabelcomponent for consistency.Other forms in this PR (FolderConfigForm, GitHubConfigForm) use the
Labelcomponent from./ui/label. Using a plain<label>here creates inconsistency.♻️ Proposed fix
+import { Label } from './ui/label'; ... <div className="space-y-2"> - <label className="text-sm font-medium">{t('packageSource.sourceType.label')}</label> + <Label>{t('packageSource.sourceType.label')}</Label>src/main/package-sources/index.ts (1)
11-24: Consider an exhaustive switch to avoidas anyin the default branch.
Tightening typing here improves safety when adding new source types.♻️ Example tightening
export function createPackageSource(config: PackageSourceConfig): PackageSource { switch (config.type) { case 'local-folder': return new LocalFolderPackageSource(config as LocalFolderConfig); @@ case 'http-index': return new HttpIndexPackageSource(config as HttpIndexConfig); default: - throw new Error(`Unknown package source type: ${(config as any).type}`); + // Exhaustiveness check for future types + const _exhaustive: never = config; + throw new Error(`Unknown package source type: ${(config as any).type}`); } }src/renderer/components/LicenseManagementPage.tsx (4)
59-70: Hard-coded Chinese strings should use i18n.The toast notification messages use hard-coded Chinese text while the rest of the component uses
useTranslation. This creates inconsistent localization behavior.♻️ Suggested refactor to use i18n
if (status.synced) { - const sourceText = status.source === 'default' ? '默认许可证' : - status.source === 'existing' ? '已有许可证' : '手动设置'; - const isDefaultText = status.isDefault ? '(默认公测许可证)' : ''; - const versionsText = status.syncedVersions !== undefined - ? `\n已同步到 ${status.syncedVersions} 个版本` - : ''; - - toast.success(`许可证已同步: ${sourceText}${isDefaultText}${versionsText}`, { - description: `时间: ${new Date(status.timestamp).toLocaleString('zh-CN')}`, + const sourceText = t(`licenseManagement.syncStatus.source.${status.source}`); + const isDefaultText = status.isDefault ? t('licenseManagement.syncStatus.defaultBeta') : ''; + const versionsText = status.syncedVersions !== undefined + ? t('licenseManagement.syncStatus.syncedVersions', { count: status.syncedVersions }) + : ''; + + toast.success(t('licenseManagement.syncStatus.synced', { source: sourceText, default: isDefaultText, versions: versionsText }), { + description: t('licenseManagement.syncStatus.timestamp', { time: new Date(status.timestamp).toLocaleString() }), });
80-86: Hard-coded error message in handleSaveLicense.Similar to the sync status toast, this error message is hard-coded in Chinese while the component uses i18n elsewhere.
♻️ Suggested fix
const handleSaveLicense = () => { if (!licenseKey.trim()) { - toast.error('许可证密钥不能为空', { - description: 'License key cannot be empty', - }); + toast.error(t('licenseManagement.errors.emptyLicenseKey')); return; }
45-78: useEffect has potential stale closure and re-subscription issue.The effect depends on
[license], which means every timelicensechanges, it will unsubscribe and re-subscribe toonSyncStatus. Additionally, Lines 47-51 setlicenseKeystate inside the effect that depends onlicense, which could cause unnecessary re-renders.Consider separating the initialization logic from the subscription logic.
♻️ Suggested refactor to separate concerns
+ // Initialize license key from current license or default + useEffect(() => { + if (license?.licenseKey) { + setLicenseKey(license.licenseKey); + } else { + setLicenseKey(DEFAULT_LICENSE_KEY); + } + }, [license?.licenseKey]); + + // Subscribe to license sync status changes (runs once on mount) useEffect(() => { - // Pre-fill the input with current license or default - if (license?.licenseKey) { - setLicenseKey(license.licenseKey); - } else { - setLicenseKey(DEFAULT_LICENSE_KEY); - } - - // Listen for license sync status changes const unsubscribe = window.electronAPI.license.onSyncStatus((status) => { console.log('[LicenseManagementPage] License sync status:', status); setSyncStatus(status); - - // Show toast notification for sync status - if (status.synced) { - // ... toast logic - } + // ... toast logic }); return () => { if (typeof unsubscribe === 'function') { unsubscribe(); } }; - }, [license]); + }, []);
156-199: Hard-coded Chinese strings in Sync Status Card UI.The sync status card contains multiple hard-coded Chinese strings (lines 162, 168, 170, 174, 176-179, 183, 190, 192). These should use i18n for consistency.
src/main/license-manager.ts (4)
13-20: Duplicate interface definition.
LicenseSyncStatusis defined here but also exists insrc/types/license.ts(lines 16-23). This duplication can lead to drift if one is updated without the other.♻️ Suggested fix to import instead of duplicate
import type { ConfigManager } from './config.js'; -import type { LicenseData } from '../types/license.js'; -import { DEFAULT_LICENSE_KEY } from '../types/license.js'; +import type { LicenseData, LicenseSyncStatus } from '../types/license.js'; +import { DEFAULT_LICENSE_KEY } from '../types/license.js'; import { PathManager } from './path-manager.js'; - -/** - * License sync status information - */ -export interface LicenseSyncStatus { - synced: boolean; - licenseKey?: string; - isDefault?: boolean; - source: 'existing' | 'default' | 'manual'; - timestamp: string; - syncedVersions?: number; // Number of versions that were synced -} + +// Re-export for consumers that import from this module +export type { LicenseSyncStatus } from '../types/license.js';
40-45: Singleton getInstance requires configManager on every call but ignores it after first instantiation.Once the singleton is created, subsequent calls to
getInstance(configManager)will ignore the providedconfigManagerargument. This could lead to confusion if differentConfigManagerinstances are passed.♻️ Consider adding a warning or assertion
static getInstance(configManager: ConfigManager): LicenseManager { if (!LicenseManager.instance) { LicenseManager.instance = new LicenseManager(configManager); + } else if (process.env.NODE_ENV === 'development') { + log.warn('[LicenseManager] getInstance called with configManager after instance already created - argument ignored'); } return LicenseManager.instance; }
106-120: saveLicense reports success even if sync to AppSettings partially fails.
syncToAppSettingscatches errors per-version and continues (line 279-281), returning only successfully updated versions. ThesaveLicensemethod then reportssuccess: trueregardless of how many versions were synced. Consider indicating partial failure if not all versions were updated.
236-244: Silent catch when reading YAML config is acceptable but consider logging at debug level.The empty catch block (lines 241-243) silently creates a new config if the file doesn't exist. While this fallback is appropriate, a debug-level log would aid troubleshooting.
♻️ Suggested improvement
try { const content = await fs.readFile(configPath, 'utf-8'); config = yaml.load(content) as Record<string, any> || {}; - } catch { + } catch (readError) { + log.debug(`[LicenseManager] Config file not found for ${versionId}, will create new one`); // Config doesn't exist, will create new one }src/main/package-sources/local-folder-source.ts (2)
99-119: Duplicate fs.stat calls for progress reporting.The code calls
fs.staton the source file before copy (line 101) and on the destination file after copy (line 113). The second stat is redundant since the copied file will have the same size. Store the size from the first call.♻️ Suggested optimization
+ let totalSize: number | undefined; + // Get file size for progress reporting if (onProgress) { const stats = await fs.stat(sourcePath); - const totalSize = stats.size; - + totalSize = stats.size; // Report progress in chunks for large files onProgress({ current: 0, total: totalSize, percentage: 0 }); } // Copy file to cache await fs.copyFile(sourcePath, cachePath); // Report completion - if (onProgress) { - const stats = await fs.stat(cachePath); + if (onProgress && totalSize !== undefined) { onProgress({ - current: stats.size, - total: stats.size, + current: totalSize, + total: totalSize, percentage: 100, }); }
252-288: Code duplication with HttpIndexPackageSource.
getCurrentPlatform()andcompareVersions()methods are identical to those inhttp-index-source.ts. Consider extracting these to a shared utility module.openspec/changes/archive/2026-02-08-custom-http-index-package-source/tasks.md (1)
81-101: Testing tasks remain incomplete.Sections 6.1, 6.2, and 6.3 are marked incomplete. If this PR is intended to be merged before tests are written, consider tracking these as follow-up work items or issues.
Would you like me to help create GitHub issues to track these testing tasks for follow-up?
openspec/changes/archive/2026-02-08-custom-http-index-package-source/proposal.md (1)
19-53: Add language specifiers to fenced code blocks.The ASCII art code blocks at lines 19-53 and 82-93 don't have a language specifier, triggering markdownlint warnings. Consider adding a specifier like
textorplaintext.♻️ Suggested fix
-``` +```text ┌─────────────────────────────────────────────────────────┐ │ 📦 包源管理 │Also applies to: 82-93
src/main/package-sources/http-index-source.ts (2)
189-204: Downloading entire file into memory before writing may cause issues with large packages.Using
responseType: 'arraybuffer'loads the complete file into memory. For large packages, this could exhaust memory. Consider using stream-based downloading.♻️ Suggested stream-based approach
+import { createWriteStream } from 'node:fs'; +import { pipeline } from 'node:stream/promises'; + async downloadPackage( version: Version, cachePath: string, onProgress?: DownloadProgressCallback ): Promise<void> { try { if (!version.downloadUrl) { throw new Error(`No download URL available for version: ${version.id}`); } log.info('[HttpIndexSource] Downloading package:', version.id); - // Download with progress tracking - const response = await axios.get<ArrayBuffer>(version.downloadUrl, { - responseType: 'arraybuffer', + // Download with streaming and progress tracking + const response = await axios.get(version.downloadUrl, { + responseType: 'stream', onDownloadProgress: (progressEvent) => { if (onProgress && progressEvent.total) { const current = progressEvent.loaded; const total = progressEvent.total; const percentage = Math.round((current / total) * 100); onProgress({ current, total, percentage }); } }, }); - // Write to cache file - const buffer = Buffer.from(response.data); - await fs.writeFile(cachePath, buffer); + // Stream to cache file + const writer = createWriteStream(cachePath); + await pipeline(response.data, writer);
358-394: Code duplication with LocalFolderPackageSource.
getCurrentPlatform()andcompareVersions()are duplicated fromlocal-folder-source.ts. Extract these to a shared utility module to improve maintainability.♻️ Suggested approach
Create a new file
src/main/package-sources/utils.ts:// src/main/package-sources/utils.ts export function getCurrentPlatform(): string { const currentPlatform = process.platform; switch (currentPlatform) { case 'win32': return 'windows'; case 'darwin': return 'osx'; case 'linux': return 'linux'; default: return 'unknown'; } } export function compareVersions(v1: string, v2: string): number { const parseVersion = (v: string) => { const parts = v.split('-')[0].split('.').map(Number); return parts; }; const p1 = parseVersion(v1); const p2 = parseVersion(v2); for (let i = 0; i < Math.max(p1.length, p2.length); i++) { const n1 = p1[i] || 0; const n2 = p2[i] || 0; if (n1 > n2) return 1; if (n1 < n2) return -1; } return 0; }Then import in both source files.
src/renderer/store/sagas/packageSourceSaga.ts (3)
27-27: Unused action type constant.
CLEAR_VERSIONSis defined but has no corresponding watcher saga. Either add a watcher for it or remove the unused constant to reduce dead code.
92-94: Inconsistent toast message languages.Toast titles are in Chinese while descriptions are in English. This creates an inconsistent user experience. Consider using i18n keys for both title and description to maintain language consistency.
- toast.success('包源配置已保存', { - description: 'Package source configuration saved successfully', + toast.success(t('packageSource.saveSuccess'), { + description: t('packageSource.saveSuccessDesc'), });Also applies to: 98-100, 107-109, 136-138, 142-144, 151-153
15-16: Cross-boundary type imports from main process.Importing types directly from
../../../main/version-managerand../../../main/package-source-config-managercreates coupling between renderer and main process code. While this works in Electron, consider creating shared type definitions in a common location (e.g.,src/shared/types/) to improve maintainability.src/main/version-manager.ts (1)
163-171: Repeated unsafe type casts.Multiple
(config as any)casts are used to access type-specific properties. This bypasses TypeScript's type checking and could lead to runtime errors if the config type doesn't match.Use type guards or discriminated union narrowing instead:
♻️ Suggested improvement
- if (config.type === 'local-folder') { - updates.path = (config as any).path; - } else if (config.type === 'github-release') { - updates.owner = (config as any).owner; + if (config.type === 'local-folder') { + updates.path = (config as LocalFolderConfig).path; + } else if (config.type === 'github-release') { + const ghConfig = config as GitHubReleaseConfig; + updates.owner = ghConfig.owner; + updates.repo = ghConfig.repo; + updates.token = ghConfig.token;src/main/package-source-config-manager.ts (2)
300-322: Default source methods are not idempotent.
getDefaultGitHubSource()andgetDefaultHttpIndexSource()create a newcreatedAttimestamp on each call. If these are used to compare or deduplicate sources, the varying timestamps will cause issues.Consider using a fixed timestamp or lazy initialization:
♻️ Suggested fix
+ private static readonly DEFAULT_CREATED_AT = '1970-01-01T00:00:00.000Z'; + getDefaultGitHubSource(): StoredPackageSourceConfig { return { id: 'github-default', type: 'github-release', name: 'HagiCode Releases', owner: 'HagiCode-org', repo: 'releases', - createdAt: new Date().toISOString(), + createdAt: PackageSourceConfigManager.DEFAULT_CREATED_AT, }; }
46-49: Unsafe double type assertion.The
as unknown as Store<PackageSourceStoreSchema>cast bypasses type checking entirely. If the provided store has an incompatible schema, this will cause runtime errors.Consider adding runtime validation or using a type guard:
if (store) { - this.store = store as unknown as Store<PackageSourceStoreSchema>; + // Validate store has expected structure + this.store = store as Store<PackageSourceStoreSchema>; + // Ensure defaults exist + if (!this.store.has('sources')) { + this.store.set('sources', []); + } }src/renderer/store/slices/packageSourceSlice.ts (1)
8-40: State slice mixes domain and UI concerns.The state combines domain data (
currentConfig,availableVersions) with UI form state (folderPath,githubOwner,githubToken, etc.). Consider separating these into distinct slices or namespaced sub-states for better maintainability and testability.src/main/main.ts (1)
35-44: Duplicate preload path resolution logic.The same preload path calculation appears twice (lines 35-44 and 130-135). Extract to a helper function to maintain DRY principle:
♻️ Suggested refactor
+function getPreloadPath(): string { + const isDev = process.env.NODE_ENV === 'development'; + return isDev + ? path.join(__dirname, '../../dist/preload/index.mjs') + : path.join(__dirname, 'preload/index.mjs'); +} + function createWindow(): void { console.log('[Hagicode] Creating window...'); - - const isDev = process.env.NODE_ENV === 'development'; - const preloadPath = isDev - ? path.join(__dirname, '../../dist/preload/index.mjs') - : path.join(__dirname, 'preload/index.mjs'); + const preloadPath = getPreloadPath();Also applies to: 130-135
src/renderer/i18n/locales/en-US/components.json (1)
265-265: Unclear translation key.The key
nortOnlyNoticewith value "Only nort packages (hagicode-*) are supported" uses the term "nort" which may be unclear to users. If "nort" is an internal project name or acronym, consider using a more descriptive term or adding clarification.
| setLicense(licenseKey: string): void { | ||
| const current = this.getLicense(); | ||
| this.set('license', { | ||
| licenseKey, | ||
| isConfigured: !!licenseKey, | ||
| updatedAt: new Date().toISOString(), | ||
| }); |
There was a problem hiding this comment.
Remove unused variable current.
The current variable is assigned but never used in the setLicense method. This appears to be leftover from a potential merge operation that was not implemented.
🧹 Proposed fix
setLicense(licenseKey: string): void {
- const current = this.getLicense();
this.set('license', {
licenseKey,
isConfigured: !!licenseKey,
updatedAt: new Date().toISOString(),
});
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| setLicense(licenseKey: string): void { | |
| const current = this.getLicense(); | |
| this.set('license', { | |
| licenseKey, | |
| isConfigured: !!licenseKey, | |
| updatedAt: new Date().toISOString(), | |
| }); | |
| setLicense(licenseKey: string): void { | |
| this.set('license', { | |
| licenseKey, | |
| isConfigured: !!licenseKey, | |
| updatedAt: new Date().toISOString(), | |
| }); | |
| } |
🤖 Prompt for AI Agents
In `@src/main/config.ts` around lines 94 - 100, In setLicense(licenseKey: string)
remove the unused local variable current (the assignment from this.getLicense())
since it’s not referenced; update the setLicense method to simply call
this.set('license', { licenseKey, isConfigured: !!licenseKey, updatedAt: new
Date().toISOString() }) without declaring current so no unused variable remains.
| // Initialize Package Source Configuration Manager | ||
| packageSourceConfigManager = new PackageSourceConfigManager(configManager.getStore() as unknown as Store); | ||
|
|
||
| // Initialize Version Manager with package source config manager | ||
| versionManager = new VersionManager(dependencyManager, packageSourceConfigManager); |
There was a problem hiding this comment.
Initialization order may cause race conditions.
VersionManager is constructed synchronously but its internal initializePackageSource() is async. The immediately following code that uses versionManager (like the IIFE at line 1466) could encounter an uninitialized package source.
This ties back to the constructor issue in version-manager.ts. Consider awaiting initialization:
♻️ Suggested approach
- versionManager = new VersionManager(dependencyManager, packageSourceConfigManager);
+ versionManager = new VersionManager(dependencyManager, packageSourceConfigManager);
+ // Ensure package source is initialized before using versionManager
+ await versionManager.ensureInitialized(); // Add this method🤖 Prompt for AI Agents
In `@src/main/main.ts` around lines 1444 - 1448, The VersionManager is constructed
synchronously but kicks off async work in initializePackageSource(), causing
callers (e.g., the immediate IIFE that uses versionManager) to race with
initialization; change the pattern so construction is synchronous-only and
expose an explicit async initializer (e.g.,
VersionManager.initializePackageSource or a static async factory like
VersionManager.create(...)) and then await that initializer right after creating
packageSourceConfigManager and before any use of versionManager (replace the
current new VersionManager(...) call with await VersionManager.create(...) or
call await versionManager.initializePackageSource()). Ensure
initializePackageSource() is moved out of the constructor and returns a Promise
so callers can reliably await completion.
| const defaultSource = process.env.NODE_ENV === 'development' | ||
| ? this.addSource({ | ||
| type: 'local-folder', | ||
| name: '本地文件夹', | ||
| path: '/home/newbe36524/repos/newbe36524/pcode/Release/release-packages/', | ||
| }) |
There was a problem hiding this comment.
Hardcoded developer-specific path should be configurable.
The hardcoded path /home/newbe36524/repos/newbe36524/pcode/Release/release-packages/ is specific to one developer's machine and will fail on other development environments.
Consider using an environment variable:
♻️ Suggested fix
const defaultSource = process.env.NODE_ENV === 'development'
? this.addSource({
type: 'local-folder',
name: '本地文件夹',
- path: '/home/newbe36524/repos/newbe36524/pcode/Release/release-packages/',
+ path: process.env.HAGICODE_DEV_PACKAGES_PATH || path.join(app.getPath('userData'), 'dev-packages'),
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const defaultSource = process.env.NODE_ENV === 'development' | |
| ? this.addSource({ | |
| type: 'local-folder', | |
| name: '本地文件夹', | |
| path: '/home/newbe36524/repos/newbe36524/pcode/Release/release-packages/', | |
| }) | |
| const defaultSource = process.env.NODE_ENV === 'development' | |
| ? this.addSource({ | |
| type: 'local-folder', | |
| name: '本地文件夹', | |
| path: process.env.HAGICODE_DEV_PACKAGES_PATH || path.join(app.getPath('userData'), 'dev-packages'), | |
| }) |
🤖 Prompt for AI Agents
In `@src/main/package-source-config-manager.ts` around lines 278 - 283, The code
in the addSource call inside package-source-config-manager.ts hardcodes a
developer-specific path for defaultSource; update that call (the defaultSource
initialization that invokes this.addSource) to read the path from an
environment/config variable (e.g. process.env.DEFAULT_LOCAL_PACKAGE_PATH) with a
sensible fallback (or omit fallback and fail-fast) instead of using
"/home/newbe36524/…"; ensure you only change the path property passed to
this.addSource and keep the same type/name values so defaultSource creation
logic remains intact.
| private extractVersionFromFilename(filename: string): string { | ||
| // Try to extract from tag name (remove 'v' prefix if present) | ||
| // Format: hagicode-{version}-{platform}.zip | ||
| const match = filename.match(/^hagicode-([0-9]\.[0-9]\.[0-9](?:-[a-zA-Z0-9\.]+)?)-(linux|windows|osx)\.zip$/); | ||
| return match ? match[1] : 'unknown'; | ||
| } |
There was a problem hiding this comment.
Version regex only matches single-digit version components.
The pattern [0-9]\.[0-9]\.[0-9] only matches versions like 1.2.3 but not 10.20.30. Use [0-9]+ for multi-digit support.
🐛 Proposed fix
private extractVersionFromFilename(filename: string): string {
- const match = filename.match(/^hagicode-([0-9]\.[0-9]\.[0-9](?:-[a-zA-Z0-9\.]+)?)-(linux|windows|osx)\.zip$/);
+ const match = filename.match(/^hagicode-([0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9\.]+)?)-(linux|windows|osx)\.zip$/);
return match ? match[1] : 'unknown';
}
private extractPlatformFromFilename(filename: string): string {
- let match = filename.match(/^hagicode-([0-9]\.[0-9]\.[0-9](?:-[a-zA-Z0-9\.]+)?)-(linux|windows|osx)\.zip$/);
+ let match = filename.match(/^hagicode-([0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9\.]+)?)-(linux|windows|osx)\.zip$/);
if (match) {
return match[2];
}
- match = filename.match(/^hagicode-([0-9]\.[0-9]\.[0-9](?:-[a-zA-Z0-9\.]+)?)-([a-zA-Z]+)-x64\.zip$/);
+ match = filename.match(/^hagicode-([0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9\.]+)?)-([a-zA-Z]+)-x64\.zip$/);Also applies to: 339-344
🤖 Prompt for AI Agents
In `@src/main/package-sources/github-release-source.ts` around lines 329 - 334,
The regex in extractVersionFromFilename only allows single-digit version parts,
so update the pattern(s) to use [0-9]+ for each numeric component (e.g., change
[0-9]\.[0-9]\.[0-9] to [0-9]+(?:\.[0-9]+){2} or similar) to support multi-digit
segments and keep the rest of the capture group intact; apply the same change to
the other occurrence around lines 339-344 so both extractors accept versions
like 10.20.30 while preserving the existing optional prerelease capture and
platform suffix handling in extractVersionFromFilename.
| private compareVersions(v1: string, v2: string): number { | ||
| const parseVersion = (v: string) => { | ||
| const parts = v.split('-')[0].split('.').map(Number); | ||
| return parts; | ||
| }; | ||
|
|
||
| const p1 = parseVersion(v1); | ||
| const p2 = parseVersion(v2); | ||
|
|
||
| for (let i = 0; i < Math.max(p1.length, p2.length); i++) { | ||
| const n1 = p1[i] || 0; | ||
| const n2 = p2[i] || 0; | ||
|
|
||
| if (n1 > n2) return 1; | ||
| if (n1 < n2) return -1; | ||
| } | ||
|
|
||
| return 0; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if the file exists and examine the compareVersions method
if [ -f "src/main/package-sources/github-release-source.ts" ]; then
echo "=== File found, checking lines 388-406 ==="
sed -n '388,406p' "src/main/package-sources/github-release-source.ts"
else
echo "File not found at expected path"
find . -name "github-release-source.ts" -type f 2>/dev/null | head -5
fiRepository: HagiCode-org/desktop
Length of output: 569
🏁 Script executed:
#!/bin/bash
# Check package.json for semver dependency
if [ -f "package.json" ]; then
echo "=== Checking package.json for semver dependency ==="
cat package.json | jq '.dependencies.semver // .devDependencies.semver // "Not found"'
fiRepository: HagiCode-org/desktop
Length of output: 124
🌐 Web query:
latest version of semver npm package
💡 Result:
The latest published version of the semver npm package is 7.7.3. [1][2]
Use semver library for accurate prerelease version comparison.
compareVersions strips prerelease suffixes with split('-')[0], causing 1.0.0-alpha.1 and 1.0.0-alpha.2 to be treated as equal and sort inconsistently. Since semver is already a dependency, replace the implementation with semver.compare(v1, v2):
🛠️ Suggested fix
private compareVersions(v1: string, v2: string): number {
- const parseVersion = (v: string) => {
- const parts = v.split('-')[0].split('.').map(Number);
- return parts;
- };
-
- const p1 = parseVersion(v1);
- const p2 = parseVersion(v2);
-
- for (let i = 0; i < Math.max(p1.length, p2.length); i++) {
- const n1 = p1[i] || 0;
- const n2 = p2[i] || 0;
-
- if (n1 > n2) return 1;
- if (n1 < n2) return -1;
- }
-
- return 0;
+ return semver.compare(v1, v2);
}Ensure semver is imported at the top of the file.
🤖 Prompt for AI Agents
In `@src/main/package-sources/github-release-source.ts` around lines 388 - 406,
The current compareVersions implementation in compareVersions(v1: string, v2:
string) strips prerelease info and misorders semver pre-releases; replace its
body to use the semver library by calling semver.compare(v1, v2) (ensure semver
is imported at the top of the file), removing the manual parseVersion logic so
prerelease and build metadata are handled correctly.
| "valid": "配置有效", | ||
| "fetchingVersions": "获取版本中...", | ||
| "nortOnlyNotice": "仅支持 nort 包(hagicode-*)" | ||
| "nortOnlyNotice": "仅支持 nort 包(hagicode-*)", |
There was a problem hiding this comment.
Possible typo in nortOnlyNotice key/value.
If the intended term isn’t “nort,” this may cause mismatches across locales or confusing UI text. Please confirm spelling and alignment with other locale keys.
🤖 Prompt for AI Agents
In `@src/renderer/i18n/locales/zh-CN/components.json` at line 265, The translation
entry "nortOnlyNotice" appears to contain a possible typo in its value ("nort");
verify the intended term by checking the matching key in other locale files and
any UI usage of the key "nortOnlyNotice", then correct the Chinese string to the
accurate term and ensure the same key/value spelling is used consistently across
all locale files and in code references (search for "nortOnlyNotice" to update
any mismatches).
| toast.success('许可证已更新', { | ||
| description: 'License updated successfully', | ||
| }); | ||
| } else { | ||
| yield put(setError(result.error || 'Failed to save license')); | ||
|
|
||
| toast.error('许可证更新失败', { | ||
| description: result.error || 'Failed to save license', | ||
| }); |
There was a problem hiding this comment.
Localize toast strings instead of hard‑coded CN/EN mix.
These messages bypass i18n and will show Chinese even in English UI. Please route through translation keys.
Also applies to: 70-72
🤖 Prompt for AI Agents
In `@src/renderer/store/sagas/licenseSaga.ts` around lines 55 - 63, Replace the
hard-coded mixed CN/EN toast messages in licenseSaga.ts with i18n translation
lookups: call the app's translation function (e.g., i18n.t or the translator
used elsewhere) for both the toast title and description where toast.success and
toast.error are invoked (including the other occurrences noted at lines ~70-72),
and also use translated text when dispatching setError(result.error || '...') so
fallback messages come from translation keys rather than literal strings; update
the toast.success/toast.error calls and the setError fallback to use the
appropriate translation keys (e.g., 'license.updated.success' /
'license.updated.failure') so messages follow the app i18n pattern.
| function* loadSourceConfig() { | ||
| try { | ||
| yield put(setLoading(true)); | ||
|
|
||
| const config: StoredPackageSourceConfig | null = yield call( | ||
| window.electronAPI.packageSource.getConfig | ||
| ); | ||
|
|
||
| yield put(setCurrentConfig(config)); | ||
| } catch (error) { | ||
| yield put( | ||
| setError(error instanceof Error ? error.message : 'Failed to load package source configuration') | ||
| ); | ||
| } finally { | ||
| yield put(setLoading(false)); | ||
| } | ||
| } |
There was a problem hiding this comment.
Potential loading state race condition.
Both loadSourceConfig and loadAllSourceConfigs independently set loading to true/false. When called concurrently in initializePackageSourceSaga (lines 393-397), the first saga to complete will set loading: false while the other is still in progress.
Consider using separate loading flags or a counter-based approach:
♻️ Suggested fix using distinct flags
- yield put(setLoading(true));
+ // Use a dedicated flag or track concurrent operations
+ yield put(setLoadingConfig(true));
// ... saga logic ...
- yield put(setLoading(false));
+ yield put(setLoadingConfig(false));Alternatively, keep loading true until both complete by restructuring initializePackageSourceSaga.
Also applies to: 53-69
🤖 Prompt for AI Agents
In `@src/renderer/store/sagas/packageSourceSaga.ts` around lines 32 - 48, The
loading flag races because loadSourceConfig and loadAllSourceConfigs both
dispatch setLoading(true/false); change coordination so
initializePackageSourceSaga doesn't get a false from the first completer: either
(A) add a counter-based loading API (e.g., dispatch startLoading() increments a
reducer counter and finishLoading() decrements it, keep setLoading usage
replaced by startLoading()/finishLoading() inside loadSourceConfig and
loadAllSourceConfigs), or (B) add two distinct flags (e.g., setConfigLoading and
setAllConfigsLoading) and have loadSourceConfig call
setConfigLoading(true/false) and loadAllSourceConfigs call
setAllConfigsLoading(true/false); update the reducer to derive overall loading
if needed and update initializePackageSourceSaga to remove its own setLoading
toggles so loading remains true until both sagas finish. Ensure the change
references functions loadSourceConfig, loadAllSourceConfigs,
initializePackageSourceSaga, and the existing setLoading action so you update
all call sites consistently.
| @@ -0,0 +1,223 @@ | |||
| import { createSlice, PayloadAction } from '@reduxjs/toolkit'; | |||
| import type { Version } from '../../../main/version-manager'; | |||
| import type { PackageSourceConfig, StoredPackageSourceConfig } from '../../../main/package-source-config-manager'; | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check where PackageSourceConfig is actually exported from
rg -n "export.*PackageSourceConfig" --type=tsRepository: HagiCode-org/desktop
Length of output: 776
🏁 Script executed:
#!/bin/bash
# Check the contents of package-source-config-manager.ts to see all imports/exports
cat -n src/main/package-source-config-manager.tsRepository: HagiCode-org/desktop
Length of output: 12461
🏁 Script executed:
#!/bin/bash
# Check the actual import statement in packageSourceSlice.ts
head -20 src/renderer/store/slices/packageSourceSlice.tsRepository: HagiCode-org/desktop
Length of output: 666
Fix incorrect import path for PackageSourceConfig.
PackageSourceConfig is exported from src/main/package-sources/package-source.ts, not package-source-config-manager.ts. Update the import to:
import type { PackageSourceConfig } from '../../../main/package-sources/package-source';
import type { StoredPackageSourceConfig } from '../../../main/package-source-config-manager';The current import statement will fail at compile time because package-source-config-manager.ts only exports StoredPackageSourceConfig and PackageSourceConfigManager.
🤖 Prompt for AI Agents
In `@src/renderer/store/slices/packageSourceSlice.ts` at line 3, Update the import
so PackageSourceConfig is imported from the correct module: replace the current
combined import that pulls PackageSourceConfig from
package-source-config-manager with two imports — import type {
PackageSourceConfig } from '../../../main/package-sources/package-source' and
import type { StoredPackageSourceConfig } from
'../../../main/package-source-config-manager' — ensuring the symbols
PackageSourceConfig and StoredPackageSourceConfig are referenced from their
proper files (package-source.ts for PackageSourceConfig,
package-source-config-manager for StoredPackageSourceConfig).
| folderPath: process.env.NODE_ENV === 'development' | ||
| ? '/home/newbe36524/repos/newbe36524/pcode/Release/release-packages/' | ||
| : '', |
There was a problem hiding this comment.
Duplicate hardcoded developer-specific path.
This is the same hardcoded path as in package-source-config-manager.ts (line 282). Having this path in two places increases maintenance burden and will cause inconsistencies.
Consider extracting to a shared constant or using environment variables consistently.
🤖 Prompt for AI Agents
In `@src/renderer/store/slices/packageSourceSlice.ts` around lines 52 - 54, The
hardcoded developer-specific path assigned to folderPath in packageSourceSlice
(the ternary using process.env.NODE_ENV === 'development') is duplicated
elsewhere; extract that literal into a shared constant or environment variable
and import/use it in both packageSourceSlice and package-source-config-manager
instead of repeating the string. Create a single exported symbol (e.g.,
RELEASE_PACKAGES_PATH or RELEASE_PACKAGES_DIR) in a shared config/module and
replace the inline string in the folderPath ternary and the other occurrence so
both modules read the same value (or read from process.env.RELEASE_PACKAGES_DIR)
to avoid duplication and maintenance drift.
- Add jq -c parameter to generate compressed JSON format - Add JSON validation step to ensure output validity - Expected 20-40% file size reduction Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
d7ba661 to
6fd956a
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@openspec/changes/archive/2026-02-08-compress-client-index-json/proposal.md`:
- Line 88: Update the incorrect jq manual link referenced as "jq 手册 - compact
output" to use the current domain and correct anchor: replace the URL
"https://stedolan.github.io/jq/manual/#v4" with
"https://jqlang.org/manual/v1.5/#invoking-jq" so the `-c` / `--compact-output`
option points to the "Invoking jq" section on jqlang.org.
In
`@openspec/changes/archive/2026-02-08-remove-view-transition-animation/tasks.md`:
- Around line 1-200: Update the task checklist to reflect the completed
implementation: mark each step in the tasks list (1. 代码准备, 2. 修改 App.tsx 移除视图动画,
3. 清理未使用的导入, 4. 本地功能测试, 5. 代码审查与优化, 6. 提交变更, 7. 合并与清理) as completed (- [x]) and
tick off all acceptance criteria (功能完整性, 视觉一致性, 代码质量, 性能) after confirming the
changes described in the PR commit ("Remove view transition fade animation" —
removal of AnimatePresence and motion.div and replacement with a standard div
preserving sidebar margin transition); ensure the note about keeping
project-level dependency on motion/react remains and update the overall status
and estimated work/time fields accordingly.
🧹 Nitpick comments (5)
openspec/changes/archive/2026-02-08-remove-view-transition-animation/proposal.md (2)
1-60: Add UI mockups and user interaction flow diagrams.This proposal describes a UI change (removing view transition animations), but lacks visual design documentation. Based on learnings, proposal.md files for UI changes should include:
- UI mockups showing before/after states
- Mermaid sequence diagrams illustrating user interaction flows
- Reference to PROPOSAL_DESIGN_GUIDELINES.md
Consider adding:
- A simple before/after comparison (even text-based timeline showing animation vs. instant transition)
- A Mermaid sequence diagram showing: User clicks sidebar → View switch → Content display
- A reference section pointing to PROPOSAL_DESIGN_GUIDELINES.md
📊 Example Mermaid diagram structure
Add after line 60 in the "解决方案" section:
### 用户交互流程 ```mermaid sequenceDiagram participant User participant Sidebar participant App participant View User->>Sidebar: 点击导航项 Sidebar->>App: 触发视图切换 Note over App: 无动画延迟 App->>View: 立即渲染新视图 View-->>User: 内容即时显示```
设计参考
详细的 UI 设计指南请参考 PROPOSAL_DESIGN_GUIDELINES.md
</details> Based on learnings: Structure proposal.md with Why, What Changes, UI Design Changes (if applicable), Code Flow Changes (if applicable), and Impact sections; Include UI mockups, Mermaid sequence diagrams for user interaction flows, and reference PROPOSAL_DESIGN_GUIDELINES.md in proposal.md for UI changes. --- `102-107`: **Fix code block formatting.** The fenced code block on line 102 is missing a language specifier, causing markdown linter warnings. Since this appears to be a bulleted list rather than code, consider using standard markdown list formatting instead. <details> <summary>📝 Proposed fix</summary> ```diff ### 依赖清理 `motion/react` 导入在移除视图动画后可能仍然被 `SidebarNavigation.tsx` 使用(该组件使用了大量 motion 动画)。需要检查是否仍有其他组件使用该库: -- -- `SidebarNavigation.tsx` - 仍使用 `motion` 和 `AnimatePresence`(用于侧边栏折叠动画、导航项动画等) -- +- `SidebarNavigation.tsx` - 仍使用 `motion` 和 `AnimatePresence`(用于侧边栏折叠动画、导航项动画等) **结论**:不应从 `App.tsx` 移除 `motion/react` 导入,因为该项目仍在其他组件中使用。.github/workflows/sync-azure-storage.yml (1)
179-187: Compression and validation logic looks good.The use of
jq -c .for compact JSON output andjq emptyfor validation are both correct approaches. The error handling properly uses GitHub Actions error annotations and exits with code 1 on failure.One minor inconsistency: Line 181 has a Chinese comment ("验证生成的 JSON 格式有效") while all other comments in the workflow are in English. Consider using English for consistency:
🔧 Suggested fix for comment consistency
- # 验证生成的 JSON 格式有效 + # Validate the generated JSON format echo "Validating index.json..."openspec/changes/archive/2026-02-08-compress-client-index-json/tasks.md (2)
22-24: Update completed task checkboxes.Based on learnings, tasks should be marked
[x]as they are completed. Since the PR is already open with the workflow changes implemented, tasks 1 through 8 (at minimum) appear to be complete and their checkboxes should be updated to reflect progress.For example, Task 1's verification items should be checked:
**验证**: -- [ ] 修改后的命令语法正确 -- [ ] YAML 缩进保持一致 +- [x] 修改后的命令语法正确 +- [x] YAML 缩进保持一致
58-66: Minor inconsistency in validation message.The success message in Task 3 (line 65) shows
"JSON validation passed", but Task 4 (line 87) and the actual workflow implementation use"index.json validation passed". Consider aligning these for consistency.🔧 Suggested fix
if ! jq empty index.json; then echo "::error::Generated index.json is not valid JSON" exit 1 fi -echo "JSON validation passed" +echo "index.json validation passed"
|
|
||
| ## 参考资料 | ||
|
|
||
| - [jq 手册 - compact output](https://stedolan.github.io/jq/manual/#v4) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
jq manual compact output -c option anchor link
💡 Result:
jq’s compact output option is -c / --compact-output (“put each JSON object on a single line”, i.e., no pretty-print whitespace). [1]
A stable section link in the official manual where -c is documented (under Invoking jq) is:
https://jqlang.org/manual/v1.5/#invoking-jq
[1]
(There doesn’t appear to be a dedicated per-option anchor for --compact-output on that page; it’s listed as a bullet within the “Invoking jq” section.) [1]
Example:
jq -c '.'[2]
Sources: jq manual v1.5 [1], jq(1) man page (Arch Linux) [2].
Fix the jq manual link anchor and domain.
The link uses an incorrect anchor (#v4) that does not exist in the jq manual. The correct reference should point to https://jqlang.org/manual/v1.5/#invoking-jq, where the -c / --compact-output option is documented under the "Invoking jq" section. Additionally, the domain appears to have moved from stedolan.github.io to jqlang.org.
🤖 Prompt for AI Agents
In `@openspec/changes/archive/2026-02-08-compress-client-index-json/proposal.md`
at line 88, Update the incorrect jq manual link referenced as "jq 手册 - compact
output" to use the current domain and correct anchor: replace the URL
"https://stedolan.github.io/jq/manual/#v4" with
"https://jqlang.org/manual/v1.5/#invoking-jq" so the `-c` / `--compact-output`
option points to the "Invoking jq" section on jqlang.org.
| # 实施任务清单 | ||
|
|
||
| 移除视图切换淡入淡出动画的有序任务列表。 | ||
|
|
||
| --- | ||
|
|
||
| ## 任务列表 | ||
|
|
||
| ### 1. 代码准备 | ||
| **状态:** `pending` | **预计工作量:** 5 分钟 | ||
|
|
||
| - [ ] 创建功能分支:`git checkout -b feature/remove-view-transition-animation` | ||
| - [ ] 验证当前分支:确保在 `feature/multiplerelease` 基础上创建 | ||
|
|
||
| --- | ||
|
|
||
| ### 2. 修改 App.tsx 移除视图动画 | ||
| **状态:** `pending` | **预计工作量:** 10 分钟 | ||
| **文件:** `src/renderer/App.tsx` | ||
|
|
||
| - [ ] 定位主内容区域的 `AnimatePresence` 和 `motion.div` 组件(约第 73-91 行) | ||
| - [ ] 移除 `AnimatePresence` 包装标签 | ||
| - [ ] 将 `motion.div` 替换为普通 `div` 元素 | ||
| - [ ] 删除以下动画属性: | ||
| - `key={currentView}` | ||
| - `initial={{ opacity: 0, y: 20, scale: 0.98 }}` | ||
| - `animate={{ opacity: 1, y: 0, scale: 1 }}` | ||
| - `exit={{ opacity: 0, y: -20, scale: 0.98 }}` | ||
| - `transition={{ duration: 0.4, ease: [0.25, 0.1, 0.25, 1] }}` | ||
| - [ ] 保留容器样式类:`className="container mx-auto px-4 py-8 min-h-screen"` | ||
| - [ ] 保留外层 div 的边距过渡:`className="ml-64 transition-all duration-500 ease-out"` | ||
|
|
||
| **预期变更:** | ||
| ```tsx | ||
| <!-- 之前 --> | ||
| <AnimatePresence mode="wait"> | ||
| <motion.div | ||
| key={currentView} | ||
| initial={{ opacity: 0, y: 20, scale: 0.98 }} | ||
| animate={{ opacity: 1, y: 0, scale: 1 }} | ||
| exit={{ opacity: 0, y: -20, scale: 0.98 }} | ||
| transition={{ duration: 0.4, ease: [0.25, 0.1, 0.25, 1] }} | ||
| className="container mx-auto px-4 py-8 min-h-screen" | ||
| > | ||
| {currentView === 'system' && <SystemManagementView />} | ||
| ... | ||
| </motion.div> | ||
| </AnimatePresence> | ||
|
|
||
| <!-- 之后 --> | ||
| <div className="container mx-auto px-4 py-8 min-h-screen"> | ||
| {currentView === 'system' && <SystemManagementView />} | ||
| ... | ||
| </div> | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ### 3. 清理未使用的导入(可选) | ||
| **状态:** `pending` | **预计工作量:** 5 分钟 | ||
| **文件:** `src/renderer/App.tsx` | ||
|
|
||
| - [ ] 检查 `AnimatePresence` 和 `motion` 是否在文件中还有其他用途 | ||
| - [ ] 如无其他用途,从导入语句中移除: | ||
| ```diff | ||
| - import { AnimatePresence, motion } from 'motion/react'; | ||
| + import { AnimatePresence, motion } from 'motion/react'; // 暂时保留,SidebarNavigation 仍在使用 | ||
| ``` | ||
| - [ ] **注意**:由于 `SidebarNavigation.tsx` 仍使用 `motion/react`,项目级别不应移除依赖,仅清理当前文件的导入即可 | ||
|
|
||
| --- | ||
|
|
||
| ### 4. 本地功能测试 | ||
| **状态:** `pending` | **预计工作量:** 15 分钟 | ||
|
|
||
| - [ ] 启动开发服务器:`npm run dev` 或 `pnpm dev` | ||
| - [ ] 测试所有视图切换: | ||
| - [ ] System Management → Version Management | ||
| - [ ] Version Management → License Management | ||
| - [ ] License Management → System Management | ||
| - [ ] 所有其他视图组合 | ||
| - [ ] 验证视图内容立即显示(无淡入淡出延迟) | ||
| - [ ] 验证无视觉闪烁或布局跳动 | ||
| - [ ] 验证侧边栏折叠/展开时的边距过渡仍然正常 | ||
|
|
||
| --- | ||
|
|
||
| ### 5. 代码审查与优化 | ||
| **状态:** `pending` | **预计工作量:** 5 分钟 | ||
|
|
||
| - [ ] 检查代码格式是否符合项目规范 | ||
| - [ ] 确认无 console 错误或警告 | ||
| - [ ] 验证 TypeScript 类型检查通过:`npm run type-check` 或类似命令 | ||
|
|
||
| --- | ||
|
|
||
| ### 6. 提交变更 | ||
| **状态:** `pending` | **预计工作量:** 5 分钟 | ||
|
|
||
| - [ ] 添加变更到暂存区:`git add src/renderer/App.tsx` | ||
| - [ ] 创建提交: | ||
| ``` | ||
| git commit -m "feat: remove view transition fade animation | ||
|
|
||
| - Remove AnimatePresence and motion.div wrapper from main content area | ||
| - Replace with standard div for instant view switching | ||
| - Preserve sidebar margin transition effect | ||
|
|
||
| This change improves perceived responsiveness when navigating between | ||
| views in the sidebar navigation." | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ### 7. 合并与清理 | ||
| **状态:** `pending` | **预计工作量:** 5 分钟 | ||
|
|
||
| - [ ] 推送功能分支:`git push origin feature/remove-view-transition-animation` | ||
| - [ ] 创建 Pull Request 到 `feature/multiplerelease` 分支 | ||
| - [ ] 在 PR 描述中引用此 OpenSpec 提案 | ||
| - [ ] 等待代码审查和合并 | ||
| - [ ] 合并后删除功能分支 | ||
|
|
||
| --- | ||
|
|
||
| ## 验收检查清单 | ||
|
|
||
| 在标记任务完成前,确认以下所有项目: | ||
|
|
||
| ### 功能完整性 | ||
| - [ ] 所有四个视图(system、web、version、license)可正常切换 | ||
| - [ ] 视图切换时内容立即显示 | ||
| - [ ] 无 0.4 秒动画延迟 | ||
|
|
||
| ### 视觉一致性 | ||
| - [ ] 侧边栏宽度变化时的过渡效果保持正常 | ||
| - [ ] 侧边栏导航项活跃状态指示器正常 | ||
| - [ ] 无页面闪烁或布局错位 | ||
|
|
||
| ### 代码质量 | ||
| - [ ] 无 TypeScript 类型错误 | ||
| - [ ] 无运行时控制台错误 | ||
| - [ ] 代码格式符合项目规范 | ||
|
|
||
| ### 性能 | ||
| - [ ] 视图切换响应主观感觉即时(< 100ms) | ||
|
|
||
| --- | ||
|
|
||
| ## 回滚计划 | ||
|
|
||
| 如果需要回滚此变更: | ||
|
|
||
| 1. **紧急回滚**:使用 `git revert <commit-hash>` 撤销提交 | ||
| 2. **手动回滚**:恢复 `AnimatePresence` 和 `motion.div` 代码结构 | ||
| 3. **验证**:确认动画效果恢复正常后重新部署 | ||
|
|
||
| --- | ||
|
|
||
| ## 依赖关系 | ||
|
|
||
| ``` | ||
| ┌─────────────────┐ | ||
| │ 1. 代码准备 │ | ||
| └────────┬────────┘ | ||
| │ | ||
| ▼ | ||
| ┌─────────────────┐ | ||
| │ 2. 修改 App.tsx │ | ||
| └────────┬────────┘ | ||
| │ | ||
| ▼ | ||
| ┌─────────────────┐ | ||
| │ 3. 清理导入 │ (可选) | ||
| └────────┬────────┘ | ||
| │ | ||
| ▼ | ||
| ┌─────────────────┐ | ||
| │ 4. 本地测试 │ | ||
| └────────┬────────┘ | ||
| │ | ||
| ▼ | ||
| ┌─────────────────┐ | ||
| │ 5. 代码审查 │ | ||
| └────────┬────────┘ | ||
| │ | ||
| ▼ | ||
| ┌─────────────────┐ | ||
| │ 6. 提交变更 │ | ||
| └────────┬────────┘ | ||
| │ | ||
| ▼ | ||
| ┌─────────────────┐ | ||
| │ 7. 合并与清理 │ | ||
| └─────────────────┘ | ||
| ``` | ||
|
|
||
| **总预计工作量:** 45-50 分钟 | ||
| **风险等级:** 低 | ||
| **可并行任务:** 无(任务需顺序执行) |
There was a problem hiding this comment.
Update task completion status to reflect implementation progress.
According to the PR commit message, the view transition animation removal has been completed ("Remove view transition fade animation — removed AnimatePresence and motion.div wrapper from main content area, replaced with a standard div; preserved sidebar margin transition."). However, all tasks in this file remain unchecked with "pending" status.
Based on learnings, tasks should be checked off sequentially as implementation progresses, and all tasks should be marked - [x] after implementation work is complete and confirmed. Please update the checkboxes to accurately reflect the current state of implementation.
Based on learnings: "Track implementation progress sequentially in tasks.md with checkboxes, completing one task at a time before moving to the next" and "Update every task in tasks.md to - [x] after all implementation work is complete and confirmed."
🤖 Prompt for AI Agents
In
`@openspec/changes/archive/2026-02-08-remove-view-transition-animation/tasks.md`
around lines 1 - 200, Update the task checklist to reflect the completed
implementation: mark each step in the tasks list (1. 代码准备, 2. 修改 App.tsx 移除视图动画,
3. 清理未使用的导入, 4. 本地功能测试, 5. 代码审查与优化, 6. 提交变更, 7. 合并与清理) as completed (- [x]) and
tick off all acceptance criteria (功能完整性, 视觉一致性, 代码质量, 性能) after confirming the
changes described in the PR commit ("Remove view transition fade animation" —
removal of AnimatePresence and motion.div and replacement with a standard div
preserving sidebar margin transition); ensure the note about keeping
project-level dependency on motion/react remains and update the overall status
and estimated work/time fields accordingly.
## 概述 - 在 `sync-azure-storage.yml` 工作流中添加 JSON 压缩 - 使用 `jq -c` 参数生成紧凑格式的 `index.json` ## 变更内容 - 修改 jq 输出参数,添加 `-c` 选项 - 添加 JSON 有效性验证步骤 ## 预期收益 - 减少 index.json 文件体积 20-40% - 提升客户端加载性能 ## 验证 - [x] 本地测试 jq -c 输出正确 - [x] JSON 格式验证通过 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added HTTP index-based package source support for discovering and downloading packages from custom servers. * Introduced global license management with configuration persistence and sync status tracking. * Enabled multi-source package management, supporting local folders, GitHub Releases, and HTTP index sources. * **Improvements** * Removed animated view transitions for instantaneous page switching. * Enhanced data integrity with JSON validation in configuration workflows. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
概述
sync-azure-storage.yml工作流中添加 JSON 压缩jq -c参数生成紧凑格式的index.json变更内容
-c选项预期收益
验证
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
UI/UX Improvements
Internationalization