Skip to content

Feature updates#77

Open
peterjthomson wants to merge 7 commits into
masterfrom
feature/updates
Open

Feature updates#77
peterjthomson wants to merge 7 commits into
masterfrom
feature/updates

Conversation

@peterjthomson
Copy link
Copy Markdown
Owner

@peterjthomson peterjthomson commented Apr 21, 2026

Note

Medium Risk
Adds new main-process IPC and gh CLI execution paths plus a persistent macOS tray window, which can affect app stability and security boundaries if misconfigured. Most changes are additive but touch core app refresh/repo switching and new renderer/preload entrypoints.

Overview
Adds GitHub Issues integration end-to-end: new typed Issue models and IPC channels, main-process gh-backed issue operations (list/detail/comments/create/edit/close/reopen/comment/develop-branch), and new UI panels (IssueList, IssueDetailPanel) wired into the canvas system and app refresh flow.

Introduces a macOS menu-bar tray with a Quick Capture popover (separate renderer + preload + IPC handlers) to create issues quickly, optionally auto-capturing/retaking a screenshot, and shows an open-issue count badge via gh.

Includes small UX/refactor tweaks: centralizes repo-switch state reset/open logic in App, allows renaming custom canvas column labels in Settings, and replaces lucide-react Github icon usage with an inline GitHubIcon SVG.

Reviewed by Cursor Bugbot for commit 8ef0d50. Bugbot is set up for automated code reviews on this repo. Configure here.

claude and others added 7 commits January 14, 2026 22:48
Research and spec document for bringing GitHub Issues into Ledger.
Includes:
- Full gh CLI command reference and JSON output fields
- TypeScript interface definitions
- IPC API design following PR service patterns
- UI wireframes for list and detail panels
- Implementation plan with phased approach
- Integration points with branches, PRs, and commits
Research and incorporate design patterns from Taska (taska.now),
a native Mac app for GitHub/GitLab issues:

- Inline editing (title, assignees, labels on one line)
- Multi-select bulk edit with Inspector panel
- Priority as first-class citizen (P1-P4, Agile, Simple systems)
- Grouping by milestone/label/assignee
- Live search with debounced as-you-type
- Quick actions (checkbox to close, etc.)

Updated implementation plan to 5 phases with Taska-inspired features.
New feature for non-developers to quickly create GitHub issues:

- macOS menu bar icon with open issue count badge
- Click to open minimal popover with auto-screenshot
- Single text field for description, quick label/priority pickers
- Submit creates issue and shows notification
- Global keyboard shortcut (⌘+Shift+I)

Future extensibility:
- Linear tickets via linear CLI
- Notion pages via API
- Jira issues via CLI/API
- TicketProvider abstraction layer

Added Phase 6 & 7 to implementation plan and new files to create.
Add full GitHub Issues support following the existing PR implementation pattern:

- Service layer: issue functions in git-service.ts using gh CLI
- IPC handlers: registerIssueHandlers with conveyor typed IPC
- API layer: IssueApi class for renderer process
- UI components: IssueList and IssueDetailPanel
- Types: Issue, IssueDetail, IssueComment interfaces
- Canvas integration: issue handlers, data, and selection state

Features:
- List issues with state/search/sort filters
- View issue details with body, comments, linked PRs
- Close/reopen issues with reason selection
- Add comments to issues
- Create development branches from issues
- Priority detection from labels (P1-P4, agile systems)
- Label display with GitHub colors
Implement the Quick Capture feature from the Issues spec - a macOS menu bar
widget for fast issue creation:

Infrastructure:
- lib/main/tray.ts: Electron Tray with programmatic ticket icon, click handlers
- lib/main/quick-capture-window.ts: Popover BrowserWindow with vibrancy
- lib/services/quick-capture/: Service layer for screenshots and issue creation

IPC:
- lib/conveyor/handlers/quick-capture-handler.ts: IPC handlers for quick capture
- lib/preload/quick-capture-preload.ts: Minimal preload exposing quickCapture API

UI:
- app/quick-capture/QuickCaptureApp.tsx: React component with:
  - Auto-screenshot capture on open
  - Single text field (first line = title, rest = body)
  - Repository selector dropdown
  - Quick label/priority selection
  - Keyboard shortcuts (Cmd+Enter submit, Escape close)
- app/quick-capture/quick-capture.css: macOS-native styling with dark mode

Build:
- electron.vite.config.ts: Added quick-capture entries for preload and renderer

Features:
- Left-click tray icon: Opens quick capture popover
- Right-click: Context menu (Open Ledger, Refresh Count, Quit)
- Badge: Shows open issue count next to icon
- Auto-hide on blur (click outside)
@peterjthomson
Copy link
Copy Markdown
Owner Author

