Skip to content

Commit 8c5e707

Browse files
Add new extensions: Export Conversation, Sticky Notes, and Word Counter
- Implemented Export Conversation extension to allow users to export active conversations as Markdown or plain text. - Created Sticky Notes extension for a persistent scratchpad that saves notes using extension settings. - Developed Word Counter extension to display live statistics for the active conversation, including message count, word count, and estimated token usage. - Each extension includes necessary manifest and entry point files, along with UI and functionality to interact with the conversation data.
1 parent 65ff4e0 commit 8c5e707

13 files changed

Lines changed: 1437 additions & 51 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@openconduit/core",
3-
"version": "2.0.0-beta.1",
3+
"version": "2.0.0-beta.2",
44
"description": "Shared UI components, stores, hooks, and service interface for OpenConduit",
55
"main": "src/index.ts",
66
"types": "src/index.ts",

src/components/MarketplaceSidebarPanel.tsx

Lines changed: 107 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ import { useRegistryStore } from '../stores/registryStore';
1313
import type { RegistryType, RegistryEntry } from '../stores/registryStore';
1414
import { loadInstalledExtensions } from '../extensions/loader';
1515

16+
// ─── Extension IPC bridge (minimal shape) ─────────────────────────────────────
17+
interface ExtensionsIpcApi {
18+
getInstalled: () => Promise<Array<{ id: string; version: string }>>;
19+
install: (id: string, downloadUrl: string) => Promise<{ success: boolean; error?: string }>;
20+
uninstall: (id: string) => Promise<void>;
21+
}
22+
type ExtApiWindow = Window & { api?: { extensions?: ExtensionsIpcApi } };
23+
1624
/** Returns true if `registryVer` is strictly greater than `installedVer` (semver). */
1725
function isNewer(registryVer: string | undefined, installedVer: string | undefined): boolean {
1826
if (!registryVer || !installedVer) return false;
@@ -78,52 +86,56 @@ function Badge({ label }: { label: string }) {
7886

7987
// ─── Type pill ────────────────────────────────────────────────────────────────
8088

81-
type TypeFilter = 'all' | 'provider' | 'mcp' | 'theme' | 'persona' | 'prompt' | 'profile';
89+
type TypeFilter = 'all' | 'provider' | 'mcp' | 'theme' | 'persona' | 'prompt' | 'profile' | 'extension';
8290

8391
const TYPE_PILLS: { id: TypeFilter; label: string }[] = [
84-
{ id: 'all', label: 'All' },
85-
{ id: 'provider', label: 'Providers' },
86-
{ id: 'mcp', label: 'MCP' },
87-
{ id: 'theme', label: 'Themes' },
88-
{ id: 'persona', label: 'Personas' },
89-
{ id: 'prompt', label: 'Prompts' },
90-
{ id: 'profile', label: 'Profiles' },
92+
{ id: 'all', label: 'All' },
93+
{ id: 'provider', label: 'Providers' },
94+
{ id: 'mcp', label: 'MCP' },
95+
{ id: 'theme', label: 'Themes' },
96+
{ id: 'persona', label: 'Personas' },
97+
{ id: 'prompt', label: 'Prompts' },
98+
{ id: 'profile', label: 'Profiles' },
99+
{ id: 'extension', label: 'Extensions' },
91100
];
92101

93102
/** Registry type → TypeFilter mapping */
94103
const _REGISTRY_TYPE_MAP: Record<RegistryType, TypeFilter> = {
95-
themes: 'theme',
96-
personas: 'persona',
97-
prompts: 'prompt',
98-
profiles: 'profile',
99-
providers: 'provider',
100-
mcp: 'mcp',
104+
themes: 'theme',
105+
personas: 'persona',
106+
prompts: 'prompt',
107+
profiles: 'profile',
108+
providers: 'provider',
109+
mcp: 'mcp',
110+
extensions: 'extension',
101111
};
102112

103113
// ─── Unified entry ────────────────────────────────────────────────────────────
104114

105115
type UnifiedEntry =
106-
| { kind: 'provider'; id: string; name: string; description: string; badge: string; installed: boolean; emoji?: string }
107-
| { kind: 'mcp'; id: string; name: string; description: string; badge?: string; installed: boolean; emoji: string; notes?: string }
108-
| { kind: 'theme'; id: string; name: string; description: string; author: string; verified: boolean; installed: boolean; hasUpdate: boolean; entry: RegistryEntry }
109-
| { kind: 'persona'; id: string; name: string; description: string; author: string; verified: boolean; installed: boolean; hasUpdate: boolean; entry: RegistryEntry }
110-
| { kind: 'prompt'; id: string; name: string; description: string; author: string; verified: boolean; installed: boolean; hasUpdate: boolean; entry: RegistryEntry }
111-
| { kind: 'profile'; id: string; name: string; description: string; author: string; verified: boolean; installed: boolean; hasUpdate: boolean; entry: RegistryEntry };
116+
| { kind: 'provider'; id: string; name: string; description: string; badge: string; installed: boolean; emoji?: string }
117+
| { kind: 'mcp'; id: string; name: string; description: string; badge?: string; installed: boolean; emoji: string; notes?: string }
118+
| { kind: 'theme'; id: string; name: string; description: string; author: string; verified: boolean; installed: boolean; hasUpdate: boolean; entry: RegistryEntry }
119+
| { kind: 'persona'; id: string; name: string; description: string; author: string; verified: boolean; installed: boolean; hasUpdate: boolean; entry: RegistryEntry }
120+
| { kind: 'prompt'; id: string; name: string; description: string; author: string; verified: boolean; installed: boolean; hasUpdate: boolean; entry: RegistryEntry }
121+
| { kind: 'profile'; id: string; name: string; description: string; author: string; verified: boolean; installed: boolean; hasUpdate: boolean; entry: RegistryEntry }
122+
| { kind: 'extension'; id: string; name: string; description: string; author: string; verified: boolean; installed: boolean; hasUpdate: boolean; entry: RegistryEntry };
112123

113124
// ─── Kind colour map ──────────────────────────────────────────────────────────
114125

115126
const KIND_COLORS: Record<string, string> = {
116-
provider: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/60 dark:text-indigo-300',
117-
mcp: 'bg-teal-100 text-teal-700 dark:bg-teal-900/60 dark:text-teal-300',
118-
theme: 'bg-pink-100 text-pink-700 dark:bg-pink-900/60 dark:text-pink-300',
119-
persona: 'bg-violet-100 text-violet-700 dark:bg-violet-900/60 dark:text-violet-300',
120-
prompt: 'bg-amber-100 text-amber-700 dark:bg-amber-900/60 dark:text-amber-300',
121-
profile: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/60 dark:text-cyan-300',
127+
provider: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/60 dark:text-indigo-300',
128+
mcp: 'bg-teal-100 text-teal-700 dark:bg-teal-900/60 dark:text-teal-300',
129+
theme: 'bg-pink-100 text-pink-700 dark:bg-pink-900/60 dark:text-pink-300',
130+
persona: 'bg-violet-100 text-violet-700 dark:bg-violet-900/60 dark:text-violet-300',
131+
prompt: 'bg-amber-100 text-amber-700 dark:bg-amber-900/60 dark:text-amber-300',
132+
profile: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/60 dark:text-cyan-300',
133+
extension: 'bg-orange-100 text-orange-700 dark:bg-orange-900/60 dark:text-orange-300',
122134
};
123135

124136
const KIND_LABELS: Record<string, string> = {
125137
provider: 'Provider', mcp: 'MCP', theme: 'Theme',
126-
persona: 'Persona', prompt: 'Prompt', profile: 'Profile',
138+
persona: 'Persona', prompt: 'Prompt', profile: 'Profile', extension: 'Extension',
127139
};
128140

129141
// ─── Verified checkmark ───────────────────────────────────────────────────────
@@ -244,6 +256,20 @@ export default function MarketplaceSidebarPanel() {
244256
const [query, setQuery] = useState('');
245257
const [typePill, setTypePill] = useState<TypeFilter>('all');
246258
const inputRef = useRef<HTMLInputElement>(null);
259+
260+
// Installed extension ids → installed version (refreshed on mount + after install/uninstall)
261+
const [installedExtMap, setInstalledExtMap] = useState<Map<string, string>>(new Map());
262+
263+
const refreshInstalledExt = async () => {
264+
const api = (window as ExtApiWindow).api;
265+
if (!api?.extensions?.getInstalled) return;
266+
try {
267+
const exts = await api.extensions.getInstalled();
268+
setInstalledExtMap(new Map(exts.map((e) => [e.id, e.version])));
269+
} catch { /* extensions not available in this context */ }
270+
};
271+
272+
useEffect(() => { void refreshInstalledExt(); }, []);
247273
const { settings, saveSettings } = useSettingsStore();
248274

249275
// Registry stores
@@ -256,14 +282,15 @@ export default function MarketplaceSidebarPanel() {
256282
// Fetch registry data for the active tab on demand
257283
useEffect(() => {
258284
const typeMap: Partial<Record<TypeFilter, RegistryType>> = {
259-
theme: 'themes',
260-
persona: 'personas',
261-
prompt: 'prompts',
262-
profile: 'profiles',
285+
theme: 'themes',
286+
persona: 'personas',
287+
prompt: 'prompts',
288+
profile: 'profiles',
289+
extension: 'extensions',
263290
};
264291
if (typePill === 'all') {
265292
// Fetch all registry types in parallel on first open
266-
(['themes', 'personas', 'prompts', 'profiles'] as RegistryType[]).forEach((t) => fetchType(t));
293+
(['themes', 'personas', 'prompts', 'profiles', 'extensions'] as RegistryType[]).forEach((t) => fetchType(t));
267294
} else {
268295
const rt = typeMap[typePill];
269296
if (rt) fetchType(rt);
@@ -273,7 +300,7 @@ export default function MarketplaceSidebarPanel() {
273300
// Parse @mcp / @provider / @theme / etc. prefix from raw query
274301
const PREFIX_MAP: Record<string, TypeFilter> = {
275302
mcp: 'mcp', provider: 'provider', theme: 'theme',
276-
persona: 'persona', prompt: 'prompt', profile: 'profile',
303+
persona: 'persona', prompt: 'prompt', profile: 'profile', extension: 'extension',
277304
};
278305
const atMatch = query.match(/^@(\w+)\s*/i);
279306
const prefixType: TypeFilter = atMatch
@@ -376,14 +403,27 @@ export default function MarketplaceSidebarPanel() {
376403
entry: e,
377404
}));
378405

406+
const extensionEntries: UnifiedEntry[] = getEntries('extensions').map((e) => ({
407+
kind: 'extension',
408+
id: e.id,
409+
name: e.name,
410+
description: e.description,
411+
author: e.author,
412+
verified: e.verified,
413+
installed: installedExtMap.has(e.id),
414+
hasUpdate: isNewer(e.version, installedExtMap.get(e.id)),
415+
entry: e,
416+
}));
417+
379418
let pool: UnifiedEntry[] =
380-
effectiveType === 'provider' ? providers
381-
: effectiveType === 'mcp' ? mcpServers
382-
: effectiveType === 'theme' ? themes
383-
: effectiveType === 'persona'? personaEntries
384-
: effectiveType === 'prompt' ? promptEntries
385-
: effectiveType === 'profile'? profileEntries
386-
: [...providers, ...mcpServers, ...themes, ...personaEntries, ...promptEntries, ...profileEntries];
419+
effectiveType === 'provider' ? providers
420+
: effectiveType === 'mcp' ? mcpServers
421+
: effectiveType === 'theme' ? themes
422+
: effectiveType === 'persona' ? personaEntries
423+
: effectiveType === 'prompt' ? promptEntries
424+
: effectiveType === 'profile' ? profileEntries
425+
: effectiveType === 'extension' ? extensionEntries
426+
: [...providers, ...mcpServers, ...themes, ...personaEntries, ...promptEntries, ...profileEntries, ...extensionEntries];
387427

388428
if (cleanQuery) {
389429
pool = pool.filter(
@@ -394,7 +434,7 @@ export default function MarketplaceSidebarPanel() {
394434
}
395435

396436
return pool;
397-
}, [settings, effectiveType, cleanQuery, getEntries, installedThemeIds, installedPersonaNames, installedTemplateNames, installedProfileNames, installedThemes, installedPersonas, installedTemplates, installedProfiles]);
437+
}, [settings, effectiveType, cleanQuery, getEntries, installedThemeIds, installedPersonaNames, installedTemplateNames, installedProfileNames, installedThemes, installedPersonas, installedTemplates, installedProfiles, installedExtMap]);
398438

399439
const handleAdd = async (entry: UnifiedEntry) => {
400440
if (!settings) return;
@@ -480,14 +520,25 @@ export default function MarketplaceSidebarPanel() {
480520
taskOverrides: c.taskOverrides,
481521
fromRegistry: true,
482522
});
523+
} else if (entry.kind === 'extension') {
524+
const c = entry.entry.content as { downloadUrl?: string };
525+
if (!c.downloadUrl) return;
526+
const api = (window as ExtApiWindow).api;
527+
if (!api?.extensions?.install) return;
528+
const result = await api.extensions.install(entry.id, c.downloadUrl);
529+
if (result.success) {
530+
await refreshInstalledExt();
531+
await loadInstalledExtensions();
532+
}
533+
return;
483534
}
484535

485536
// Re-run the extension loader so any newly-downloaded bundle extension
486537
// appears immediately without requiring an app restart.
487538
await loadInstalledExtensions();
488539
};
489540

490-
const handleRemove = (entry: UnifiedEntry) => {
541+
const handleRemove = async (entry: UnifiedEntry) => {
491542
if (entry.kind === 'theme') {
492543
uninstallTheme(entry.id);
493544
} else if (entry.kind === 'persona') {
@@ -498,6 +549,11 @@ export default function MarketplaceSidebarPanel() {
498549
} else if (entry.kind === 'profile') {
499550
const match = installedProfiles.find((p) => p.name === entry.name);
500551
if (match) removeProfile(match.id);
552+
} else if (entry.kind === 'extension') {
553+
const api = (window as ExtApiWindow).api;
554+
if (!api?.extensions?.uninstall) return;
555+
await api.extensions.uninstall(entry.id);
556+
await refreshInstalledExt();
501557
}
502558
};
503559

@@ -519,15 +575,20 @@ export default function MarketplaceSidebarPanel() {
519575
const existing = installedProfiles.find((p) => p.name === entry.name);
520576
if (existing) removeProfile(existing.id);
521577
await handleAdd(entry);
578+
} else if (entry.kind === 'extension') {
579+
// Uninstall old version, then re-download the new .ocx
580+
await handleRemove(entry);
581+
await handleAdd(entry);
522582
}
523583
};
524584

525585
// Loading/error state for the active registry type
526586
const activeRegistryType: RegistryType | null =
527-
effectiveType === 'theme' ? 'themes'
528-
: effectiveType === 'persona' ? 'personas'
529-
: effectiveType === 'prompt' ? 'prompts'
530-
: effectiveType === 'profile' ? 'profiles'
587+
effectiveType === 'theme' ? 'themes'
588+
: effectiveType === 'persona' ? 'personas'
589+
: effectiveType === 'prompt' ? 'prompts'
590+
: effectiveType === 'profile' ? 'profiles'
591+
: effectiveType === 'extension' ? 'extensions'
531592
: null;
532593

533594
const isRegistryLoading = activeRegistryType ? !!regLoading[activeRegistryType] : false;

src/stores/registryStore.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const REGISTRY_BACKUP = 'https://openconduit-registry.chumchal-account.workers.d
1111

1212
// ─── Types ────────────────────────────────────────────────────────────────────
1313

14-
export type RegistryType = 'themes' | 'personas' | 'prompts' | 'profiles' | 'providers' | 'mcp';
14+
export type RegistryType = 'themes' | 'personas' | 'prompts' | 'profiles' | 'providers' | 'mcp' | 'extensions';
1515

1616
export interface RegistryEntry {
1717
id: string;
@@ -126,7 +126,7 @@ export const useRegistryStore = create<RegistryState>()(
126126
},
127127

128128
fetchAll: async () => {
129-
const types: RegistryType[] = ['themes', 'personas', 'prompts', 'profiles', 'providers', 'mcp'];
129+
const types: RegistryType[] = ['themes', 'personas', 'prompts', 'profiles', 'providers', 'mcp', 'extensions'];
130130
await Promise.all(types.map((t) => get().fetchType(t)));
131131
},
132132
}),
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"id": "ref.export-conversation",
3+
"name": "Export Conversation",
4+
"version": "1.0.0",
5+
"description": "Export the active conversation as Markdown or plain text. Demonstrates reading conversation data and calling ui.showNotification() from a sandboxed extension.",
6+
"author": "OpenConduit",
7+
"entryPoint": "src/index.js",
8+
"contributes": {
9+
"activityBarItems": [
10+
{
11+
"panelId": "ref.export-conversation",
12+
"label": "Export Conversation",
13+
"order": 92
14+
}
15+
]
16+
}
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"id": "ref.export-conversation",
3+
"name": "Export Conversation",
4+
"version": "1.0.0",
5+
"description": "Export the active conversation as Markdown or plain text. Demonstrates reading conversation data and calling ui.showNotification() from a sandboxed extension.",
6+
"author": "OpenConduit",
7+
"entryPoint": "src/index.js",
8+
"contributes": {
9+
"activityBarItems": [
10+
{
11+
"panelId": "ref.export-conversation",
12+
"label": "Export Conversation",
13+
"order": 92
14+
}
15+
]
16+
}
17+
}

0 commit comments

Comments
 (0)