Skip to content

Commit 6dfc50a

Browse files
authored
feat(api): expose BrowserExtension.getTabs to extensions (#1612)
1 parent 23d6131 commit 6dfc50a

12 files changed

Lines changed: 175 additions & 3 deletions

File tree

extra/extension-boilerplate/package-lock.json

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

extra/extension-boilerplate/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@
103103
"description": "Test action panel reconciliation, deep submenus, dynamic content, and filter preservation",
104104
"mode": "view"
105105
},
106+
{
107+
"name": "browser",
108+
"title": "Browser Extension Showcase",
109+
"description": "Browser extension",
110+
"mode": "view"
111+
},
106112
{
107113
"name": "desktop-notif-interval",
108114
"title": "Desktop Notification Interval",
@@ -125,6 +131,7 @@
125131
"devDependencies": {
126132
"@biomejs/biome": "2.3.2",
127133
"@types/node": "^24.9.2",
134+
"@types/react": "^19.2.17",
128135
"typescript": "^5.9.2"
129136
}
130137
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {
2+
Action,
3+
ActionPanel,
4+
BrowserExtension,
5+
Icon,
6+
List,
7+
} from "@vicinae/api";
8+
import { useEffect, useState } from "react";
9+
10+
export default function BrowserExt() {
11+
const [tabs, setTabs] = useState<BrowserExtension.Tab[]>();
12+
13+
useEffect(() => {
14+
BrowserExtension.getTabs().then(setTabs);
15+
}, []);
16+
17+
return (
18+
<List searchBarPlaceholder="Search for browser tabs...">
19+
{tabs?.map((t) => (
20+
<List.Item
21+
title={t.title ?? "Unnamed"}
22+
subtitle={t.url}
23+
icon={Icon.Tag}
24+
actions={
25+
<ActionPanel>
26+
<Action title="Focus" onAction={() => t.focus()} />
27+
</ActionPanel>
28+
}
29+
/>
30+
))}
31+
</List>
32+
);
33+
}

figura/tsapi.fig

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,19 @@ service Wallpaper {
313313
fn set(path: string, options: SetWallpaperOptions) => void;
314314
};
315315

316+
struct BrowserTab {
317+
id: int;
318+
title?: string;
319+
url: string;
320+
active: boolean;
321+
browserId: string;
322+
};
323+
324+
service BrowserExtension {
325+
fn getTabs() => BrowserTab[];
326+
fn focusTab(browserId: string, tabId: int) => void;
327+
};
328+
316329
// EventCore - used for loosely typed callbacks, required for component event handlers
317330

318331
service EventCore {

src/server/src/extension/extension-command-runtime.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "common/context.hpp"
44
#include "extension-error-view-host.hpp"
55
#include "extension/services/application-service.hpp"
6+
#include "extension/services/browser-extension-service.hpp"
67
#include "extension/services/clipboard-service.hpp"
78
#include "extension/services/command-service.hpp"
89
#include "extension/services/event-core-service.hpp"
@@ -55,9 +56,10 @@ void ExtensionCommandRuntime::initialize() {
5556
auto *command = new ExtCommandService(*m_transport, m_command, services->rootItemManager(), *ctx.settings);
5657
auto *oauth = new ExtOAuthService(*m_transport, m_command->extensionId(), ctx);
5758
auto wallpaper = new ExtWallpaperService(*m_transport, *services->wallpaperManager());
59+
auto browserExtension = new ExtBrowserExtensionService(*m_transport, *services->browserExtension());
5860

5961
m_server = new tsapi::Server(*m_transport, app, ui, wm, clipboard, storage, fileSearch, command, oauth,
60-
wallpaper, eventCore);
62+
wallpaper, browserExtension, eventCore);
6163
m_server->setLogger(m_logger.get());
6264
m_server->setParent(this);
6365
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#pragma once
2+
#include "generated/tsapi.hpp"
3+
#include "services/browser-extension-service.hpp"
4+
5+
class ExtBrowserExtensionService : public tsapi::AbstractBrowserExtension {
6+
using Void = tsapi::Result<void>;
7+
8+
public:
9+
ExtBrowserExtensionService(tsapi::RpcTransport &transport, BrowserExtensionService &browser)
10+
: AbstractBrowserExtension(transport), m_browser(browser) {}
11+
12+
tsapi::Result<std::vector<tsapi::BrowserTab>>::Future getTabs() override {
13+
auto tabs = m_browser.tabs();
14+
std::vector<tsapi::BrowserTab> transformed;
15+
16+
transformed.reserve(tabs.size());
17+
18+
for (auto &tab : tabs) {
19+
transformed.emplace_back(tsapi::BrowserTab{
20+
.id = tab.id,
21+
.title = std::move(tab.title),
22+
.url = std::move(tab.url),
23+
.active = tab.active,
24+
.browserId = std::move(tab.browserId),
25+
});
26+
}
27+
28+
return tsapi::Result<std::vector<tsapi::BrowserTab>>::ok(transformed);
29+
}
30+
31+
tsapi::Result<void>::Future focusTab(std::string browserId, int32_t tabId) override {
32+
m_browser.focusTab(browserId, tabId);
33+
return Void::ok();
34+
}
35+
36+
private:
37+
BrowserExtensionService &m_browser;
38+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { getClient } from "./client";
2+
3+
/**
4+
* Use browser extension features, provided that the user has it installed and active.
5+
*/
6+
export namespace BrowserExtension {
7+
export type Tab = {
8+
id: number;
9+
title?: string;
10+
url: string;
11+
active: boolean;
12+
focus: () => void;
13+
};
14+
15+
export async function getTabs(): Promise<Tab[]> {
16+
const browser = getClient().BrowserExtension;
17+
return browser.getTabs().then((bb) =>
18+
bb.map<Tab>((b) => ({
19+
...b,
20+
focus: () => {
21+
getClient().BrowserExtension.focusTab(b.browserId, b.id);
22+
},
23+
})),
24+
);
25+
}
26+
}

src/typescript/api/src/api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ export * from "./file-search.js";
1919
export * from "./window-management.js";
2020
export * from "./command.js";
2121
export * from "./wallpaper.js";
22+
export * from "./browser.js";

src/typescript/api/src/api/proto/api.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,14 @@ export type SetWallpaperOptions = {
262262
screen?: string;
263263
}
264264

265+
export type BrowserTab = {
266+
id: number;
267+
title?: string;
268+
url: string;
269+
active: boolean;
270+
browserId: string;
271+
}
272+
265273
class ApplicationService {
266274
constructor(private readonly transport: RpcTransport) {}
267275

@@ -485,6 +493,19 @@ class WallpaperService {
485493

486494
}
487495

496+
class BrowserExtensionService {
497+
constructor(private readonly transport: RpcTransport) {}
498+
499+
getTabs(): Promise<BrowserTab[]> {
500+
return this.transport.request("BrowserExtension/getTabs", { });
501+
}
502+
503+
focusTab(browserId: string, tabId: number): Promise<void> {
504+
return this.transport.request("BrowserExtension/focusTab", { browserId, tabId});
505+
}
506+
507+
}
508+
488509
class EventCoreService {
489510
constructor(private readonly transport: RpcTransport) {}
490511

@@ -504,6 +525,7 @@ export class Client {
504525
this.Command = new CommandService(this.transport);
505526
this.OAuth = new OAuthService(this.transport);
506527
this.Wallpaper = new WallpaperService(this.transport);
528+
this.BrowserExtension = new BrowserExtensionService(this.transport);
507529
this.EventCore = new EventCoreService(this.transport);
508530
}
509531

@@ -517,6 +539,7 @@ export class Client {
517539
Command: CommandService;
518540
OAuth: OAuthService;
519541
Wallpaper: WallpaperService;
542+
BrowserExtension: BrowserExtensionService;
520543
EventCore: EventCoreService;
521544

522545
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type * as raycast from "@raycast/api";
2+
import * as vicinae from "@vicinae/api";
3+
import { unsupported } from "./unsupported";
4+
5+
export namespace BrowserExtension {
6+
export const getTabs: typeof raycast.BrowserExtension.getTabs = async () => {
7+
return vicinae.BrowserExtension.getTabs();
8+
};
9+
10+
export const getContent: typeof raycast.BrowserExtension.getContent =
11+
unsupported("getContent");
12+
}

0 commit comments

Comments
 (0)