Implement auto-update functionality and enhance build configuration
- Integrated electron-updater for automatic updates, including IPC handlers for checking, downloading, and installing updates. - Updated electron-builder configuration to include entitlements and notarization scripts for macOS. - Enhanced README with installation instructions and updated features. - Added new components for update notifications and dialogs in the renderer. - Improved CI workflow permissions for better release management.
This commit is contained in:
parent
fc48f6e099
commit
540aefc3d3
22 changed files with 880 additions and 69 deletions
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
|
|
@ -6,6 +6,9 @@ on:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
package-mac:
|
package-mac:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
|
|
||||||
162
README.md
162
README.md
|
|
@ -1,70 +1,134 @@
|
||||||
# Claude Code Context
|
<p align="center">
|
||||||
|
<img src="resources/icons/png/128x128.png" alt="Claude Code Context" width="120" />
|
||||||
|
</p>
|
||||||
|
|
||||||
Desktop app for exploring Claude Code session context usage.
|
<h1 align="center">Claude Code Context</h1>
|
||||||
|
|
||||||
It helps you inspect session timelines, search across sessions, debug context injections (`CLAUDE.md`, mentioned files, tool outputs), and configure notification triggers.
|
<p align="center">
|
||||||
|
<strong>Stop guessing. See exactly what Claude is doing.</strong>
|
||||||
|
<br />
|
||||||
|
A desktop app that turns Claude Code's opaque session logs into a visual, searchable, actionable interface.
|
||||||
|
</p>
|
||||||
|
|
||||||
## Features
|
<p align="center">
|
||||||
- Repository/worktree-aware project grouping
|
<a href="https://github.com/matt1398/claude-code-context/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT" /></a>
|
||||||
- Session search with context snippets
|
<a href="#"><img src="https://img.shields.io/badge/platform-macOS%20%7C%20Windows-lightgrey.svg" alt="Platform" /></a>
|
||||||
- Structured conversation/chunk parsing from Claude JSONL logs
|
<a href="#"><img src="https://img.shields.io/badge/electron-40-47848F.svg?logo=electron" alt="Electron" /></a>
|
||||||
- Context usage inspection (CLAUDE.md + mentioned files + tool output)
|
<a href="#"><img src="https://img.shields.io/badge/react-18-61DAFB.svg?logo=react" alt="React" /></a>
|
||||||
- Native notifications with configurable trigger rules
|
<a href="#"><img src="https://img.shields.io/badge/typescript-5-3178C6.svg?logo=typescript" alt="TypeScript" /></a>
|
||||||
- Real-time updates from Claude session/todo file changes
|
</p>
|
||||||
|
|
||||||
## Tech Stack
|
<br />
|
||||||
- Electron + electron-vite
|
|
||||||
- React + TypeScript + Zustand
|
|
||||||
- Tailwind CSS
|
|
||||||
- Vitest + ESLint
|
|
||||||
|
|
||||||
## Requirements
|
<p align="center">
|
||||||
- Node.js 20+
|
<!-- TODO: Replace with actual demo GIF/video -->
|
||||||
- pnpm 10+
|
<img src="docs/assets/demo.gif" alt="Claude Code Context Demo" width="900" />
|
||||||
- macOS or Windows
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why This Exists
|
||||||
|
|
||||||
|
There are many GUI wrappers for Claude Code — Conductor, Craft Agents, Vibe Kanban, 1Code, ccswitch, and others. I tried them all. None of them solved the actual problem:
|
||||||
|
|
||||||
|
**They wrap Claude Code.** They inject their own prompts, add their own abstractions, and change how Claude behaves. If you love the terminal — and I do — you don't want that. You want Claude Code exactly as it is.
|
||||||
|
|
||||||
|
**They only show their own sessions.** Run something in the terminal? It doesn't exist in their UI. You can only see what was executed through *their* tool. The terminal and the GUI are two separate worlds.
|
||||||
|
|
||||||
|
**You can't debug what went wrong.** A session failed — but why? The context filled up too fast — but what consumed it? A subagent spawned 5 child agents — but what did they do? Even in the terminal, scrolling back through a long session to reconstruct what happened is nearly impossible.
|
||||||
|
|
||||||
|
**You can't monitor what matters.** Want to know when Claude reads `.env`? When a single tool call exceeds 4K tokens of context? When a teammate sends a shutdown request? You'd have to wire up hooks manually, every time, for every project.
|
||||||
|
|
||||||
|
**Claude Code Context takes a different approach.** It doesn't wrap or modify Claude Code at all. It reads the session logs that already exist on your machine (`~/.claude/`) and turns them into a rich, interactive interface — regardless of whether the session ran in the terminal, in an IDE, or through another tool.
|
||||||
|
|
||||||
|
> Zero configuration. No API keys. Works with every session you've ever run.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### :mag: Visible Context Tracking
|
||||||
|
|
||||||
|
See exactly what's eating your context window. The **Session Context Panel** breaks down token usage across 6 categories — CLAUDE.md files, @-mentioned files, tool outputs, extended thinking, team coordination, and user messages — so you can instantly identify what's consuming tokens and optimize your workflow.
|
||||||
|
|
||||||
|
### :hammer_and_wrench: Rich Tool Call Inspector
|
||||||
|
|
||||||
|
Every tool call is paired with its result in an expandable card. Specialized viewers render each tool natively:
|
||||||
|
- **Read** calls show syntax-highlighted code with line numbers
|
||||||
|
- **Edit** calls show inline diffs with added/removed highlighting
|
||||||
|
- **Bash** calls show command output
|
||||||
|
- **Subagent** calls show the full execution tree, expandable in-place
|
||||||
|
|
||||||
|
### :bell: Custom Notification Triggers
|
||||||
|
|
||||||
|
Define rules for when you want to be notified. Match on regex patterns, assign colors, and filter your inbox by trigger. Built-in triggers catch common errors out of the box; add your own for project-specific patterns.
|
||||||
|
|
||||||
|
### :busts_in_silhouette: Team & Subagent Visualization
|
||||||
|
|
||||||
|
When Claude uses multi-agent orchestration, see the full picture. Teammate messages render as color-coded cards. Subagent sessions are expandable inline with their own execution traces, metrics, and tool calls.
|
||||||
|
|
||||||
|
### :zap: Command Palette & Cross-Session Search
|
||||||
|
|
||||||
|
Hit **Cmd+K** for a Spotlight-style command palette. Search across all sessions in a project — results show context snippets with highlighted keywords. Navigate directly to the exact message.
|
||||||
|
|
||||||
|
### :bar_chart: Multi-Pane Layout
|
||||||
|
|
||||||
|
Open multiple sessions side-by-side. Drag-and-drop tabs between panes, split views, and compare sessions in parallel — like a proper IDE for your AI conversations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- **Node.js** 20+
|
||||||
|
- **pnpm** 10+
|
||||||
|
- macOS or Windows
|
||||||
|
|
||||||
|
### Install & Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
git clone https://github.com/matt1398/claude-code-context.git
|
||||||
|
cd claude-code-context
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Data Source
|
That's it. The app auto-discovers your Claude Code projects from `~/.claude/`.
|
||||||
The app reads Claude local data from:
|
|
||||||
- `~/.claude/projects/`
|
### Build for Distribution
|
||||||
- `~/.claude/todos/`
|
|
||||||
|
|
||||||
## Scripts
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dev # Run app in development
|
pnpm dist:mac # macOS (.dmg)
|
||||||
pnpm typecheck # TypeScript checks
|
pnpm dist:win # Windows (.exe)
|
||||||
pnpm lint # ESLint (no auto-fix)
|
pnpm dist # Both platforms
|
||||||
pnpm test # Unit tests
|
|
||||||
pnpm build # Electron/Vite production build
|
|
||||||
pnpm check # Full local quality gate
|
|
||||||
pnpm dist:mac # Package macOS app (electron-builder)
|
|
||||||
pnpm dist:win # Package Windows app (electron-builder)
|
|
||||||
pnpm dist # Package both targets
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Packaging and Release
|
---
|
||||||
- Packaging is configured with `electron-builder.yml`.
|
|
||||||
- CI workflow (`.github/workflows/ci.yml`) runs typecheck/lint/test/build on macOS + Windows.
|
|
||||||
- Release workflow (`.github/workflows/release.yml`) builds distributables on tags (`v*`).
|
|
||||||
- Code signing/notarization uses GitHub secrets:
|
|
||||||
- `CSC_LINK`, `CSC_KEY_PASSWORD`
|
|
||||||
- `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID` (macOS notarization)
|
|
||||||
|
|
||||||
## Security Notes
|
## Scripts
|
||||||
- IPC handlers validate IDs/inputs and apply strict path containment checks.
|
|
||||||
- File reads for context injection are constrained to project root and `~/.claude`.
|
| Command | Description |
|
||||||
- Sensitive credential path patterns are blocked.
|
|---------|-------------|
|
||||||
|
| `pnpm dev` | Development with hot reload |
|
||||||
|
| `pnpm build` | Production build |
|
||||||
|
| `pnpm typecheck` | TypeScript type checking |
|
||||||
|
| `pnpm lint:fix` | Lint and auto-fix |
|
||||||
|
| `pnpm test` | Run all tests |
|
||||||
|
| `pnpm test:watch` | Watch mode |
|
||||||
|
| `pnpm test:coverage` | Coverage report |
|
||||||
|
| `pnpm check` | Full quality gate (types + lint + test + build) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
See:
|
|
||||||
- `CONTRIBUTING.md`
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for development guidelines. Please read our [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||||
- `CODE_OF_CONDUCT.md`
|
|
||||||
- `SECURITY.md`
|
## Security
|
||||||
|
|
||||||
|
IPC handlers validate all inputs with strict path containment checks. File reads are constrained to the project root and `~/.claude`. Sensitive credential paths are blocked. See [SECURITY.md](SECURITY.md) for details.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
MIT (`LICENSE`)
|
|
||||||
|
[MIT](LICENSE)
|
||||||
|
|
|
||||||
12
build/entitlements.mac.plist
Normal file
12
build/entitlements.mac.plist
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -22,8 +22,9 @@ mac:
|
||||||
hardenedRuntime: true
|
hardenedRuntime: true
|
||||||
gatekeeperAssess: false
|
gatekeeperAssess: false
|
||||||
icon: resources/icons/mac/icon.icns
|
icon: resources/icons/mac/icon.icns
|
||||||
notarize:
|
entitlements: build/entitlements.mac.plist
|
||||||
teamId: ${env.APPLE_TEAM_ID}
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
|
afterSign: scripts/notarize.cjs
|
||||||
|
|
||||||
dmg:
|
dmg:
|
||||||
sign: false
|
sign: false
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@tanstack/react-virtual": "^3.10.8",
|
"@tanstack/react-virtual": "^3.10.8",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
|
"electron-updater": "^6.7.3",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"mdast-util-to-hast": "^13.2.1",
|
"mdast-util-to-hast": "^13.2.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ importers:
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^3.6.0
|
specifier: ^3.6.0
|
||||||
version: 3.6.0
|
version: 3.6.0
|
||||||
|
electron-updater:
|
||||||
|
specifier: ^6.7.3
|
||||||
|
version: 6.7.3
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.562.0
|
specifier: ^0.562.0
|
||||||
version: 0.562.0(react@18.3.1)
|
version: 0.562.0(react@18.3.1)
|
||||||
|
|
@ -1532,6 +1535,10 @@ packages:
|
||||||
resolution: {integrity: sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==}
|
resolution: {integrity: sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
builder-util-runtime@9.5.1:
|
||||||
|
resolution: {integrity: sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
builder-util@24.13.1:
|
builder-util@24.13.1:
|
||||||
resolution: {integrity: sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==}
|
resolution: {integrity: sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==}
|
||||||
|
|
||||||
|
|
@ -1830,6 +1837,9 @@ packages:
|
||||||
electron-to-chromium@1.5.267:
|
electron-to-chromium@1.5.267:
|
||||||
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
|
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
|
||||||
|
|
||||||
|
electron-updater@6.7.3:
|
||||||
|
resolution: {integrity: sha512-EgkT8Z9noqXKbwc3u5FkJA+r48jwZ5DTUiOkJMOTEEH//n5Am6wfQGz7nvSFEA2oIAMv9jRzn5JKTyWeSKOPgg==}
|
||||||
|
|
||||||
electron-vite@2.3.0:
|
electron-vite@2.3.0:
|
||||||
resolution: {integrity: sha512-lsN2FymgJlp4k6MrcsphGqZQ9fKRdJKasoaiwIrAewN1tapYI/KINLdfEL7n10LuF0pPSNf/IqjzZbB5VINctg==}
|
resolution: {integrity: sha512-lsN2FymgJlp4k6MrcsphGqZQ9fKRdJKasoaiwIrAewN1tapYI/KINLdfEL7n10LuF0pPSNf/IqjzZbB5VINctg==}
|
||||||
engines: {node: ^18.0.0 || >=20.0.0}
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
|
@ -2679,9 +2689,16 @@ packages:
|
||||||
lodash.difference@4.5.0:
|
lodash.difference@4.5.0:
|
||||||
resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==}
|
resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==}
|
||||||
|
|
||||||
|
lodash.escaperegexp@4.1.2:
|
||||||
|
resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==}
|
||||||
|
|
||||||
lodash.flatten@4.4.0:
|
lodash.flatten@4.4.0:
|
||||||
resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==}
|
resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==}
|
||||||
|
|
||||||
|
lodash.isequal@4.5.0:
|
||||||
|
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
|
||||||
|
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
|
||||||
|
|
||||||
lodash.isplainobject@4.0.6:
|
lodash.isplainobject@4.0.6:
|
||||||
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||||
|
|
||||||
|
|
@ -3634,6 +3651,9 @@ packages:
|
||||||
thenify@3.3.1:
|
thenify@3.3.1:
|
||||||
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
||||||
|
|
||||||
|
tiny-typed-emitter@2.1.0:
|
||||||
|
resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==}
|
||||||
|
|
||||||
tinybench@2.9.0:
|
tinybench@2.9.0:
|
||||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||||
|
|
||||||
|
|
@ -5308,6 +5328,13 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
builder-util-runtime@9.5.1:
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.3
|
||||||
|
sax: 1.4.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
builder-util@24.13.1:
|
builder-util@24.13.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
7zip-bin: 5.2.0
|
7zip-bin: 5.2.0
|
||||||
|
|
@ -5653,6 +5680,19 @@ snapshots:
|
||||||
|
|
||||||
electron-to-chromium@1.5.267: {}
|
electron-to-chromium@1.5.267: {}
|
||||||
|
|
||||||
|
electron-updater@6.7.3:
|
||||||
|
dependencies:
|
||||||
|
builder-util-runtime: 9.5.1
|
||||||
|
fs-extra: 10.1.0
|
||||||
|
js-yaml: 4.1.1
|
||||||
|
lazy-val: 1.0.5
|
||||||
|
lodash.escaperegexp: 4.1.2
|
||||||
|
lodash.isequal: 4.5.0
|
||||||
|
semver: 7.7.3
|
||||||
|
tiny-typed-emitter: 2.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
electron-vite@2.3.0(vite@5.4.21(@types/node@25.0.7)):
|
electron-vite@2.3.0(vite@5.4.21(@types/node@25.0.7)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.6
|
'@babel/core': 7.28.6
|
||||||
|
|
@ -6757,8 +6797,12 @@ snapshots:
|
||||||
|
|
||||||
lodash.difference@4.5.0: {}
|
lodash.difference@4.5.0: {}
|
||||||
|
|
||||||
|
lodash.escaperegexp@4.1.2: {}
|
||||||
|
|
||||||
lodash.flatten@4.4.0: {}
|
lodash.flatten@4.4.0: {}
|
||||||
|
|
||||||
|
lodash.isequal@4.5.0: {}
|
||||||
|
|
||||||
lodash.isplainobject@4.0.6: {}
|
lodash.isplainobject@4.0.6: {}
|
||||||
|
|
||||||
lodash.merge@4.6.2: {}
|
lodash.merge@4.6.2: {}
|
||||||
|
|
@ -8017,6 +8061,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
any-promise: 1.3.0
|
any-promise: 1.3.0
|
||||||
|
|
||||||
|
tiny-typed-emitter@2.1.0: {}
|
||||||
|
|
||||||
tinybench@2.9.0: {}
|
tinybench@2.9.0: {}
|
||||||
|
|
||||||
tinyexec@0.3.2: {}
|
tinyexec@0.3.2: {}
|
||||||
|
|
|
||||||
22
scripts/notarize.cjs
Normal file
22
scripts/notarize.cjs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
const { notarize } = require('@electron/notarize');
|
||||||
|
|
||||||
|
exports.default = async function notarizing(context) {
|
||||||
|
const { electronPlatformName, appOutDir } = context;
|
||||||
|
if (electronPlatformName !== 'darwin') return;
|
||||||
|
|
||||||
|
if (!process.env.APPLE_ID || !process.env.APPLE_APP_SPECIFIC_PASSWORD || !process.env.APPLE_TEAM_ID) {
|
||||||
|
console.log('Skipping notarization: Apple credentials not set');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appName = context.packager.appInfo.productFilename;
|
||||||
|
|
||||||
|
return await notarize({
|
||||||
|
tool: 'notarytool',
|
||||||
|
appBundleId: 'com.claudecode.context',
|
||||||
|
appPath: `${appOutDir}/${appName}.app`,
|
||||||
|
appleId: process.env.APPLE_ID,
|
||||||
|
appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,
|
||||||
|
teamId: process.env.APPLE_TEAM_ID,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -44,6 +44,7 @@ import {
|
||||||
ProjectScanner,
|
ProjectScanner,
|
||||||
SessionParser,
|
SessionParser,
|
||||||
SubagentResolver,
|
SubagentResolver,
|
||||||
|
UpdaterService,
|
||||||
} from './services';
|
} from './services';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -60,6 +61,7 @@ let chunkBuilder: ChunkBuilder;
|
||||||
let dataCache: DataCache;
|
let dataCache: DataCache;
|
||||||
let fileWatcher: FileWatcher;
|
let fileWatcher: FileWatcher;
|
||||||
let notificationManager: NotificationManager;
|
let notificationManager: NotificationManager;
|
||||||
|
let updaterService: UpdaterService;
|
||||||
let cleanupInterval: NodeJS.Timeout | null = null;
|
let cleanupInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -75,11 +77,19 @@ function initializeServices(): void {
|
||||||
chunkBuilder = new ChunkBuilder();
|
chunkBuilder = new ChunkBuilder();
|
||||||
const disableCache = process.env.CLAUDE_CONTEXT_DISABLE_CACHE === '1';
|
const disableCache = process.env.CLAUDE_CONTEXT_DISABLE_CACHE === '1';
|
||||||
dataCache = new DataCache(MAX_CACHE_SESSIONS, CACHE_TTL_MINUTES, !disableCache);
|
dataCache = new DataCache(MAX_CACHE_SESSIONS, CACHE_TTL_MINUTES, !disableCache);
|
||||||
|
updaterService = new UpdaterService();
|
||||||
|
|
||||||
logger.info(`Projects directory: ${projectScanner.getProjectsDir()}`);
|
logger.info(`Projects directory: ${projectScanner.getProjectsDir()}`);
|
||||||
|
|
||||||
// Initialize IPC handlers
|
// Initialize IPC handlers
|
||||||
initializeIpcHandlers(projectScanner, sessionParser, subagentResolver, chunkBuilder, dataCache);
|
initializeIpcHandlers(
|
||||||
|
projectScanner,
|
||||||
|
sessionParser,
|
||||||
|
subagentResolver,
|
||||||
|
chunkBuilder,
|
||||||
|
dataCache,
|
||||||
|
updaterService
|
||||||
|
);
|
||||||
|
|
||||||
// Initialize notification manager using singleton pattern
|
// Initialize notification manager using singleton pattern
|
||||||
// This ensures IPC handlers and FileWatcher use the same instance
|
// This ensures IPC handlers and FileWatcher use the same instance
|
||||||
|
|
@ -171,10 +181,12 @@ function createWindow(): void {
|
||||||
void mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
|
void mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set traffic light position + notify renderer on first load
|
// Set traffic light position + notify renderer on first load, and auto-check for updates
|
||||||
mainWindow.webContents.on('did-finish-load', () => {
|
mainWindow.webContents.on('did-finish-load', () => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
syncTrafficLightPosition(mainWindow);
|
syncTrafficLightPosition(mainWindow);
|
||||||
|
// Auto-check for updates 3 seconds after window loads
|
||||||
|
setTimeout(() => updaterService.checkForUpdates(), 3000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -214,10 +226,13 @@ function createWindow(): void {
|
||||||
|
|
||||||
mainWindow.on('closed', () => {
|
mainWindow.on('closed', () => {
|
||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
// Clear main window reference from notification manager
|
// Clear main window references
|
||||||
if (notificationManager) {
|
if (notificationManager) {
|
||||||
notificationManager.setMainWindow(null);
|
notificationManager.setMainWindow(null);
|
||||||
}
|
}
|
||||||
|
if (updaterService) {
|
||||||
|
updaterService.setMainWindow(null);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle renderer process crashes (render-process-gone replaces deprecated 'crashed' event)
|
// Handle renderer process crashes (render-process-gone replaces deprecated 'crashed' event)
|
||||||
|
|
@ -226,10 +241,13 @@ function createWindow(): void {
|
||||||
// Could show an error dialog or attempt to reload the window
|
// Could show an error dialog or attempt to reload the window
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set main window reference for notification manager
|
// Set main window reference for notification manager and updater
|
||||||
if (notificationManager) {
|
if (notificationManager) {
|
||||||
notificationManager.setMainWindow(mainWindow);
|
notificationManager.setMainWindow(mainWindow);
|
||||||
}
|
}
|
||||||
|
if (updaterService) {
|
||||||
|
updaterService.setMainWindow(mainWindow);
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('Main window created');
|
logger.info('Main window created');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,11 @@ import {
|
||||||
registerSubagentHandlers,
|
registerSubagentHandlers,
|
||||||
removeSubagentHandlers,
|
removeSubagentHandlers,
|
||||||
} from './subagents';
|
} from './subagents';
|
||||||
|
import {
|
||||||
|
initializeUpdaterHandlers,
|
||||||
|
registerUpdaterHandlers,
|
||||||
|
removeUpdaterHandlers,
|
||||||
|
} from './updater';
|
||||||
import { registerUtilityHandlers, removeUtilityHandlers } from './utility';
|
import { registerUtilityHandlers, removeUtilityHandlers } from './utility';
|
||||||
import { registerValidationHandlers, removeValidationHandlers } from './validation';
|
import { registerValidationHandlers, removeValidationHandlers } from './validation';
|
||||||
|
|
||||||
|
|
@ -44,6 +49,7 @@ import type {
|
||||||
ProjectScanner,
|
ProjectScanner,
|
||||||
SessionParser,
|
SessionParser,
|
||||||
SubagentResolver,
|
SubagentResolver,
|
||||||
|
UpdaterService,
|
||||||
} from '../services';
|
} from '../services';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -54,13 +60,15 @@ export function initializeIpcHandlers(
|
||||||
parser: SessionParser,
|
parser: SessionParser,
|
||||||
resolver: SubagentResolver,
|
resolver: SubagentResolver,
|
||||||
builder: ChunkBuilder,
|
builder: ChunkBuilder,
|
||||||
cache: DataCache
|
cache: DataCache,
|
||||||
|
updater: UpdaterService
|
||||||
): void {
|
): void {
|
||||||
// Initialize domain handlers with their required services
|
// Initialize domain handlers with their required services
|
||||||
initializeProjectHandlers(scanner);
|
initializeProjectHandlers(scanner);
|
||||||
initializeSessionHandlers(scanner, parser, resolver, builder, cache);
|
initializeSessionHandlers(scanner, parser, resolver, builder, cache);
|
||||||
initializeSearchHandlers(scanner);
|
initializeSearchHandlers(scanner);
|
||||||
initializeSubagentHandlers(builder, cache, parser, resolver);
|
initializeSubagentHandlers(builder, cache, parser, resolver);
|
||||||
|
initializeUpdaterHandlers(updater);
|
||||||
|
|
||||||
// Register all handlers
|
// Register all handlers
|
||||||
registerProjectHandlers(ipcMain);
|
registerProjectHandlers(ipcMain);
|
||||||
|
|
@ -71,6 +79,7 @@ export function initializeIpcHandlers(
|
||||||
registerUtilityHandlers(ipcMain);
|
registerUtilityHandlers(ipcMain);
|
||||||
registerNotificationHandlers(ipcMain);
|
registerNotificationHandlers(ipcMain);
|
||||||
registerConfigHandlers(ipcMain);
|
registerConfigHandlers(ipcMain);
|
||||||
|
registerUpdaterHandlers(ipcMain);
|
||||||
|
|
||||||
logger.info('All handlers registered');
|
logger.info('All handlers registered');
|
||||||
}
|
}
|
||||||
|
|
@ -88,6 +97,7 @@ export function removeIpcHandlers(): void {
|
||||||
removeUtilityHandlers(ipcMain);
|
removeUtilityHandlers(ipcMain);
|
||||||
removeNotificationHandlers(ipcMain);
|
removeNotificationHandlers(ipcMain);
|
||||||
removeConfigHandlers(ipcMain);
|
removeConfigHandlers(ipcMain);
|
||||||
|
removeUpdaterHandlers(ipcMain);
|
||||||
|
|
||||||
logger.info('All handlers removed');
|
logger.info('All handlers removed');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
75
src/main/ipc/updater.ts
Normal file
75
src/main/ipc/updater.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
/**
|
||||||
|
* IPC Handlers for Update Operations.
|
||||||
|
*
|
||||||
|
* Handlers:
|
||||||
|
* - updater:check: Check for available updates
|
||||||
|
* - updater:download: Download the available update
|
||||||
|
* - updater:install: Quit and install the downloaded update
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getErrorMessage } from '@shared/utils/errorHandling';
|
||||||
|
import { createLogger } from '@shared/utils/logger';
|
||||||
|
import { type IpcMain, type IpcMainInvokeEvent } from 'electron';
|
||||||
|
|
||||||
|
import type { UpdaterService } from '../services';
|
||||||
|
|
||||||
|
const logger = createLogger('IPC:updater');
|
||||||
|
|
||||||
|
let updaterService: UpdaterService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes updater handlers with the service instance.
|
||||||
|
*/
|
||||||
|
export function initializeUpdaterHandlers(service: UpdaterService): void {
|
||||||
|
updaterService = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers all updater-related IPC handlers.
|
||||||
|
*/
|
||||||
|
export function registerUpdaterHandlers(ipcMain: IpcMain): void {
|
||||||
|
ipcMain.handle('updater:check', handleCheck);
|
||||||
|
ipcMain.handle('updater:download', handleDownload);
|
||||||
|
ipcMain.handle('updater:install', handleInstall);
|
||||||
|
|
||||||
|
logger.info('Updater handlers registered');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all updater IPC handlers.
|
||||||
|
*/
|
||||||
|
export function removeUpdaterHandlers(ipcMain: IpcMain): void {
|
||||||
|
ipcMain.removeHandler('updater:check');
|
||||||
|
ipcMain.removeHandler('updater:download');
|
||||||
|
ipcMain.removeHandler('updater:install');
|
||||||
|
|
||||||
|
logger.info('Updater handlers removed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Handler Implementations
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
async function handleCheck(_event: IpcMainInvokeEvent): Promise<void> {
|
||||||
|
try {
|
||||||
|
await updaterService.checkForUpdates();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in updater:check:', getErrorMessage(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDownload(_event: IpcMainInvokeEvent): Promise<void> {
|
||||||
|
try {
|
||||||
|
await updaterService.downloadUpdate();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in updater:download:', getErrorMessage(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInstall(_event: IpcMainInvokeEvent): void {
|
||||||
|
try {
|
||||||
|
updaterService.quitAndInstall();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in updater:install:', getErrorMessage(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
118
src/main/services/infrastructure/UpdaterService.ts
Normal file
118
src/main/services/infrastructure/UpdaterService.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
/**
|
||||||
|
* UpdaterService - Wraps electron-updater's autoUpdater for OTA updates.
|
||||||
|
*
|
||||||
|
* Forwards update lifecycle events to the renderer via IPC.
|
||||||
|
* Auto-download is disabled so users must confirm before downloading.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getErrorMessage } from '@shared/utils/errorHandling';
|
||||||
|
import { createLogger } from '@shared/utils/logger';
|
||||||
|
import electronUpdater from 'electron-updater';
|
||||||
|
|
||||||
|
const { autoUpdater } = electronUpdater;
|
||||||
|
|
||||||
|
import type { UpdaterStatus } from '@shared/types';
|
||||||
|
import type { BrowserWindow } from 'electron';
|
||||||
|
|
||||||
|
const logger = createLogger('UpdaterService');
|
||||||
|
|
||||||
|
export class UpdaterService {
|
||||||
|
private mainWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
autoUpdater.autoDownload = false;
|
||||||
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
|
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the main window reference for sending status events.
|
||||||
|
*/
|
||||||
|
setMainWindow(window: BrowserWindow | null): void {
|
||||||
|
this.mainWindow = window;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for available updates.
|
||||||
|
*/
|
||||||
|
async checkForUpdates(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await autoUpdater.checkForUpdates();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Check for updates failed:', getErrorMessage(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the available update.
|
||||||
|
*/
|
||||||
|
async downloadUpdate(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await autoUpdater.downloadUpdate();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Download update failed:', getErrorMessage(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quit the app and install the downloaded update.
|
||||||
|
*/
|
||||||
|
quitAndInstall(): void {
|
||||||
|
autoUpdater.quitAndInstall();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendStatus(status: UpdaterStatus): void {
|
||||||
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
|
this.mainWindow.webContents.send('updater:status', status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bindEvents(): void {
|
||||||
|
autoUpdater.on('checking-for-update', () => {
|
||||||
|
logger.info('Checking for update...');
|
||||||
|
this.sendStatus({ type: 'checking' });
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('update-available', (info) => {
|
||||||
|
logger.info('Update available:', info.version);
|
||||||
|
this.sendStatus({
|
||||||
|
type: 'available',
|
||||||
|
version: info.version,
|
||||||
|
releaseNotes: typeof info.releaseNotes === 'string' ? info.releaseNotes : undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('update-not-available', () => {
|
||||||
|
logger.info('No update available');
|
||||||
|
this.sendStatus({ type: 'not-available' });
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('download-progress', (progress) => {
|
||||||
|
this.sendStatus({
|
||||||
|
type: 'downloading',
|
||||||
|
progress: {
|
||||||
|
percent: progress.percent,
|
||||||
|
transferred: progress.transferred,
|
||||||
|
total: progress.total,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('update-downloaded', (info) => {
|
||||||
|
logger.info('Update downloaded:', info.version);
|
||||||
|
this.sendStatus({
|
||||||
|
type: 'downloaded',
|
||||||
|
version: info.version,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('error', (error) => {
|
||||||
|
logger.error('Updater error:', getErrorMessage(error));
|
||||||
|
this.sendStatus({
|
||||||
|
type: 'error',
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,3 +14,4 @@ export * from './DataCache';
|
||||||
export * from './FileWatcher';
|
export * from './FileWatcher';
|
||||||
export * from './NotificationManager';
|
export * from './NotificationManager';
|
||||||
export * from './TriggerManager';
|
export * from './TriggerManager';
|
||||||
|
export * from './UpdaterService';
|
||||||
|
|
|
||||||
|
|
@ -58,3 +58,19 @@ export const CONFIG_PIN_SESSION = 'config:pinSession';
|
||||||
|
|
||||||
/** Unpin a session */
|
/** Unpin a session */
|
||||||
export const CONFIG_UNPIN_SESSION = 'config:unpinSession';
|
export const CONFIG_UNPIN_SESSION = 'config:unpinSession';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Updater API Channels
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/** Check for updates */
|
||||||
|
export const UPDATER_CHECK = 'updater:check';
|
||||||
|
|
||||||
|
/** Download available update */
|
||||||
|
export const UPDATER_DOWNLOAD = 'updater:download';
|
||||||
|
|
||||||
|
/** Quit and install downloaded update */
|
||||||
|
export const UPDATER_INSTALL = 'updater:install';
|
||||||
|
|
||||||
|
/** Status event channel (main -> renderer) */
|
||||||
|
export const UPDATER_STATUS = 'updater:status';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
import { WINDOW_ZOOM_FACTOR_CHANGED_CHANNEL } from '@shared/constants';
|
import { WINDOW_ZOOM_FACTOR_CHANGED_CHANNEL } from '@shared/constants';
|
||||||
import { contextBridge, ipcRenderer } from 'electron';
|
import { contextBridge, ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
import {
|
||||||
|
UPDATER_CHECK,
|
||||||
|
UPDATER_DOWNLOAD,
|
||||||
|
UPDATER_INSTALL,
|
||||||
|
UPDATER_STATUS,
|
||||||
|
} from './constants/ipcChannels';
|
||||||
import {
|
import {
|
||||||
CONFIG_ADD_IGNORE_REGEX,
|
CONFIG_ADD_IGNORE_REGEX,
|
||||||
CONFIG_ADD_IGNORE_REPOSITORY,
|
CONFIG_ADD_IGNORE_REPOSITORY,
|
||||||
|
|
@ -285,6 +291,25 @@ const electronAPI: ElectronAPI = {
|
||||||
ipcRenderer.removeListener('todo-change', listener);
|
ipcRenderer.removeListener('todo-change', listener);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Updater API
|
||||||
|
updater: {
|
||||||
|
check: () => ipcRenderer.invoke(UPDATER_CHECK),
|
||||||
|
download: () => ipcRenderer.invoke(UPDATER_DOWNLOAD),
|
||||||
|
install: () => ipcRenderer.invoke(UPDATER_INSTALL),
|
||||||
|
onStatus: (callback: (event: unknown, status: unknown) => void): (() => void) => {
|
||||||
|
ipcRenderer.on(
|
||||||
|
UPDATER_STATUS,
|
||||||
|
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
|
||||||
|
);
|
||||||
|
return (): void => {
|
||||||
|
ipcRenderer.removeListener(
|
||||||
|
UPDATER_STATUS,
|
||||||
|
callback as (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use contextBridge to securely expose the API to the renderer process
|
// Use contextBridge to securely expose the API to the renderer process
|
||||||
|
|
|
||||||
71
src/renderer/components/common/UpdateBanner.tsx
Normal file
71
src/renderer/components/common/UpdateBanner.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
/**
|
||||||
|
* UpdateBanner - Slim top banner for download progress and restart prompt.
|
||||||
|
*
|
||||||
|
* Visible during download and after the update is ready to install.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useStore } from '@renderer/store';
|
||||||
|
import { CheckCircle, Loader2, X } from 'lucide-react';
|
||||||
|
|
||||||
|
export const UpdateBanner = (): React.JSX.Element | null => {
|
||||||
|
const showUpdateBanner = useStore((s) => s.showUpdateBanner);
|
||||||
|
const updateStatus = useStore((s) => s.updateStatus);
|
||||||
|
const downloadProgress = useStore((s) => s.downloadProgress);
|
||||||
|
const availableVersion = useStore((s) => s.availableVersion);
|
||||||
|
const installUpdate = useStore((s) => s.installUpdate);
|
||||||
|
const dismissUpdateBanner = useStore((s) => s.dismissUpdateBanner);
|
||||||
|
|
||||||
|
if (!showUpdateBanner || (updateStatus !== 'downloading' && updateStatus !== 'downloaded')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDownloading = updateStatus === 'downloading';
|
||||||
|
const percent = Math.round(downloadProgress);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative flex items-center gap-3 border-b px-4 py-2 text-sm"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--color-surface-raised)',
|
||||||
|
borderColor: 'var(--color-border)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isDownloading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="size-4 shrink-0 animate-spin text-blue-400" />
|
||||||
|
<span style={{ color: 'var(--color-text-secondary)' }}>
|
||||||
|
Downloading update... {percent}%
|
||||||
|
</span>
|
||||||
|
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-white/10">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full bg-blue-500 transition-all duration-300"
|
||||||
|
style={{ width: `${percent}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="size-4 shrink-0 text-green-400" />
|
||||||
|
<span style={{ color: 'var(--color-text-secondary)' }}>
|
||||||
|
Update ready{availableVersion ? ` (v${availableVersion})` : ''}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={installUpdate}
|
||||||
|
className="ml-auto rounded-md bg-green-600 px-3 py-1 text-xs font-medium text-white transition-colors hover:bg-green-500"
|
||||||
|
>
|
||||||
|
Restart to Update
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dismiss */}
|
||||||
|
<button
|
||||||
|
onClick={dismissUpdateBanner}
|
||||||
|
className="shrink-0 rounded p-0.5 transition-colors hover:bg-white/10"
|
||||||
|
style={{ color: 'var(--color-text-muted)' }}
|
||||||
|
>
|
||||||
|
<X className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
89
src/renderer/components/common/UpdateDialog.tsx
Normal file
89
src/renderer/components/common/UpdateDialog.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
/**
|
||||||
|
* UpdateDialog - Modal dialog shown when a new version is available.
|
||||||
|
*
|
||||||
|
* Prompts the user to download the update or dismiss it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useStore } from '@renderer/store';
|
||||||
|
import { Download, X } from 'lucide-react';
|
||||||
|
|
||||||
|
export const UpdateDialog = (): React.JSX.Element | null => {
|
||||||
|
const showUpdateDialog = useStore((s) => s.showUpdateDialog);
|
||||||
|
const availableVersion = useStore((s) => s.availableVersion);
|
||||||
|
const releaseNotes = useStore((s) => s.releaseNotes);
|
||||||
|
const downloadUpdate = useStore((s) => s.downloadUpdate);
|
||||||
|
const dismissUpdateDialog = useStore((s) => s.dismissUpdateDialog);
|
||||||
|
|
||||||
|
if (!showUpdateDialog) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||||
|
style={{ backgroundColor: 'rgba(0, 0, 0, 0.6)' }}
|
||||||
|
onClick={dismissUpdateDialog}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="relative mx-4 w-full max-w-md rounded-lg border p-6 shadow-xl"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--color-surface-overlay)',
|
||||||
|
borderColor: 'var(--color-border-emphasis)',
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Close button */}
|
||||||
|
<button
|
||||||
|
onClick={dismissUpdateDialog}
|
||||||
|
className="absolute right-3 top-3 rounded p-1 transition-colors hover:bg-white/10"
|
||||||
|
style={{ color: 'var(--color-text-muted)' }}
|
||||||
|
>
|
||||||
|
<X className="size-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h2 className="text-lg font-semibold" style={{ color: 'var(--color-text)' }}>
|
||||||
|
Update Available
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<p className="mt-2 text-sm" style={{ color: 'var(--color-text-secondary)' }}>
|
||||||
|
Version {availableVersion} is available. Would you like to download it?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Release notes */}
|
||||||
|
{releaseNotes && (
|
||||||
|
<div
|
||||||
|
className="mt-3 max-h-40 overflow-y-auto rounded border p-3 text-xs"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--color-surface)',
|
||||||
|
borderColor: 'var(--color-border)',
|
||||||
|
color: 'var(--color-text-muted)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{releaseNotes}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="mt-5 flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onClick={dismissUpdateDialog}
|
||||||
|
className="rounded-md border px-4 py-2 text-sm font-medium transition-colors hover:bg-white/5"
|
||||||
|
style={{
|
||||||
|
borderColor: 'var(--color-border)',
|
||||||
|
color: 'var(--color-text-secondary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Later
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={downloadUpdate}
|
||||||
|
className="flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-500"
|
||||||
|
>
|
||||||
|
<Download className="size-4" />
|
||||||
|
Download Update
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -10,6 +10,8 @@ import { getTrafficLightPaddingForZoom } from '@renderer/constants/layout';
|
||||||
import { useKeyboardShortcuts } from '@renderer/hooks/useKeyboardShortcuts';
|
import { useKeyboardShortcuts } from '@renderer/hooks/useKeyboardShortcuts';
|
||||||
import { useZoomFactor } from '@renderer/hooks/useZoomFactor';
|
import { useZoomFactor } from '@renderer/hooks/useZoomFactor';
|
||||||
|
|
||||||
|
import { UpdateBanner } from '../common/UpdateBanner';
|
||||||
|
import { UpdateDialog } from '../common/UpdateDialog';
|
||||||
import { CommandPalette } from '../search/CommandPalette';
|
import { CommandPalette } from '../search/CommandPalette';
|
||||||
|
|
||||||
import { PaneContainer } from './PaneContainer';
|
import { PaneContainer } from './PaneContainer';
|
||||||
|
|
@ -23,19 +25,23 @@ export const TabbedLayout = (): React.JSX.Element => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex h-screen bg-claude-dark-bg text-claude-dark-text"
|
className="flex h-screen flex-col bg-claude-dark-bg text-claude-dark-text"
|
||||||
style={
|
style={
|
||||||
{ '--macos-traffic-light-padding-left': `${trafficLightPadding}px` } as React.CSSProperties
|
{ '--macos-traffic-light-padding-left': `${trafficLightPadding}px` } as React.CSSProperties
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* Command Palette (Cmd+K) */}
|
<UpdateBanner />
|
||||||
<CommandPalette />
|
<div className="flex flex-1 overflow-hidden">
|
||||||
|
{/* Command Palette (Cmd+K) */}
|
||||||
|
<CommandPalette />
|
||||||
|
|
||||||
{/* Sidebar - Project dropdown + Sessions (280px) */}
|
{/* Sidebar - Project dropdown + Sessions (280px) */}
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
||||||
{/* Multi-pane content area */}
|
{/* Multi-pane content area */}
|
||||||
<PaneContainer />
|
<PaneContainer />
|
||||||
|
</div>
|
||||||
|
<UpdateDialog />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@
|
||||||
* AdvancedSection - Advanced settings including config management and about info.
|
* AdvancedSection - Advanced settings including config management and about info.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import appIcon from '@renderer/favicon.png';
|
import appIcon from '@renderer/favicon.png';
|
||||||
import { Code2, Download, RefreshCw, Upload } from 'lucide-react';
|
import { useStore } from '@renderer/store';
|
||||||
|
import { CheckCircle, Code2, Download, Loader2, RefreshCw, Upload } from 'lucide-react';
|
||||||
|
|
||||||
import { SettingsSectionHeader } from '../components';
|
import { SettingsSectionHeader } from '../components';
|
||||||
|
|
||||||
|
|
@ -25,11 +26,65 @@ export const AdvancedSection = ({
|
||||||
onOpenInEditor,
|
onOpenInEditor,
|
||||||
}: AdvancedSectionProps): React.JSX.Element => {
|
}: AdvancedSectionProps): React.JSX.Element => {
|
||||||
const [version, setVersion] = useState<string>('');
|
const [version, setVersion] = useState<string>('');
|
||||||
|
const updateStatus = useStore((s) => s.updateStatus);
|
||||||
|
const availableVersion = useStore((s) => s.availableVersion);
|
||||||
|
const checkForUpdates = useStore((s) => s.checkForUpdates);
|
||||||
|
|
||||||
|
// Auto-revert "not-available" / "error" status back to idle after a brief display
|
||||||
|
const revertTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
useEffect(() => {
|
||||||
|
if (updateStatus === 'not-available' || updateStatus === 'error') {
|
||||||
|
revertTimerRef.current = setTimeout(() => {
|
||||||
|
useStore.setState({ updateStatus: 'idle' });
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (revertTimerRef.current) clearTimeout(revertTimerRef.current);
|
||||||
|
};
|
||||||
|
}, [updateStatus]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.electronAPI.getAppVersion().then(setVersion).catch(console.error);
|
window.electronAPI.getAppVersion().then(setVersion).catch(console.error);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleCheckForUpdates = useCallback(() => {
|
||||||
|
checkForUpdates();
|
||||||
|
}, [checkForUpdates]);
|
||||||
|
|
||||||
|
const getUpdateButtonContent = (): React.JSX.Element => {
|
||||||
|
switch (updateStatus) {
|
||||||
|
case 'checking':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Loader2 className="size-3.5 animate-spin" />
|
||||||
|
Checking...
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case 'not-available':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="size-3.5" />
|
||||||
|
Up to date
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case 'available':
|
||||||
|
case 'downloaded':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Download className="size-3.5" />
|
||||||
|
{updateStatus === 'downloaded' ? 'Update ready' : `v${availableVersion} available`}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="size-3.5" />
|
||||||
|
Check for Updates
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SettingsSectionHeader title="Configuration" />
|
<SettingsSectionHeader title="Configuration" />
|
||||||
|
|
@ -87,9 +142,27 @@ export const AdvancedSection = ({
|
||||||
<div className="flex items-start gap-4 py-3">
|
<div className="flex items-start gap-4 py-3">
|
||||||
<img src={appIcon} alt="App Icon" className="size-10 rounded-lg" />
|
<img src={appIcon} alt="App Icon" className="size-10 rounded-lg" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
|
<div className="flex items-center gap-3">
|
||||||
Claude Code Context
|
<p className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
|
||||||
</p>
|
Claude Code Context
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={handleCheckForUpdates}
|
||||||
|
disabled={updateStatus === 'checking'}
|
||||||
|
className="flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium transition-colors hover:bg-white/5 disabled:opacity-50"
|
||||||
|
style={{
|
||||||
|
borderColor: 'var(--color-border)',
|
||||||
|
color:
|
||||||
|
updateStatus === 'not-available'
|
||||||
|
? 'var(--color-text-muted)'
|
||||||
|
: updateStatus === 'available' || updateStatus === 'downloaded'
|
||||||
|
? '#60a5fa'
|
||||||
|
: 'var(--color-text-secondary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getUpdateButtonContent()}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<p className="mt-0.5 text-xs" style={{ color: 'var(--color-text-muted)' }}>
|
<p className="mt-0.5 text-xs" style={{ color: 'var(--color-text-muted)' }}>
|
||||||
Version {version || '...'}
|
Version {version || '...'}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,11 @@ import { createSubagentSlice } from './slices/subagentSlice';
|
||||||
import { createTabSlice } from './slices/tabSlice';
|
import { createTabSlice } from './slices/tabSlice';
|
||||||
import { createTabUISlice } from './slices/tabUISlice';
|
import { createTabUISlice } from './slices/tabUISlice';
|
||||||
import { createUISlice } from './slices/uiSlice';
|
import { createUISlice } from './slices/uiSlice';
|
||||||
|
import { createUpdateSlice } from './slices/updateSlice';
|
||||||
|
|
||||||
import type { DetectedError } from '../types/data';
|
import type { DetectedError } from '../types/data';
|
||||||
import type { AppState } from './types';
|
import type { AppState } from './types';
|
||||||
|
import type { UpdaterStatus } from '@shared/types';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Store Creation
|
// Store Creation
|
||||||
|
|
@ -37,6 +39,7 @@ export const useStore = create<AppState>()((...args) => ({
|
||||||
...createUISlice(...args),
|
...createUISlice(...args),
|
||||||
...createNotificationSlice(...args),
|
...createNotificationSlice(...args),
|
||||||
...createConfigSlice(...args),
|
...createConfigSlice(...args),
|
||||||
|
...createUpdateSlice(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -226,6 +229,51 @@ export function initializeNotificationListeners(): () => void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen for updater status events from main process
|
||||||
|
if (window.electronAPI.updater?.onStatus) {
|
||||||
|
const cleanup = window.electronAPI.updater.onStatus((_event: unknown, status: unknown) => {
|
||||||
|
const s = status as UpdaterStatus;
|
||||||
|
switch (s.type) {
|
||||||
|
case 'checking':
|
||||||
|
useStore.setState({ updateStatus: 'checking' });
|
||||||
|
break;
|
||||||
|
case 'available':
|
||||||
|
useStore.setState({
|
||||||
|
updateStatus: 'available',
|
||||||
|
availableVersion: s.version ?? null,
|
||||||
|
releaseNotes: s.releaseNotes ?? null,
|
||||||
|
showUpdateDialog: true,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'not-available':
|
||||||
|
useStore.setState({ updateStatus: 'not-available' });
|
||||||
|
break;
|
||||||
|
case 'downloading':
|
||||||
|
useStore.setState({
|
||||||
|
updateStatus: 'downloading',
|
||||||
|
downloadProgress: s.progress?.percent ?? 0,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'downloaded':
|
||||||
|
useStore.setState({
|
||||||
|
updateStatus: 'downloaded',
|
||||||
|
downloadProgress: 100,
|
||||||
|
availableVersion: s.version ?? useStore.getState().availableVersion,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
useStore.setState({
|
||||||
|
updateStatus: 'error',
|
||||||
|
updateError: s.error ?? 'Unknown error',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (typeof cleanup === 'function') {
|
||||||
|
cleanupFns.push(cleanup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Return cleanup function
|
// Return cleanup function
|
||||||
return () => {
|
return () => {
|
||||||
for (const timer of pendingSessionRefreshTimers.values()) {
|
for (const timer of pendingSessionRefreshTimers.values()) {
|
||||||
|
|
|
||||||
82
src/renderer/store/slices/updateSlice.ts
Normal file
82
src/renderer/store/slices/updateSlice.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
/**
|
||||||
|
* Update slice - manages OTA auto-update state and actions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createLogger } from '@shared/utils/logger';
|
||||||
|
|
||||||
|
import type { AppState } from '../types';
|
||||||
|
import type { StateCreator } from 'zustand';
|
||||||
|
|
||||||
|
const logger = createLogger('Store:update');
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Slice Interface
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface UpdateSlice {
|
||||||
|
// State
|
||||||
|
updateStatus:
|
||||||
|
| 'idle'
|
||||||
|
| 'checking'
|
||||||
|
| 'available'
|
||||||
|
| 'not-available'
|
||||||
|
| 'downloading'
|
||||||
|
| 'downloaded'
|
||||||
|
| 'error';
|
||||||
|
availableVersion: string | null;
|
||||||
|
releaseNotes: string | null;
|
||||||
|
downloadProgress: number;
|
||||||
|
updateError: string | null;
|
||||||
|
showUpdateDialog: boolean;
|
||||||
|
showUpdateBanner: boolean;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
checkForUpdates: () => void;
|
||||||
|
downloadUpdate: () => void;
|
||||||
|
installUpdate: () => void;
|
||||||
|
dismissUpdateDialog: () => void;
|
||||||
|
dismissUpdateBanner: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Slice Creator
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const createUpdateSlice: StateCreator<AppState, [], [], UpdateSlice> = (set) => ({
|
||||||
|
// Initial state
|
||||||
|
updateStatus: 'idle',
|
||||||
|
availableVersion: null,
|
||||||
|
releaseNotes: null,
|
||||||
|
downloadProgress: 0,
|
||||||
|
updateError: null,
|
||||||
|
showUpdateDialog: false,
|
||||||
|
showUpdateBanner: false,
|
||||||
|
|
||||||
|
checkForUpdates: () => {
|
||||||
|
set({ updateStatus: 'checking', updateError: null });
|
||||||
|
window.electronAPI.updater.check().catch((error) => {
|
||||||
|
logger.error('Failed to check for updates:', error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadUpdate: () => {
|
||||||
|
set({ showUpdateDialog: false, showUpdateBanner: true, downloadProgress: 0 });
|
||||||
|
window.electronAPI.updater.download().catch((error) => {
|
||||||
|
logger.error('Failed to download update:', error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
installUpdate: () => {
|
||||||
|
window.electronAPI.updater.install().catch((error) => {
|
||||||
|
logger.error('Failed to install update:', error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
dismissUpdateDialog: () => {
|
||||||
|
set({ showUpdateDialog: false });
|
||||||
|
},
|
||||||
|
|
||||||
|
dismissUpdateBanner: () => {
|
||||||
|
set({ showUpdateBanner: false });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -15,6 +15,7 @@ import type { SubagentSlice } from './slices/subagentSlice';
|
||||||
import type { TabSlice } from './slices/tabSlice';
|
import type { TabSlice } from './slices/tabSlice';
|
||||||
import type { TabUISlice } from './slices/tabUISlice';
|
import type { TabUISlice } from './slices/tabUISlice';
|
||||||
import type { UISlice } from './slices/uiSlice';
|
import type { UISlice } from './slices/uiSlice';
|
||||||
|
import type { UpdateSlice } from './slices/updateSlice';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Shared Types
|
// Shared Types
|
||||||
|
|
@ -84,4 +85,5 @@ export type AppState = ProjectSlice &
|
||||||
PaneSlice &
|
PaneSlice &
|
||||||
UISlice &
|
UISlice &
|
||||||
NotificationSlice &
|
NotificationSlice &
|
||||||
ConfigSlice;
|
ConfigSlice &
|
||||||
|
UpdateSlice;
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,31 @@ export interface ClaudeMdFileInfo {
|
||||||
estimatedTokens: number;
|
estimatedTokens: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Updater API
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status payload sent from the main process updater to the renderer.
|
||||||
|
*/
|
||||||
|
export interface UpdaterStatus {
|
||||||
|
type: 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
|
||||||
|
version?: string;
|
||||||
|
releaseNotes?: string;
|
||||||
|
progress?: { percent: number; transferred: number; total: number };
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updater API exposed via preload.
|
||||||
|
*/
|
||||||
|
export interface UpdaterAPI {
|
||||||
|
check: () => Promise<void>;
|
||||||
|
download: () => Promise<void>;
|
||||||
|
install: () => Promise<void>;
|
||||||
|
onStatus: (callback: (event: unknown, status: unknown) => void) => () => void;
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Main Electron API
|
// Main Electron API
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -197,6 +222,9 @@ export interface ElectronAPI {
|
||||||
projectRoot?: string
|
projectRoot?: string
|
||||||
) => Promise<{ success: boolean; error?: string }>;
|
) => Promise<{ success: boolean; error?: string }>;
|
||||||
openExternal: (url: string) => Promise<{ success: boolean; error?: string }>;
|
openExternal: (url: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
|
|
||||||
|
// Updater API
|
||||||
|
updater: UpdaterAPI;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue