-
Notifications
You must be signed in to change notification settings - Fork 21
Feat: Add custom app launcher functionality #228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
- Add customApps database table for storing user-added applications - Implement CustomAppProvider for managing custom apps (CRUD operations) - Add IPC channels for custom app management and file selection - Create CustomAppIntegration for search functionality - Register CustomAppSearchProvider in search engine - Support JAR files, scripts, executables, and other file types - Include pinyin keyword generation for Chinese app names - Track usage statistics (launch count, last used time) This feature allows users to manually add any file to the launcher, solving the problem of missing applications in automatic scans.
📝 WalkthroughWalkthroughIntroduces a custom applications management feature by adding a database schema for custom apps, a provider for CRUD operations, Electron IPC handlers for main/renderer communication, search integration with caching and scoring, and module lifecycle management. Enables users to add, manage, search, and launch custom applications. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant Renderer as Renderer Process
participant IPC as IPC Handler
participant Provider as CustomAppProvider
participant DB as Database
User->>Renderer: Click "Add App"
Renderer->>IPC: SELECT_FILE dialog
IPC->>DB: (file selected)
IPC->>Renderer: Return file path
Renderer->>IPC: ADD (CustomAppInput)
IPC->>Provider: addCustomApp()
Provider->>Provider: Validate file existence
Provider->>Provider: detectFileType()
Provider->>DB: Insert new record
DB-->>Provider: Return CustomApp
Provider-->>IPC: CustomApp result
IPC-->>Renderer: Success response
Renderer->>User: App added
sequenceDiagram
participant User as User
participant Search as SearchEngine
participant Provider as CustomAppSearchProvider
participant Integration as CustomAppIntegration
participant Cache as TTL Cache
participant DB as Database
User->>Search: Enter search query
Search->>Provider: provide(query)
Provider->>Integration: search(query)
Integration->>Cache: Check cached apps
alt Cache expired
Cache->>DB: Fetch enabled apps
DB-->>Cache: Return CustomApps
Cache->>Cache: Store with TTL
end
Integration->>Integration: calculateScore() for each app
Integration->>Integration: Filter positive scores
Integration->>Integration: Sort by score
Integration-->>Provider: TuffSearchResult[]
Provider-->>Search: Results with logging
Search->>User: Display results
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c2dc7e987a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const now = Date.now() | ||
| const updateData: any = { | ||
| ...updates, | ||
| updatedAt: now |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| case 'jar': | ||
| // JAR 文件使用 java -jar 启动 | ||
| await execAsync(`java -jar "${appPath}"`, { | ||
| cwd: workingDirectory | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In
`@apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-integration.ts`:
- Around line 80-131: In calculateScore (the function that uses
customAppProvider.generateKeywords on app.displayName and scores
exact/prefix/contains/file/path/notes/useCount/lastUsedAt), add a short
clarifying comment near the keyword matching block stating that scoring is
cumulative across criteria (e.g., note that an exact match may also pass
prefix/contains checks and points are intentionally summed), so future
maintainers understand the design intent and that multiple matches reinforce
relevance.
- Around line 136-173: appToSearchResult currently instantiates
TuffSearchResultBuilder incorrectly and calls item-level methods on it; instead,
build a TuffItem using TuffItemBuilder (setId, setTitle, setSubtitle, setIcon,
setAction, setMetadata, setTags) to produce a TuffItem, then construct a
TuffSearchResult by creating new TuffSearchResultBuilder(query) (provide a
TuffQuery instance consistent with other providers), call
setItems([theTuffItem]) and any needed setDuration()/setActivate()/setSources(),
then call build(); ensure the action uses
customAppProvider.launchCustomApp(app.id) as before and that
TuffSearchResultBuilder is given the required query parameter.
In `@apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-ipc.ts`:
- Around line 33-42: The file currently uses ipcMain.handle for IPC handlers
(e.g., in registerCustomAppIpc and the handler for CustomAppIpcChannels.GET_ALL
that calls customAppProvider.getAllCustomApps); replace each ipcMain.handle
usage with the project's standard regChannel(ChannelType.MAIN, ...) pattern
(import regChannel and ChannelType/ChannelCore as needed) so all handlers are
registered via the centralized channel infrastructure; ensure each handler's
signature and error handling are preserved when migrating (use
regChannel(ChannelType.MAIN, CustomAppIpcChannels.GET_ALL, async () => { ... }))
and remove the direct ipcMain.handle calls.
In `@apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-provider.ts`:
- Around line 168-216: The launch method currently uses execAsync with
interpolated strings (in launch, cases for 'jar','sh','bash','py' and the
launchCommand path) which allows command injection; change these to use
child_process.spawn (or execFile) with a command and args array instead of
concatenated strings, e.g., spawn('java', ['-jar', appPath], { cwd, env }) for
the 'jar' branch, spawn('bash', [appPath], { cwd }) for shell scripts,
spawn('python3', [appPath], { cwd }) for 'py', and when a user-supplied
launchCommand is provided, parse it into a command and args (or require separate
command + args fields) and call spawn(command, args, { cwd, env }) rather than
passing the raw string to execAsync; remove usage of execAsync for these flows
and keep shell.openPath(appPath) only for cases that truly require opening with
the OS.
- Around line 242-247: The current logic treats displayName as a file path by
calling path.basename on it; replace that by pulling the actual file path from
the CustomApp object (e.g., use the CustomApp.path field) and only compute const
fileName = path.basename(customApp.path, path.extname(customApp.path)) when
customApp.path is defined/truthy, then push fileName.toLowerCase() into
keywords; remove the fileName !== displayName comparison and guard against
missing path to avoid calling path.* on a non-path string.
- Around line 152-159: The update currently reads app.useCount and writes
app.useCount + 1, causing a race condition; change the db update for customApps
(the db.update(...).set({...}).where(eq(customApps.id, id)) call) to perform a
database-side increment for useCount (use an SQL increment expression or the
query-builder's increment helper) while still setting lastUsedAt to Date.now(),
so the update is atomic and avoids lost concurrent increments.
🧹 Nitpick comments (8)
apps/core-app/src/main/db/schema.ts (2)
767-770: Timestamp columns lack SQL defaults unlike other tables in this schema.Other tables in this schema (e.g.,
embeddings,itemUsageStats,intelligenceQuotas) usesql\(strftime('%s', 'now'))`for automatic timestamp defaults. HerecreatedAtandupdatedAt` require manual population, which could lead to null/missing timestamps if the application code forgets to set them.♻️ Consider adding SQL defaults for consistency
// 创建时间 - createdAt: integer('created_at').notNull(), + createdAt: integer('created_at').notNull().default(sql`(strftime('%s', 'now'))`), // 最后修改时间 - updatedAt: integer('updated_at').notNull(), + updatedAt: integer('updated_at').notNull().default(sql`(strftime('%s', 'now'))`),
781-785: Redundant index onpathcolumn.The
pathcolumn already has a.unique()constraint (line 743), which implicitly creates an index in SQLite. The explicitpathIdxindex is redundant and adds unnecessary overhead during writes.♻️ Remove redundant index
(table) => ({ - pathIdx: index('idx_custom_apps_path').on(table.path), enabledIdx: index('idx_custom_apps_enabled').on(table.enabled), lastUsedIdx: index('idx_custom_apps_last_used').on(table.lastUsedAt) })apps/core-app/src/main/modules/box-tool/search-engine/search-core.ts (1)
73-88: Consider adding custom apps to the@appfilter alias.The
custom-app-providerisn't included inPROVIDER_ALIASES, so users searching with@appwon't see custom apps. If custom apps should be discoverable via the@appfilter, add the provider ID to the alias list.♻️ Add custom-app-provider to app alias
const PROVIDER_ALIASES: Record<string, string[]> = { file: [ 'file-provider', 'file-index', 'files', 'fs', 'document', 'everything-provider', 'everything' ], - app: ['app-provider', 'applications', 'apps'], + app: ['app-provider', 'applications', 'apps', 'custom-app-provider'], plugin: ['plugin-features', 'plugins', 'extension', 'extensions'], system: ['system-provider', 'sys'], url: ['url-provider', 'link', 'links'], preview: ['preview-provider'] }apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-ipc.ts (2)
55-62: Add input validation for IPC parameters.IPC handlers receive data from the renderer process, which should be treated as untrusted input. The
idparameter should be validated (positive integer check), and in the ADD/UPDATE handlers, theCustomAppInputshould be sanitized.Example validation
ipcMain.handle(CustomAppIpcChannels.GET_ONE, async (_event, id: number) => { try { + if (typeof id !== 'number' || !Number.isInteger(id) || id <= 0) { + throw new Error('Invalid app id') + } return await customAppProvider.getCustomApp(id) } catch (error) {
126-148: Consider specifying a parent window for the dialog.The dialog may appear behind the main window on some platforms when no parent is specified. Using
BrowserWindow.getFocusedWindow()as the parent can improve the user experience.Suggested improvement
+import { ipcMain, dialog, BrowserWindow } from 'electron' ... ipcMain.handle(CustomAppIpcChannels.SELECT_FILE, async () => { try { + const parentWindow = BrowserWindow.getFocusedWindow() const result = await dialog.showOpenDialog({ + ...(parentWindow ? { parentWindow } : {}), title: 'Select Application or File',apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-provider.ts (3)
44-76: Add validation that the path is a regular file.
fs.existsSynconly checks existence, not whether the path is a regular file. Users could potentially add directories or symbolic links. Consider usingfs.statSyncto verify it's a file.Suggested improvement
async addCustomApp(input: CustomAppInput): Promise<CustomApp> { - if (!fs.existsSync(input.path)) { - throw new Error(`File not found: ${input.path}`) + try { + const stat = fs.statSync(input.path) + if (!stat.isFile()) { + throw new Error(`Path is not a file: ${input.path}`) + } + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + throw new Error(`File not found: ${input.path}`) + } + throw error }
81-103: Avoidanytype and validate path updates.The
anytype on line 83 bypasses TypeScript safety. Also, ifupdates.pathis provided, the new path should be validated for existence before updating.Suggested improvement
async updateCustomApp(id: number, updates: Partial<CustomAppInput>): Promise<CustomApp> { + if (updates.path) { + if (!fs.existsSync(updates.path)) { + throw new Error(`File not found: ${updates.path}`) + } + } + const now = Date.now() - const updateData: any = { + const updateData: Partial<typeof customApps.$inferInsert> & { updatedAt: number } = { ...updates, updatedAt: now }
276-293: Add error handling forJSON.parse.If
environmentVarscontains malformed JSON (e.g., due to data corruption),JSON.parsewill throw and crash the operation. Wrap in try/catch for resilience.Suggested fix
private mapDbToCustomApp(row: any): CustomApp { + let environmentVars: Record<string, string> | undefined + if (row.environmentVars) { + try { + environmentVars = JSON.parse(row.environmentVars) + } catch { + environmentVars = undefined + } + } + return { id: row.id, path: row.path, displayName: row.displayName, // ... - environmentVars: row.environmentVars ? JSON.parse(row.environmentVars) : undefined, + environmentVars, // ... } }
| private calculateScore(app: CustomApp, query: string): number { | ||
| let score = 0 | ||
|
|
||
| // 生成搜索关键词 | ||
| const keywords = customAppProvider.generateKeywords(app.displayName) | ||
|
|
||
| // 完全匹配 | ||
| if (keywords.some((kw) => kw === query)) { | ||
| score += 100 | ||
| } | ||
|
|
||
| // 前缀匹配 | ||
| if (keywords.some((kw) => kw.startsWith(query))) { | ||
| score += 80 | ||
| } | ||
|
|
||
| // 包含匹配 | ||
| if (keywords.some((kw) => kw.includes(query))) { | ||
| score += 60 | ||
| } | ||
|
|
||
| // 文件名匹配 | ||
| const fileName = path.basename(app.path).toLowerCase() | ||
| if (fileName.includes(query)) { | ||
| score += 40 | ||
| } | ||
|
|
||
| // 路径匹配 | ||
| if (app.path.toLowerCase().includes(query)) { | ||
| score += 20 | ||
| } | ||
|
|
||
| // 备注匹配 | ||
| if (app.notes && app.notes.toLowerCase().includes(query)) { | ||
| score += 10 | ||
| } | ||
|
|
||
| // 使用频率加权 | ||
| if (app.useCount > 0) { | ||
| score += Math.min(app.useCount, 20) | ||
| } | ||
|
|
||
| // 最近使用加权 | ||
| if (app.lastUsedAt) { | ||
| const daysSinceLastUse = (Date.now() - app.lastUsedAt) / (1000 * 60 * 60 * 24) | ||
| if (daysSinceLastUse < 7) { | ||
| score += 10 | ||
| } | ||
| } | ||
|
|
||
| return score | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Check for other scoring implementations in the box-tool addon modules
rg -n 'calculateScore|score \+=' --type=ts apps/core-app/src/main/modules/box-tool/addon -A 3 -B 1Repository: talex-touch/tuff
Length of output: 5076
🏁 Script executed:
# Also check if there are any other search providers with similar ranking logic
fd -t f '\.(ts|tsx)$' apps/core-app/src/main/modules/box-tool/addon -exec rg -l 'score|ranking|weight' {} \;Repository: talex-touch/tuff
Length of output: 272
🏁 Script executed:
# Look at the actual structure to see if this is the pattern elsewhere
rg -n 'if.*\.some|keyword.*match' --type=ts apps/core-app/src/main/modules/box-tool/addon -A 2 -B 1Repository: talex-touch/tuff
Length of output: 4426
🏁 Script executed:
# Check if there are any tests that might clarify scoring intent
fd -t f 'custom-app.*test\|test.*custom-app' apps/core-app/ --type f 2>/dev/null | head -20Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
# Look at search-processing-service.ts to see if it has different ranking approach
cat -n apps/core-app/src/main/modules/box-tool/addon/apps/search-processing-service.ts | sed -n '90,120p'Repository: talex-touch/tuff
Length of output: 1303
Add clarifying comment about intentional cumulative scoring in calculateScore().
The scoring logic accumulates points across multiple criteria—an exact keyword match will also pass prefix and contains checks, yielding up to 240 points from keywords alone. This appears to be an intentional design to heavily boost relevance when an app matches multiple scoring tiers. Consider adding a brief comment (e.g., "// Cumulative: matches across multiple criteria reinforce relevance") to make this behavior explicit for future maintainers.
🤖 Prompt for AI Agents
In `@apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-integration.ts`
around lines 80 - 131, In calculateScore (the function that uses
customAppProvider.generateKeywords on app.displayName and scores
exact/prefix/contains/file/path/notes/useCount/lastUsedAt), add a short
clarifying comment near the keyword matching block stating that scoring is
cumulative across criteria (e.g., note that an exact match may also pass
prefix/contains checks and points are intentionally summed), so future
maintainers understand the design intent and that multiple matches reinforce
relevance.
| private appToSearchResult(app: CustomApp): TuffSearchResult { | ||
| const builder = new TuffSearchResultBuilder() | ||
|
|
||
| // 基本信息 | ||
| builder | ||
| .setId(`custom-app:${app.id}`) | ||
| .setTitle(app.displayName) | ||
| .setSubtitle(app.path) | ||
| .setIcon(app.iconPath || this.getDefaultIcon(app.fileType)) | ||
|
|
||
| // 设置执行动作 | ||
| builder.setAction(async () => { | ||
| try { | ||
| await customAppProvider.launchCustomApp(app.id) | ||
| return true | ||
| } catch (error) { | ||
| logger.error(`Failed to launch custom app ${app.id}:`, error) | ||
| return false | ||
| } | ||
| }) | ||
|
|
||
| // 添加元数据 | ||
| builder.setMetadata({ | ||
| type: 'custom-app', | ||
| customAppId: app.id, | ||
| fileType: app.fileType, | ||
| path: app.path | ||
| }) | ||
|
|
||
| // 添加标签 | ||
| const tags: string[] = ['Custom App'] | ||
| if (app.fileType) { | ||
| tags.push(app.fileType.toUpperCase()) | ||
| } | ||
| builder.setTags(tags) | ||
|
|
||
| return builder.build() | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, find TuffSearchResultBuilder definition
fd -t f "\.ts" apps/core-app/src | xargs rg -l "class TuffSearchResultBuilder"Repository: talex-touch/tuff
Length of output: 44
🏁 Script executed:
#!/bin/bash
# Find and examine TuffSearchResultBuilder class
rg -A 50 "class TuffSearchResultBuilder" --type=tsRepository: talex-touch/tuff
Length of output: 3508
🏁 Script executed:
#!/bin/bash
# Check search-core.ts for source property requirements
fd -t f "search-core.ts" apps/core-app/srcRepository: talex-touch/tuff
Length of output: 128
🏁 Script executed:
#!/bin/bash
# Look for other TuffSearchResultBuilder usage in box-tool addon
rg "TuffSearchResultBuilder" --type=ts apps/core-app/src/main/modules/box-tool/addon -B 2 -A 15Repository: talex-touch/tuff
Length of output: 50372
🏁 Script executed:
#!/bin/bash
# Find TuffItemBuilder class definition
rg -A 30 "class TuffItemBuilder" --type=ts packages/utilsRepository: talex-touch/tuff
Length of output: 2422
🏁 Script executed:
#!/bin/bash
# Look at custom-app-integration.ts to understand the full context
fd -t f "custom-app-integration.ts" apps/core-app/srcRepository: talex-touch/tuff
Length of output: 136
🏁 Script executed:
#!/bin/bash
# Check the actual file around the method
cat -n apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-integration.ts | sed -n '130,180p'Repository: talex-touch/tuff
Length of output: 1542
🏁 Script executed:
#!/bin/bash
# Check if TuffItemBuilder has setSource, setAction, setMetadata, setTags methods
rg -A 100 "class TuffItemBuilder" --type=ts packages/utils/core-box/builder/tuff-builder.ts | grep -E "(setSource|setAction|setMetadata|setTags)"Repository: talex-touch/tuff
Length of output: 72
🏁 Script executed:
#!/bin/bash
# Look for usage of appToSearchResult to understand the expected return type and how it's called
rg -B 5 -A 5 "appToSearchResult" apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-integration.tsRepository: talex-touch/tuff
Length of output: 879
🏁 Script executed:
#!/bin/bash
# Check how the method should properly use both builders based on context
rg -B 10 "appToSearchResult\(" apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-integration.ts | head -30Repository: talex-touch/tuff
Length of output: 660
🏁 Script executed:
#!/bin/bash
# Check full TuffSearchResultBuilder API to see what methods it actually has
rg -A 150 "class TuffSearchResultBuilder" packages/utils/core-box/builder/tuff-builder.ts | head -100Repository: talex-touch/tuff
Length of output: 2050
🏁 Script executed:
#!/bin/bash
# Check TuffItemBuilder full API with all its methods
rg -A 200 "class TuffItemBuilder" packages/utils/core-box/builder/tuff-builder.ts | grep -E "^\s+(public |private ).*\(" | head -30Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check what TuffItemBuilder methods are available
rg -A 200 "class TuffItemBuilder" packages/utils/core-box/builder/tuff-builder.ts | grep "public.*(" | head -40Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Look at how other providers actually build results - check app-provider.ts more carefully
rg -B 5 -A 15 "private.*toSearchResult|appToSearchResult" apps/core-app/src/main/modules/box-tool/addon/apps/app-provider.ts | head -60Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Get the full TuffItemBuilder class definition without grep filtering
rg "class TuffItemBuilder" -A 300 packages/utils/core-box/builder/tuff-builder.ts | head -200Repository: talex-touch/tuff
Length of output: 4013
🏁 Script executed:
#!/bin/bash
# Look at app-provider.ts to see how it properly converts apps to items
cat apps/core-app/src/main/modules/box-tool/addon/apps/app-provider.ts | grep -A 50 "mapFileToTuffItem\|toTuffItem" | head -70Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Search to see if appToSearchResult is using the wrong method pattern - maybe look at how search results are built in other integrations
fd -t f ".*-integration.ts" apps/core-app/src/main/modules/box-tool/addon/appsRepository: talex-touch/tuff
Length of output: 136
🏁 Script executed:
#!/bin/bash
# Let's check if there's a similar integration to compare the pattern
cat apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-integration.ts | sed -n '1,50p'Repository: talex-touch/tuff
Length of output: 1399
🏁 Script executed:
#!/bin/bash
# Check what TuffSearchResult type definition looks like
rg "type TuffSearchResult|interface TuffSearchResult" -A 10 packages/utilsRepository: talex-touch/tuff
Length of output: 1394
🏁 Script executed:
#!/bin/bash
# Check how the search method is being called - does it have access to query?
cat apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-integration.ts | sed -n '10,80p'Repository: talex-touch/tuff
Length of output: 1734
🏁 Script executed:
#!/bin/bash
# Let me verify what methods are actually available on TuffSearchResultBuilder - looking for setId, setTitle etc
rg "setId|setTitle|setSubtitle|setIcon|setAction|setMetadata" packages/utils/core-box/builder/tuff-builder.tsRepository: talex-touch/tuff
Length of output: 812
🏁 Script executed:
#!/bin/bash
# Verify the search method signature that's calling appToSearchResult
rg "async search" apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-integration.ts -A 20 | head -30Repository: talex-touch/tuff
Length of output: 631
🏁 Script executed:
#!/bin/bash
# Check if TuffQuery is available in the context to build properly
rg "TuffQuery" apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-integration.tsRepository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Let's see how the search method signature uses query - is it TuffQuery or just string?
cat apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-integration.ts | head -150 | tail -50Repository: talex-touch/tuff
Length of output: 1180
🏁 Script executed:
#!/bin/bash
# Check if there's a TuffQuery import or if it's just a string
rg "import.*TuffQuery\|type TuffQuery" apps/core-app/src/main/modules/box-tool/addon/apps/Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Verify the exact type of what's being passed around - look at search method more carefully
rg -B 5 "appToSearchResult\(app\)" apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-integration.tsRepository: talex-touch/tuff
Length of output: 422
Fix critical bugs in appToSearchResult(): use wrong builder class and missing required constructor parameter.
The method has multiple critical issues:
-
Missing required parameter:
TuffSearchResultBuilderconstructor requires aquery: TuffQueryparameter, but the code callsnew TuffSearchResultBuilder()with no arguments. -
Wrong builder class: The code attempts to call
setId(),setTitle(),setSubtitle(),setIcon(),setAction(),setMetadata(), andsetTags()onTuffSearchResultBuilder, but these methods don't exist on that class. These are methods onTuffItemBuilder.TuffSearchResultBuilderonly has:setItems(),setDuration(),setActivate(),setSources(), andsetSortStats(). -
Incorrect architecture: The method should build individual
TuffItemobjects usingTuffItemBuilder, then pass them toTuffSearchResultBuilder.setItems()to create the result container.
The correct pattern (based on other providers) is:
- Build
TuffItemwithTuffItemBuilder - Create
TuffSearchResultwithTuffSearchResultBuilder(query).setItems([item]).build() - Each result must include a
queryobject, not just a string
🤖 Prompt for AI Agents
In `@apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-integration.ts`
around lines 136 - 173, appToSearchResult currently instantiates
TuffSearchResultBuilder incorrectly and calls item-level methods on it; instead,
build a TuffItem using TuffItemBuilder (setId, setTitle, setSubtitle, setIcon,
setAction, setMetadata, setTags) to produce a TuffItem, then construct a
TuffSearchResult by creating new TuffSearchResultBuilder(query) (provide a
TuffQuery instance consistent with other providers), call
setItems([theTuffItem]) and any needed setDuration()/setActivate()/setSources(),
then call build(); ensure the action uses
customAppProvider.launchCustomApp(app.id) as before and that
TuffSearchResultBuilder is given the required query parameter.
| export function registerCustomAppIpc(): void { | ||
| // 获取所有自定义应用 | ||
| ipcMain.handle(CustomAppIpcChannels.GET_ALL, async () => { | ||
| try { | ||
| return await customAppProvider.getAllCustomApps() | ||
| } catch (error) { | ||
| logger.error('Failed to get all custom apps:', error) | ||
| throw error | ||
| } | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for regChannel usage in the codebase
rg -n "regChannel" --type=ts -B2 -A2 apps/core-app/src/main/Repository: talex-touch/tuff
Length of output: 2943
🏁 Script executed:
#!/bin/bash
# Check the full content of the custom-app-ipc.ts file to understand structure
wc -l "apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-ipc.ts"Repository: talex-touch/tuff
Length of output: 132
🏁 Script executed:
#!/bin/bash
# Search for ipcMain.handle usage in main process files to see if pattern is used elsewhere
rg -n "ipcMain\.handle" --type=ts apps/core-app/src/main/ | head -20Repository: talex-touch/tuff
Length of output: 1464
🏁 Script executed:
#!/bin/bash
# Check what's exported from utils package related to regChannel
find . -name "*utils*" -o -name "*channel*" | grep -E "(src|lib)" | head -20Repository: talex-touch/tuff
Length of output: 1070
🏁 Script executed:
#!/bin/bash
# Read the custom-app-ipc.ts file to see imports and structure
head -40 "apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-ipc.ts"Repository: talex-touch/tuff
Length of output: 1075
🏁 Script executed:
#!/bin/bash
# Check how other addon IPC files are structured
find "apps/core-app/src/main/modules/box-tool/addon" -name "*ipc*.ts" -o -name "*channel*.ts" | head -10Repository: talex-touch/tuff
Length of output: 128
🏁 Script executed:
#!/bin/bash
# Check how ChannelCore is instantiated and made available in modules
rg -n "ChannelCore\|channelBridge\|channelCore" --type=ts "apps/core-app/src/main/modules/box-tool" -B2 -A2 | head -40Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check how the addon module is initialized and if it has access to channel infrastructure
rg -n "registerCustomAppIpc\|custom-app-ipc" --type=ts "apps/core-app/src/main" -B3 -A1Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check if addon modules have a base structure or initialization pattern
ls -la "apps/core-app/src/main/modules/box-tool/addon/apps/" | head -20Repository: talex-touch/tuff
Length of output: 1200
🏁 Script executed:
#!/bin/bash
# Check the module entry points and structure
head -50 "apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-provider.ts" 2>/dev/null || echo "File not found, checking directory..."Repository: talex-touch/tuff
Length of output: 1201
🏁 Script executed:
#!/bin/bash
# Check the custom-app-module.ts to see how the addon is structured
cat -n "apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-module.ts"Repository: talex-touch/tuff
Length of output: 1244
🏁 Script executed:
#!/bin/bash
# Search for where registerCustomAppIpc is called
rg -n "registerCustomAppIpc\|unregisterCustomAppIpc" --type=ts "apps/core-app/src/main"Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check if other addon or non-module files use ipcMain.handle vs regChannel
rg -n "ipcMain\.handle" --type=ts "apps/core-app/src/main" | wc -lRepository: talex-touch/tuff
Length of output: 62
🏁 Script executed:
#!/bin/bash
# Check how modules typically access ChannelCore or channel infrastructure
rg -n "import.*ChannelCore\|import.*channelBridge" --type=ts "apps/core-app/src/main/modules" -B1 | head -30Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check if there's a global or singleton ChannelCore instance that should be imported
rg -n "export.*channelCore\|export.*channelBridge" --type=ts "apps/core-app/src/main" -B2 -A1Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check how BaseModule or IModule types provide access to channel functionality
rg -n "interface IModule\|class BaseModule" --type=ts "apps/core-app/src/main" -A15 | head -50Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Look at how modules access services - check module initialization patterns
head -100 "apps/core-app/src/main/modules/box-tool/core-box/window.ts" | grep -A20 "regChannel"Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Search for files that successfully use regChannel to understand the pattern
rg -l "regChannel" --type=ts "apps/core-app/src/main/modules" | head -5Repository: talex-touch/tuff
Length of output: 221
🏁 Script executed:
#!/bin/bash
# Check a file that uses regChannel to see how it imports/uses it
rg -n "regChannel" --type=ts "apps/core-app/src/main/modules/ai/agents/agent-channels.ts" -B10 -A5Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check how regChannel is used in plugin.ts which showed regChannel usage
rg -n "regChannel" --type=ts "apps/core-app/src/main/modules/plugin/plugin.ts" -B5 -A5 | head -40Repository: talex-touch/tuff
Length of output: 582
🏁 Script executed:
#!/bin/bash
# Look for imports of channelBridge or access patterns to the channel system in plugin module
head -50 "apps/core-app/src/main/modules/plugin/plugin.ts" | grep -E "import|channel|Channel"Repository: talex-touch/tuff
Length of output: 1378
🏁 Script executed:
#!/bin/bash
# Check the module manager to understand how modules are initialized and what they have access to
rg -n "class.*ModuleManager\|interface.*ModuleManager" --type=ts "apps/core-app/src/main" -A30 | head -60Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check if there's a service injection or context passed to modules
rg -n "class ModuleManager\|onLoad.*context\|onLoad.*channel" --type=ts "apps/core-app/src/main" -A10 | head -50Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Look at how the addon is loaded - check the parent module that loads CustomAppModule
rg -n "customAppModule\|CustomAppModule" --type=ts "apps/core-app/src/main" -B5 -A3Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check if there are any other addon IPC files and how they handle registration
find "apps/core-app/src/main/modules/box-tool/addon" -type f -name "*.ts" -exec grep -l "ipcMain\|regChannel" {} \;Repository: talex-touch/tuff
Length of output: 128
🏁 Script executed:
#!/bin/bash
# Search for how main-renderer IPC is handled in the codebase - look for ChannelCore instantiation
rg -n "new ChannelCore\|instance.*ChannelCore" --type=ts "apps/core-app/src/main" -B3 -A3Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check if there's a different pattern for addon modules vs regular modules
find "apps/core-app/src/main/modules/box-tool/addon" -name "*.ts" -exec grep -l "class\|export" {} \; | xargs grep -l "Module\|IPC"Repository: talex-touch/tuff
Length of output: 343
🏁 Script executed:
#!/bin/bash
# Look for where IPC handlers are typically registered in the main modules
rg -n "ipcMain\.on\|ipcMain\.handle" --type=ts "apps/core-app/src/main/modules/box-tool" -B2 -A1 | head -30Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check the coding guidelines more carefully - verify the exact requirements
cat -n "coding_guidelines" 2>/dev/null || rg -n "Use regChannel\|ChannelType.MAIN" --type=ts --type=md "apps/core-app" | head -20Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Look for how the ChannelCore or channel infrastructure is accessed in module context
rg -n "channelBridge\|ChannelCore" --type=ts "apps/core-app/src/main/modules/box-tool/addon" -B3 -A3Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check if there's a way to access the centralized channel system from the addon
find "apps/core-app/src/main" -name "*.ts" -path "*/core/*" | xargs grep -l "ChannelCore\|channelBridge" | head -5Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check if there's documentation or comments about why addon modules use direct ipcMain
grep -r "addon\|ipcMain.handle" "apps/core-app/src/main/modules/box-tool/addon/apps" --include="*.ts" | grep -i "comment\|reason\|why\|note\|TODO" | head -10Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Look at the structure of channel-core.ts to understand what regChannel does
sed -n '376,410p' "apps/core-app/src/main/core/channel-core.ts"Repository: talex-touch/tuff
Length of output: 795
🏁 Script executed:
#!/bin/bash
# Check if ipcMain usage is consistent within box-tool addon - is this a pattern?
for file in apps/core-app/src/main/modules/box-tool/addon/*/; do
echo "=== Directory: $file ==="
find "$file" -name "*.ts" -exec grep -l "ipcMain\|regChannel" {} \;
doneRepository: talex-touch/tuff
Length of output: 488
🏁 Script executed:
#!/bin/bash
# Check if ChannelCore is accessible/exported from a central location that addon could import
rg -n "export.*ChannelCore\|export.*channelCore" --type=ts "apps/core-app/src/main" -B1Repository: talex-touch/tuff
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Look at how the main module or core initializes ChannelCore
grep -r "ChannelCore\|channelBridge" "apps/core-app/src/main/core" --include="*.ts" | grep -E "export|instance" | head -10Repository: talex-touch/tuff
Length of output: 42
Replace ipcMain.handle() with regChannel() per coding guidelines.
The coding guidelines require using regChannel(ChannelType.MAIN, ...) for main-renderer IPC communication in files under apps/core-app/src/main/**/*.ts. This file uses ipcMain.handle() directly (9 handlers), which bypasses the centralized channel management provided by ChannelCore. Import ChannelCore or access the channel infrastructure and refactor all handlers to use the standard pattern.
🤖 Prompt for AI Agents
In `@apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-ipc.ts` around
lines 33 - 42, The file currently uses ipcMain.handle for IPC handlers (e.g., in
registerCustomAppIpc and the handler for CustomAppIpcChannels.GET_ALL that calls
customAppProvider.getAllCustomApps); replace each ipcMain.handle usage with the
project's standard regChannel(ChannelType.MAIN, ...) pattern (import regChannel
and ChannelType/ChannelCore as needed) so all handlers are registered via the
centralized channel infrastructure; ensure each handler's signature and error
handling are preserved when migrating (use regChannel(ChannelType.MAIN,
CustomAppIpcChannels.GET_ALL, async () => { ... })) and remove the direct
ipcMain.handle calls.
| // 更新使用统计 | ||
| await db | ||
| .update(customApps) | ||
| .set({ | ||
| lastUsedAt: Date.now(), | ||
| useCount: app.useCount + 1 | ||
| }) | ||
| .where(eq(customApps.id, id)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential race condition in useCount increment.
The read-then-increment pattern (app.useCount + 1) can cause lost updates if the app is launched multiple times concurrently. Use a SQL increment expression instead.
Suggested fix using SQL increment
+import { eq, sql } from 'drizzle-orm'
...
await db
.update(customApps)
.set({
lastUsedAt: Date.now(),
- useCount: app.useCount + 1
+ useCount: sql`${customApps.useCount} + 1`
})
.where(eq(customApps.id, id))📝 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.
| // 更新使用统计 | |
| await db | |
| .update(customApps) | |
| .set({ | |
| lastUsedAt: Date.now(), | |
| useCount: app.useCount + 1 | |
| }) | |
| .where(eq(customApps.id, id)) | |
| // 更新使用统计 | |
| await db | |
| .update(customApps) | |
| .set({ | |
| lastUsedAt: Date.now(), | |
| useCount: sql`${customApps.useCount} + 1` | |
| }) | |
| .where(eq(customApps.id, id)) |
🤖 Prompt for AI Agents
In `@apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-provider.ts`
around lines 152 - 159, The update currently reads app.useCount and writes
app.useCount + 1, causing a race condition; change the db update for customApps
(the db.update(...).set({...}).where(eq(customApps.id, id)) call) to perform a
database-side increment for useCount (use an SQL increment expression or the
query-builder's increment helper) while still setting lastUsedAt to Date.now(),
so the update is atomic and avoids lost concurrent increments.
| private async launch(app: CustomApp): Promise<void> { | ||
| const { path: appPath, launchCommand, workingDirectory, environmentVars } = app | ||
|
|
||
| // 如果指定了启动命令,使用命令启动 | ||
| if (launchCommand) { | ||
| const env = environmentVars | ||
| ? { ...process.env, ...environmentVars } | ||
| : process.env | ||
|
|
||
| await execAsync(launchCommand, { | ||
| cwd: workingDirectory, | ||
| env | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| // 否则根据文件类型决定启动方式 | ||
| switch (app.fileType) { | ||
| case 'jar': | ||
| // JAR 文件使用 java -jar 启动 | ||
| await execAsync(`java -jar "${appPath}"`, { | ||
| cwd: workingDirectory | ||
| }) | ||
| break | ||
|
|
||
| case 'sh': | ||
| case 'bash': | ||
| // Shell 脚本 | ||
| await execAsync(`bash "${appPath}"`, { | ||
| cwd: workingDirectory | ||
| }) | ||
| break | ||
|
|
||
| case 'py': | ||
| // Python 脚本 | ||
| await execAsync(`python3 "${appPath}"`, { | ||
| cwd: workingDirectory | ||
| }) | ||
| break | ||
|
|
||
| case 'app': | ||
| case 'exe': | ||
| case 'other': | ||
| default: | ||
| // 其他文件使用系统默认程序打开 | ||
| await shell.openPath(appPath) | ||
| break | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: Command injection vulnerability in launch method.
Using execAsync with string interpolation allows command injection. A malicious file path like test"; rm -rf /; echo " could execute arbitrary commands. The launchCommand field is also directly executed without sanitization.
Use child_process.spawn with arguments array instead of string concatenation:
🔒 Proposed fix using spawn
-import { exec } from 'node:child_process'
-import { promisify } from 'node:util'
-
-const execAsync = promisify(exec)
+import { spawn } from 'node:child_process'
...
private async launch(app: CustomApp): Promise<void> {
const { path: appPath, launchCommand, workingDirectory, environmentVars } = app
if (launchCommand) {
- const env = environmentVars
- ? { ...process.env, ...environmentVars }
- : process.env
-
- await execAsync(launchCommand, {
- cwd: workingDirectory,
- env
- })
+ // For custom commands, use shell but warn users about security
+ const env = environmentVars ? { ...process.env, ...environmentVars } : process.env
+ const child = spawn(launchCommand, [], {
+ cwd: workingDirectory,
+ env,
+ shell: true,
+ detached: true,
+ stdio: 'ignore'
+ })
+ child.unref()
return
}
switch (app.fileType) {
case 'jar':
- await execAsync(`java -jar "${appPath}"`, { cwd: workingDirectory })
+ spawn('java', ['-jar', appPath], { cwd: workingDirectory, detached: true, stdio: 'ignore' }).unref()
break
case 'sh':
case 'bash':
- await execAsync(`bash "${appPath}"`, { cwd: workingDirectory })
+ spawn('bash', [appPath], { cwd: workingDirectory, detached: true, stdio: 'ignore' }).unref()
break
case 'py':
- await execAsync(`python3 "${appPath}"`, { cwd: workingDirectory })
+ spawn('python3', [appPath], { cwd: workingDirectory, detached: true, stdio: 'ignore' }).unref()
break🤖 Prompt for AI Agents
In `@apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-provider.ts`
around lines 168 - 216, The launch method currently uses execAsync with
interpolated strings (in launch, cases for 'jar','sh','bash','py' and the
launchCommand path) which allows command injection; change these to use
child_process.spawn (or execFile) with a command and args array instead of
concatenated strings, e.g., spawn('java', ['-jar', appPath], { cwd, env }) for
the 'jar' branch, spawn('bash', [appPath], { cwd }) for shell scripts,
spawn('python3', [appPath], { cwd }) for 'py', and when a user-supplied
launchCommand is provided, parse it into a command and args (or require separate
command + args fields) and call spawn(command, args, { cwd, env }) rather than
passing the raw string to execAsync; remove usage of execAsync for these flows
and keep shell.openPath(appPath) only for cases that truly require opening with
the OS.
| // 添加文件名(不含扩展名) | ||
| const fileName = path.basename(displayName, path.extname(displayName)) | ||
| if (fileName !== displayName) { | ||
| keywords.push(fileName.toLowerCase()) | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logic issue: Treating displayName as a file path.
Lines 243-246 use path.basename on displayName, but displayName is a user-provided label (e.g., "My Custom App"), not a file path. If the intent is to include the actual filename in keywords, use the path field from the CustomApp instead.
Suggested fix
- generateKeywords(displayName: string): string[] {
+ generateKeywords(displayName: string, appPath?: string): string[] {
const keywords: string[] = []
keywords.push(displayName.toLowerCase())
// ... pinyin logic ...
- const fileName = path.basename(displayName, path.extname(displayName))
- if (fileName !== displayName) {
- keywords.push(fileName.toLowerCase())
+ if (appPath) {
+ const fileName = path.basename(appPath, path.extname(appPath))
+ keywords.push(fileName.toLowerCase())
}🤖 Prompt for AI Agents
In `@apps/core-app/src/main/modules/box-tool/addon/apps/custom-app-provider.ts`
around lines 242 - 247, The current logic treats displayName as a file path by
calling path.basename on it; replace that by pulling the actual file path from
the CustomApp object (e.g., use the CustomApp.path field) and only compute const
fileName = path.basename(customApp.path, path.extname(customApp.path)) when
customApp.path is defined/truthy, then push fileName.toLowerCase() into
keywords; remove the fileName !== displayName comparison and guard against
missing path to avoid calling path.* on a non-path string.
🎯 Overview
This PR adds the ability for users to manually add any file to the launcher, solving the problem of missing applications in automatic scans.
✨ Features
Core Functionality
Implementation Details
customAppstable with comprehensive metadataCustomAppProviderfor app management and launchingCustomAppSearchProviderintegrated into search engine🔧 Technical Changes
New Files
custom-app-provider.ts: Core business logiccustom-app-ipc.ts: IPC channel definitions and handlerscustom-app-module.ts: Module initializationcustom-app-integration.ts: Search integrationcustom-app-search-provider.ts: Search provider implementationModified Files
schema.ts: AddedcustomAppstableindex.ts: Registered custom app modulesearch-core.ts: Registered custom app search provider🎨 User Experience
Users can now:
🧪 Testing
📝 Notes
🔗 Related Issues
Closes #[issue-number] (if applicable)
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.