-
Notifications
You must be signed in to change notification settings - Fork 74
feat: Gmail label management (view, add, remove, create with l hotkey) #79
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
Open
gpechenik
wants to merge
17
commits into
ankitvgupta:main
Choose a base branch
from
gpechenik:feat/labels-extension
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
d321173
Add labels extension: Gmail label management via extension system
gpechenik 294871d
fix(qa): ISSUE-001 — add explicit type to labels list callback to fix…
gpechenik 046a691
fix(qa): ISSUE-002 — use createLogger instead of console.error in lab…
gpechenik 5e273db
fix(qa): ISSUE-003 — read back authoritative labels from Gmail after …
gpechenik 9cad0b6
fix(qa): ISSUE-004 — handle rejected promise in labels list fetch
gpechenik fcfd9bd
fix(review): auto-fix 7 issues found during pre-landing review
gpechenik e8b9f29
fix(review): parallelize thread readback + extract shared LabelInfo type
gpechenik 4dd9a78
fix: bypass sender-scoped enrichment cache for labels
gpechenik 7080fea
feat: add 'l' keyboard shortcut to open label picker
gpechenik cd7a021
fix: aggregate labels across all thread emails
gpechenik 3c1a31b
fix: filter Gmail star variants from label display
gpechenik a65b42a
fix(review): clear label picker signal on email change + sync store a…
gpechenik 027942c
feat: create new labels from the picker
gpechenik 97eb359
fix: stabilize label rendering to prevent flickering
gpechenik 41ee6e0
fix(review): guard Enter key against busy state + show create label e…
gpechenik 1b1acc7
fix: remove console.log in deactivate + guard ArrowDown on empty list
gpechenik f9f6569
fix: include email prop in store updates after label mutation
gpechenik File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| { | ||
| "name": "mail-ext-labels", | ||
| "version": "1.0.0", | ||
| "description": "View Gmail labels on emails", | ||
| "main": "./src/index.ts", | ||
| "mailExtension": { | ||
| "id": "labels", | ||
| "displayName": "Labels", | ||
| "description": "View Gmail labels on emails", | ||
| "builtIn": true, | ||
| "activationEvents": ["onEmail"], | ||
| "contributes": { | ||
| "sidebarPanels": [ | ||
| { | ||
| "id": "email-labels", | ||
| "title": "Labels", | ||
| "priority": 80, | ||
| "scope": "sender" | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import type { | ||
| ExtensionContext, | ||
| ExtensionAPI, | ||
| ExtensionModule, | ||
| } from "../../../shared/extension-types"; | ||
| import { createLabelsProvider } from "./labels-provider"; | ||
|
|
||
| const extension: ExtensionModule = { | ||
| async activate(context: ExtensionContext, api: ExtensionAPI): Promise<void> { | ||
| context.logger.info("Activating labels extension"); | ||
| const provider = createLabelsProvider(context); | ||
| api.registerEnrichmentProvider(provider); | ||
| context.logger.info("Labels extension activated"); | ||
| }, | ||
|
|
||
| async deactivate(): Promise<void> { | ||
| // No cleanup needed — label cache is in-memory and clears on exit | ||
| }, | ||
| }; | ||
|
|
||
| export const { activate, deactivate } = extension; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import type { | ||
| ExtensionContext, | ||
| EnrichmentProvider, | ||
| EnrichmentData, | ||
| } from "../../../shared/extension-types"; | ||
| import type { DashboardEmail } from "../../../shared/types"; | ||
|
|
||
| /** | ||
| * Labels enrichment provider — registered to keep the sidebar panel active, | ||
| * but returns null for enrichment data. The LabelsPanel component resolves | ||
| * labels directly via window.api.labels to avoid the sender-scoped enrichment | ||
| * cache (which would serve stale labels across different emails from the same sender). | ||
| */ | ||
| export function createLabelsProvider(_context: ExtensionContext): EnrichmentProvider { | ||
| return { | ||
| id: "labels-provider", | ||
| panelId: "email-labels", | ||
| priority: 90, | ||
|
|
||
| canEnrich(): boolean { | ||
| return true; | ||
| }, | ||
|
|
||
| async enrich(_email: DashboardEmail): Promise<EnrichmentData | null> { | ||
| // Labels are resolved directly in LabelsPanel via window.api.labels | ||
| // to avoid sender-scoped enrichment cache returning wrong labels. | ||
| return null; | ||
| }, | ||
| }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| import { ipcMain } from "electron"; | ||
| import { getClient } from "./gmail.ipc"; | ||
| import { updateEmailLabelIds, getEmailsByThread } from "../db"; | ||
| import type { IpcResponse, LabelInfo } from "../../shared/types"; | ||
| import { createLogger } from "../services/logger"; | ||
|
|
||
| const log = createLogger("labels-ipc"); | ||
|
|
||
| export function registerLabelsIpc(): void { | ||
| // List all labels for an account | ||
| ipcMain.handle( | ||
| "labels:list", | ||
| async (_, { accountId }: { accountId: string }): Promise<IpcResponse<LabelInfo[]>> => { | ||
| try { | ||
| const client = await getClient(accountId); | ||
| const labels = await client.listLabels(); | ||
| return { success: true, data: labels }; | ||
| } catch (error) { | ||
| log.error({ err: error }, "Failed to list labels"); | ||
| return { success: false, error: String(error) }; | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| // Create a new Gmail label | ||
| ipcMain.handle( | ||
| "labels:create", | ||
| async ( | ||
| _, | ||
| { accountId, name }: { accountId: string; name: string }, | ||
| ): Promise<IpcResponse<LabelInfo>> => { | ||
| try { | ||
| const client = await getClient(accountId); | ||
| const label = await client.createLabel(name); | ||
| return { success: true, data: label }; | ||
| } catch (error) { | ||
| log.error({ err: error }, "[Labels] Failed to create label"); | ||
| return { success: false, error: String(error) }; | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| // Modify labels on a single message | ||
| ipcMain.handle( | ||
| "labels:modify-message", | ||
| async ( | ||
| _, | ||
| { | ||
| accountId, | ||
| emailId, | ||
| addLabelIds, | ||
| removeLabelIds, | ||
| }: { | ||
| accountId: string; | ||
| emailId: string; | ||
| addLabelIds: string[]; | ||
| removeLabelIds: string[]; | ||
| }, | ||
| ): Promise<IpcResponse<{ labelIds: string[] }>> => { | ||
| try { | ||
| const client = await getClient(accountId); | ||
| await client.modifyMessageLabels(emailId, addLabelIds, removeLabelIds); | ||
|
|
||
| // Read back the updated message to get authoritative labelIds | ||
| const msg = await client.readEmail(emailId); | ||
| const newLabelIds = msg?.labelIds ?? []; | ||
|
|
||
| // Update local DB | ||
| updateEmailLabelIds(emailId, newLabelIds); | ||
|
|
||
| return { success: true, data: { labelIds: newLabelIds } }; | ||
| } catch (error) { | ||
| log.error({ err: error }, "Failed to modify message labels"); | ||
| return { success: false, error: String(error) }; | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| // Modify labels on all messages in a thread | ||
| ipcMain.handle( | ||
| "labels:modify-thread", | ||
| async ( | ||
| _, | ||
| { | ||
| accountId, | ||
| threadId, | ||
| addLabelIds, | ||
| removeLabelIds, | ||
| }: { | ||
| accountId: string; | ||
| threadId: string; | ||
| addLabelIds: string[]; | ||
| removeLabelIds: string[]; | ||
| }, | ||
| ): Promise<IpcResponse<void>> => { | ||
| try { | ||
| const client = await getClient(accountId); | ||
| await client.modifyThreadLabels(threadId, addLabelIds, removeLabelIds); | ||
|
|
||
| // Read back authoritative labelIds from Gmail for each thread message (parallel) | ||
| const threadEmails = getEmailsByThread(threadId, accountId); | ||
| await Promise.all( | ||
| threadEmails.map(async (email) => { | ||
| const msg = await client.readEmail(email.id); | ||
| updateEmailLabelIds(email.id, msg?.labelIds ?? []); | ||
| }), | ||
| ); | ||
|
|
||
| return { success: true, data: undefined }; | ||
| } catch (error) { | ||
| log.error({ err: error }, "Failed to modify thread labels"); | ||
| return { success: false, error: String(error) }; | ||
| } | ||
| }, | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
🟡
updateEmailLabelIdsnot scoped byaccountId, inconsistent with other DB operationsThe
labels:modify-messageandlabels:modify-threadIPC handlers both haveaccountIdavailable but callupdateEmailLabelIds(emailId, newLabelIds)which only filters byWHERE id = ?(src/main/db/index.ts:610). In contrast,deleteEmailatsrc/main/db/index.ts:617correctly filters byWHERE id = ? AND account_id = ?. In a multi-account setup, if two Gmail accounts happen to share a message ID, this UPDATE would modify emails belonging to the wrong account. The fix should threadaccountIdthrough toupdateEmailLabelIdsand addAND account_id = ?to the WHERE clause.Prompt for agents
Was this helpful? React with 👍 or 👎 to provide feedback.
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.
Valid observation, but this is a pre-existing issue in
updateEmailLabelIds(defined atsrc/main/db/index.ts:610). The function signature is(emailId, labelIds)with noaccountIdparameter, and the WHERE clause isWHERE id = ?. Fixing it would mean changing the shared DB function signature, which is out of scope for this PR. Noting for a follow-up.