@claude review

@claude
Copy link
Copy Markdown

claude Bot commented Apr 21, 2026

Claude finished @peterjthomson's task in 2m 29s —— View job


PR Review — Feature Updates (GitHub Issues + Quick Capture Tray)

  • Gather context and understand request
  • Read core changed files
  • Review UI components
  • Review IPC wiring, types, schemas, and preload
  • Security, stability, and correctness analysis
  • Post final review

Overall this is a well-structured addition — clean service layer, proper context isolation for the tray window, good use of the conveyor/typed IPC pattern, and solid error handling throughout. A few issues need attention before merging, one of which is a real security bug.


🔴 Critical — Shell Injection in createQuickIssue

File: lib/services/quick-capture/quick-capture-service.ts:135

const { stdout } = await execAsync(
  `gh ${args.map((a) => `"${a.replace(/"/g, '\\"')}"`).join(' ')}`,
  { cwd: issue.repoPath }
)

This constructs a shell command string from user-controlled content (issue.description, issue.labels, issue.priority). Escaping only double-quotes leaves shell metacharacters like backticks, $(), !, and newlines exploitable. The rest of the codebase uses safeExec (which passes args as an array, bypassing the shell entirely) — this function is the only outlier.

The repoPath is also passed through from the renderer and used as cwd, not as a shell argument, so that part is fine. But the content args are not safe.

Fix: Replace execAsync with safeExec (already available in the project):

const result = await safeExec('gh', args, { cwd: issue.repoPath })
if (!result.success) throw new Error(result.stderr)
const url = result.stdout.trim()

Fix this →


🔴 Security — XSS via dangerouslySetInnerHTML in renderMarkdown

File: app/components/panels/editor/IssueDetailPanel.tsx:85-86

