Skip to content
This repository was archived by the owner on Apr 13, 2026. It is now read-only.

Commit 90ff17f

Browse files
authored
feat: expose store access to plugins and consolidate demo (#108)
1 parent 231613a commit 90ff17f

21 files changed

Lines changed: 307 additions & 229 deletions
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Plugin Store Access via RendererApp
2+
3+
**Date:** 2026-02-10
4+
5+
## Context
6+
7+
Plugin components (Sidebar Panel and Content Panel) need access to the current repository path information (`repoPath` and `worktreePath`) in order to:
8+
9+
1. Call backend APIs that require a `cwd` parameter
10+
2. Display repository information in the UI
11+
3. Adjust plugin behavior based on the current repository context
12+
13+
Currently, plugin components only receive `app: RendererApp` as a prop. Repository and workspace information lives in the Zustand store (`selectedRepoPath`, `workspaces[id].worktreePath`), but plugins have no official API to access it. Built-in components (FileTree, GitPanel, SearchPanel) directly import `useStore`, which is not appropriate for plugins.
14+
15+
## Discussion
16+
17+
Four approaches were evaluated:
18+
19+
| Approach | Verdict | Reason |
20+
|----------|---------|--------|
21+
| **A: Add fields to Props** | Rejected | Props would bloat as more context fields are needed over time |
22+
| **B: Custom hooks on RendererApp** | Rejected | Mounting React hooks on a class instance is unnatural |
23+
| **C: Plugins directly import store** | Rejected | Hard-couples plugins to internal module paths |
24+
| **D: Plugin Context + Provider** | Rejected | Premature abstraction — no third-party plugins exist yet, YAGNI |
25+
26+
A fifth approach emerged: expose the `useStore` reference on the `RendererApp` instance. This is a pragmatic middle ground — plugins access the store through the `app` prop they already receive, without importing internal modules or introducing new abstractions.
27+
28+
Key trade-off acknowledged: a React hook as a property on a class instance is slightly unconventional, but the `use` prefix signals hook calling rules, and all current plugins are first-party.
29+
30+
## Approach
31+
32+
Expose `useStore` as a property on `RendererApp`. Plugin components access it via `app.useStore(selector)`.
33+
34+
This is consistent with the existing `beforeRender` hook which already receives `{ store: typeof useStore }` during initialization. The same store reference is now available at component render time through the `app` prop.
35+
36+
## Architecture
37+
38+
### Change surface
39+
40+
1. **`RendererApp`** — Add a `useStore` property initialized in the constructor
41+
2. **Plugin component types** — No changes needed (`app` is already in `SidebarPanelProps` and `ContentPanelProps`)
42+
3. **Plugin rendering** — No changes needed (no new Provider or Context)
43+
44+
### Usage in plugin components
45+
46+
```tsx
47+
function MySidebar({ app }: SidebarPanelProps) {
48+
const repoPath = app.useStore((s) => s.selectedRepoPath);
49+
const workspace = app.useStore((s) =>
50+
s.selectedWorkspaceId ? s.workspaces[s.selectedWorkspaceId] : null
51+
);
52+
const cwd = workspace?.worktreePath;
53+
}
54+
```
55+
56+
### Future evolution
57+
58+
If third-party plugins or sandboxing are needed later, a Context/Provider layer can be added on top of this without breaking existing plugin code — `app.useStore` would simply point to a scoped store proxy instead of the global store.

src/renderer/core/app.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export class RendererApp {
4343
RendererPluginConfig
4444
>;
4545
readonly i18nManager: I18nManager;
46+
readonly useStore: typeof useStore;
4647

4748
/** Plugin UI contributions collected at startup */
4849
contributions: PluginConfigContribution[] = [];
@@ -60,9 +61,6 @@ export class RendererApp {
6061
* focuses the existing window instead of creating a duplicate. Different
6162
* `windowId` values with the same `windowType` open independent windows
6263
* that render the same component.
63-
* focuses the existing window instead of creating a duplicate. Different
64-
* `windowId` values with the same `windowType` open independent windows
65-
* that render the same component.
6664
*
6765
* Flow: `app.window.open(...)` → IPC → BrowserWindowManager creates
6866
* BrowserWindow with `?windowId=xxx&windowType=yyy` → renderer loads →
@@ -78,6 +76,7 @@ export class RendererApp {
7876
this.windows = options?.windows ?? [];
7977
this.pluginManager = new PluginManager(options?.plugins);
8078
this.i18nManager = new I18nManager();
79+
this.useStore = useStore;
8180
}
8281

8382
async start(): Promise<void> {
@@ -92,17 +91,16 @@ export class RendererApp {
9291
(r): r is LazyNamespaceConfig => r != null,
9392
);
9493

94+
// Hydrate store for both main and sub windows (locale, etc.)
95+
await hydrateStore(useStore);
96+
await this.i18nManager.init({ store: useStore });
97+
9598
// Sub window: render matched component directly
9699
if (windowConfig) {
97-
await this.i18nManager.init();
98100
this.i18nManager.setupLazyNamespaces(lazyNamespaceConfigs);
99101
this.render(lazy(windowConfig.componentLoader));
100102
return;
101103
}
102-
103-
// Main window: hydrate store first, then init everything else
104-
await hydrateStore(useStore);
105-
await this.i18nManager.init({ store: useStore });
106104
this.i18nManager.setupLazyNamespaces(lazyNamespaceConfigs);
107105

108106
// Collect plugin UI contributions

src/renderer/core/i18n/manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class I18nManager {
3939
async init(options: I18nInitOptions = {}): Promise<void> {
4040
const { store } = options;
4141

42-
// Get saved locale from store if available
42+
// Get locale from store or browser detection
4343
const savedLocale = store?.getState().locale;
4444

4545
await this.instance.init({

src/renderer/core/i18n/types.d.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
/**
2-
* i18n type safety for default namespace.
2+
* i18n type safety.
33
*
4-
* Plugins can add their own namespaces by creating a types.d.ts:
5-
* @see src/renderer/plugins/demo-i18n/types.d.ts
4+
* Core declares I18nResources inside i18next module with the default namespace.
5+
* Plugins augment I18nResources via `declare module 'i18next'` to add their namespaces.
6+
* @see src/renderer/plugins/demo/types.d.ts
67
*/
78
import 'i18next';
89
import type en from '../../locales/en.json';
910

1011
declare module 'i18next' {
12+
interface I18nResources {
13+
translation: typeof en;
14+
}
15+
1116
interface CustomTypeOptions {
1217
defaultNS: 'translation';
13-
resources: {
14-
translation: typeof en;
15-
};
18+
resources: I18nResources;
1619
}
1720
}

src/renderer/main.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import './index.css';
22
import { RendererApp } from './core';
3+
import { demoPlugin } from './plugins/demo';
34

45
const rendererApp = new RendererApp({
5-
plugins: [],
6-
windows: [],
6+
plugins: [demoPlugin],
7+
windows: [
8+
{
9+
windowType: 'demo',
10+
componentLoader: () => import('./plugins/demo/DemoWindow'),
11+
},
12+
],
713
});
814

915
rendererApp.start();

src/renderer/plugins/demo-i18n/locales/en-US.json

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/renderer/plugins/demo-i18n/locales/zh-CN.json

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/renderer/plugins/demo-i18n/plugin.ts

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/renderer/plugins/demo-i18n/types.d.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/renderer/plugins/demo-notes/NotesSidebar.tsx

Lines changed: 0 additions & 103 deletions
This file was deleted.

0 commit comments

Comments
 (0)