const processed = line.replace(/`([^`]+)`/g, '<code>$1</code>')
elements.push(<p key={i} dangerouslySetInnerHTML={{ __html: processed }} />)

The captured group $1 is inserted directly into HTML without escaping. A code span like `<img src=x onerror=alert(document.cookie)>` in an issue body would execute arbitrary JS. Issue bodies are user-controlled GitHub content, so this is a real XSS vector.

Fix: Escape HTML entities before substitution:

const escapeHtml = (s: string) =>
  s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')

const processed = line.replace(/`([^`]+)`/g, (_, code) => `<code>${escapeHtml(code)}</code>`)

Fix this →


🟠 Bug — editIssue Label/Assignee Clearing Broken

File: lib/services/issue/issue-service.ts:413-416

args.push('--remove-label', '')  // empty string — this won't work

gh issue edit --remove-label '' is not valid; the gh CLI expects an actual label name, not an empty string. This will either error silently or do nothing, leaving existing labels in place when the intent is to clear them. Same pattern affects assignees at line 424.

The correct approach is to read the current labels first and remove them individually, or use --remove-label '*' if supported by the installed gh version. Since reading-then-removing adds latency, it may be acceptable to document this limitation and skip the clear-all logic for now.

Fix this →


🟠 Bug — Screenshot Not Actually Uploaded to GitHub

File: lib/services/quick-capture/quick-capture-service.ts:109

body += `\n\n### Screenshot\n![](${screenshotPath})`

screenshotPath is a local filesystem path like /tmp/ledger-screenshots/screenshot-1234.png. The gh issue create --body command does not upload local image references — this will appear in the issue as a broken image link. Additionally, the temp file is not cleaned up if execAsync throws (the outer catch at line 159 returns early without running the cleanup block).

The getQuickLabels and getPriorityLabels functions in the same file also still use raw execAsync (lines 195, 221) — these are read-only so the injection risk is lower, but they're inconsistent with the rest of the codebase.


🟡 Code Duplication — LabelBadge and detectPriority

LabelBadge is defined identically in both IssueDetailPanel.tsx:25 and IssueList.tsx:97. detectPriority is implemented in issue-service.ts:53 (exported) and again in IssueList.tsx:42 (local copy with a slightly different return type).

Both should live in a shared utility — e.g., app/components/panels/list/issue-utils.ts — and be imported in both places.


🟡 Dead Code — getRecentRepos Stub in Quick Capture Service

File: lib/services/quick-capture/quick-capture-service.ts:184-188

export async function getRecentRepos(): Promise<QuickCaptureRepo[]> {
  // For now, return an empty array - will be wired up when integrated
  return []
}

This function is exported but the actual implementation is in the handler (quick-capture-handler.ts:53-65) which calls repoManager.getRecentRepositories() directly. The stub is never called. Either implement it or remove it to avoid confusion.


🟡 Privacy — Auto-Screenshot Default

File: lib/services/quick-capture/quick-capture-types.ts:83

autoScreenshot: true,

Auto-capturing the entire primary display on popover open (without a delay) may surprise users, especially on machines where sensitive content is visible. Consider defaulting to false and making it opt-in, or at minimum adding a brief delay (the screenshotDelay setting exists but defaults to 0).


🟡 Stability — Tray Global State Guard

File: lib/main/tray.ts:14-16

let tray: Tray | null = null
let quickCaptureWindow: BrowserWindow | null = null

createTray() doesn't guard against being called twice. If app.whenReady fires more than once in some code path, you'd leak a tray instance. Add a guard:

export function createTray(): Tray | null {
  if (tray) return tray  // already created
  // ...
}

🟢 Positive Notes

  • Correct security defaults on the Quick Capture BrowserWindow: sandbox: true, contextIsolation: true, nodeIntegration: false — good.
  • safeExec is used consistently across issue-service.ts — the createQuickIssue issue is the only exception.
  • Good error mapping via mapGhError — surfaces gh not installed and auth errors clearly.
  • destroyTray on will-quit — correct cleanup pattern.
  • Minimal preload surface for Quick Capture — only exposes what's needed, no bleed from main app's API.
  • The conveyor/typed IPC approach for issue handlers is clean and consistent with the rest of the codebase.

Summary

Severity Issue
🔴 Critical Shell injection in createQuickIssue — use safeExec
🔴 Security XSS in renderMarkdown via unescaped dangerouslySetInnerHTML
🟠 Bug editIssue label/assignee clearing (--remove-label '') is invalid
🟠 Bug Screenshot local path embedded in issue body won't upload; leaked temp file on error
🟡 Quality LabelBadge + detectPriority duplicated; getRecentRepos stub; tray guard missing
🟡 Privacy autoScreenshot: true by default captures screen on every popover open

The two 🔴 issues should be fixed before merging. The 🟠 bugs are functional regressions that affect the screenshot and label-editing features. The 🟡 items are lower priority but worth addressing.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8ef0d50. Configure here.

} else {
// Inline code
const processed = line.replace(/`([^`]+)`/g, '<code>$1</code>')
elements.push(<p key={i} dangerouslySetInnerHTML={{ __html: processed }} />)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XSS vulnerability via dangerouslySetInnerHTML in markdown renderer

High Severity

The renderMarkdown function inserts user-controlled content (issue bodies and comments from GitHub) using dangerouslySetInnerHTML after only converting backtick-wrapped text to <code> tags. Any HTML in the line — such as <img src=x onerror=alert(1)> — is rendered as live HTML. In an Electron app this is especially dangerous as it could enable code execution via crafted issue or comment content.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8ef0d50. Configure here.

Comment thread lib/main/git-service.ts
let cmd = `gh issue create --title "${options.title.replace(/"/g, '\\"')}"`

if (options.body) {
cmd += ` --body "${options.body.replace(/"/g, '\\"')}"`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shell command injection via unescaped metacharacters in execAsync

High Severity

Multiple issue functions construct shell commands via string interpolation with execAsync (which uses child_process.exec, executing through a shell). Only double quotes are escaped (.replace(/"/g, '\\"')), but shell metacharacters like $(), backticks, and \ remain active inside double-quoted strings. Input containing $(command) would trigger command execution. This affects createIssue, editIssue, closeIssue, reopenIssue, and commentOnIssue.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8ef0d50. Configure here.

))}
{issue.labels.length > 4 && (
<span className="issue-labels-more">+{issue.labels.length - 4}</span>
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Label overflow count uses wrong array length

Low Severity

The "+N more labels" indicator uses issue.labels.length (the unfiltered total) for both the threshold check and the overflow count, but the displayed labels come from a filtered array that excludes the priority label. When a priority label exists, the overflow count is off — it may show "+N" when fewer (or zero) labels are actually hidden.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8ef0d50. Configure here.

} from 'lucide-react'
const GitHubIcon = ({ size = 24 }: { size?: number }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.17 6.839 9.49.5.092.682-.217.682-.482 0-.237-.009-.866-.013-1.7-2.782.604-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.463-1.11-1.463-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.115 2.504.337 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.167 22 16.418 22 12c0-5.523-4.477-10-10-10z"/></svg>
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate GitHubIcon component across two files

Low Severity

The GitHubIcon SVG component is defined identically in both PluginSettingsPanel.tsx and RepositoryManagerPanel.tsx (replacing the removed Github import from lucide-react). This duplicated component could be extracted into a shared icon utility to avoid maintaining two copies.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8ef0d50. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants