Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
920770b
refactor(web): register the Diff side panel
saphid Oct 2, 2026
e1ae808
refactor(web): register the Preview side panel
github-actions[bot] Oct 4, 2026
4851ae8
perf(web): keep the panel host stable across chat view renders
github-actions[bot] Oct 7, 2026
30972c6
fix(web): send a late annotation only to the thread it was picked in
github-actions[bot] Oct 7, 2026
52a78e7
fix(web): lend the annotation sender only after the chat view commits
github-actions[bot] Oct 7, 2026
7be7fb5
refactor(web): move the thread terminal panel and drawer out of ChatView
github-actions[bot] Oct 4, 2026
a0ea164
refactor(web): open the right-panel terminal through the panel host
github-actions[bot] Oct 4, 2026
02d7fe8
fix(web): keep a local-checkout terminal launch off the thread's work…
github-actions[bot] Oct 6, 2026
ca03345
fix(web): keep a terminal the server opened on the checkout off the t…
github-actions[bot] Oct 7, 2026
4b7aa2e
fix(web): keep device results in the thread that started them
github-actions[bot] Oct 4, 2026
a1d256b
refactor(web): open the Device side panel through the panel host
github-actions[bot] Oct 4, 2026
bcb7b41
refactor(web): open the pull request side panels through the panel host
github-actions[bot] Oct 4, 2026
cfd56f6
test(web): build the pull request panel fixture against current main
github-actions[bot] Oct 11, 2026
4a90882
refactor(web): open the Files side panel through the panel host
github-actions[bot] Oct 4, 2026
3fe8920
test(web): stub the server environment atoms in the Files panel test
github-actions[bot] Oct 12, 2026
8cf75b7
fix(web): drop Files actions that settle after leaving the thread
github-actions[bot] Oct 4, 2026
4f15c62
fix(web): open tree clicks in the thread the Files panel shows now
github-actions[bot] Oct 6, 2026
0e86f37
fix(web): keep a Files browser open across composer draft switches
github-actions[bot] Oct 6, 2026
419306d
fix(web): point tree clicks at the new thread as soon as it commits
github-actions[bot] Oct 10, 2026
3b7aa41
feat(pi): show extension statuses in the thread header
github-actions[bot] Oct 4, 2026
083f6ab
fix(pi): keep statuses when Pi refuses a rollback fork
github-actions[bot] Oct 6, 2026
e8f8f57
fix(pi): carry each status generation's thread on its marker
github-actions[bot] Oct 11, 2026
b08374b
feat(mobile): show Pi extension statuses under the thread header
github-actions[bot] Oct 4, 2026
20b745e
feat(server): run consented local plugins in supervised child processes
github-actions[bot] Oct 7, 2026
c4da6af
fix(server): answer every plugin result with a message the server can…
github-actions[bot] Oct 6, 2026
a3ca98c
fix(server): drop what a plugin registered before its activation failed
github-actions[bot] Oct 6, 2026
1829ea3
fix(server): exit a plugin child on a corrupt IPC line and cover its …
github-actions[bot] Oct 7, 2026
c59bd7e
fix(server): hold a plugin's partial IPC line in one growing buffer
github-actions[bot] Oct 7, 2026
d563b47
fix(server): hold plugin calls made during startup until enabled plug…
github-actions[bot] Oct 7, 2026
5b1946f
fix(server): count a plugin IPC line's newline against the read budget
github-actions[bot] Oct 7, 2026
d11ac2c
fix(server): restore enabled plugins before any management step at st…
github-actions[bot] Oct 10, 2026
9ffc0ea
fix(server): let calls waiting on a plugin start hear how it ended
github-actions[bot] Oct 10, 2026
7c900a8
fix(server): refuse a plugin file swapped for a FIFO while digesting
github-actions[bot] Oct 10, 2026
4122e0d
chore(server): say why plugin host modules import Node builtins
github-actions[bot] Oct 10, 2026
04704fb
fix(server): answer a plugin call cancelled before its handler started
github-actions[bot] Oct 10, 2026
7a956a0
test(server): start the cooperative handler before timing it out
github-actions[bot] Oct 10, 2026
8c823b3
fix(server): close a dead plugin's stderr that a process it started h…
github-actions[bot] Oct 10, 2026
914b34e
refactor(server): build the plugin RPC handlers in their own layer
github-actions[bot] Oct 11, 2026
14d57ad
test(server): answer a replayed request only once its card is projected
github-actions[bot] Oct 10, 2026
5da52d5
feat(server): deliver finished runs to plugins from an acknowledged c…
github-actions[bot] Oct 4, 2026
1a02508
fix(server): let a run's queued checkpoint capture decide its finaliz…
github-actions[bot] Oct 6, 2026
b82a25b
fix(server): refuse the events capability without the proposed API op…
github-actions[bot] Oct 6, 2026
2e20f0a
fix(server): drop a plugin's event handlers when its activation fails
github-actions[bot] Oct 7, 2026
6f11331
test(server): cover finalization of a run stopped through the ownersh…
github-actions[bot] Oct 7, 2026
0e827f1
fix(server): stop a cancelled setup's branch rename before forgetting…
github-actions[bot] Oct 12, 2026
ff5dbc8
feat(server): let agents list and call plugin tools through MCP
github-actions[bot] Oct 7, 2026
ab90f2f
fix(server): list no plugins to MCP callers outside a T3 thread
github-actions[bot] Oct 7, 2026
3c780d4
fix(server): drop a reused credential's reservation when its grant up…
github-actions[bot] Oct 7, 2026
1561cf5
docs(plugins): say plugins run under the server's account
github-actions[bot] Oct 7, 2026
1fcbcb2
fix(server): drop a reused credential's reservation when its reuse ch…
github-actions[bot] Oct 7, 2026
f1844a2
feat(server): typed plugin settings, write-only secrets and plugin st…
github-actions[bot] Oct 7, 2026
428902e
docs(plugins): say plugins run under the server's account
github-actions[bot] Oct 7, 2026
5605a8b
fix(server): refresh open plugin settings when the declared fields ch…
github-actions[bot] Oct 10, 2026
a2a5171
fix(server): finish a plugin's host work before disable returns
github-actions[bot] Oct 10, 2026
a4e2d88
fix(server): wake host calls waiting on a plugin below the stream buffer
github-actions[bot] Oct 10, 2026
15a8bcf
feat: offer plugin actions in the palette, slash menu and thread menus
github-actions[bot] Oct 7, 2026
d7988f8
fix(contracts): keep a plugin action when only its placement is unknown
github-actions[bot] Oct 6, 2026
265b135
fix: match plugin action names in the slash menu regardless of case
github-actions[bot] Oct 6, 2026
52be65f
docs(plugins): say the slash menu opens only at the start of a line
github-actions[bot] Oct 7, 2026
3b4ca09
fix(web,mobile): offer plugin actions only to connections that can ru…
github-actions[bot] Oct 7, 2026
5cd5113
test(web): stub plugin actions in the thread action menu hook test
github-actions[bot] Oct 7, 2026
545cbfe
fix(web,mobile): check the live permission when a plugin action runs
github-actions[bot] Oct 7, 2026
7de52de
docs(plugins): say plugins run under the server's account
github-actions[bot] Oct 7, 2026
e982989
fix(mobile): offer environment plugin actions in the palette without …
github-actions[bot] Oct 7, 2026
4c6cd43
test(web): pass the active project to the plugin actions composer fix…
github-actions[bot] Oct 11, 2026
c5b6a68
feat: show plugin views as isolated right-panel tabs on web and desktop
github-actions[bot] Oct 7, 2026
4b74d88
fix: keep plugin view rate limits and navigation logs safe
github-actions[bot] Oct 6, 2026
2fa3277
fix(server): refuse the views capability without the proposed API opt-in
github-actions[bot] Oct 6, 2026
667d8f4
fix(web): keep the side-panel plugin view list stable across renders
github-actions[bot] Oct 10, 2026
950642c
fix(server): type WebSocket handlers against the uninstrumented RPC g…
github-actions[bot] Oct 9, 2026
97acb8c
fix(server): serve plugin view files whose names start with two dots
github-actions[bot] Oct 10, 2026
8995989
fix(server): serve only plugin view bytes the approval digest covered
github-actions[bot] Oct 10, 2026
d42fae0
feat(server): install plugins from npm with integrity checks and stag…
github-actions[bot] Oct 7, 2026
dc7d7b1
fix(server): accept plain http npm registries only on this machine
github-actions[bot] Oct 6, 2026
beb9d20
fix(server): hold npm registry redirects and saved registries to the …
github-actions[bot] Oct 7, 2026
846d385
fix(server): read npm tarball headers the way tar writers mean them
github-actions[bot] Oct 10, 2026
095c1cf
fix(server): list a staged npm update's tools, settings, and actions
github-actions[bot] Oct 10, 2026
9b0aab8
fix(server): size the npm unpack cap by archive entries, not files
github-actions[bot] Oct 10, 2026
22014bc
fix(server): refuse a pax record that does not end in a newline
github-actions[bot] Oct 10, 2026
232bcca
refactor(server): follow the Effect service rules in npm plugin installs
github-actions[bot] Oct 10, 2026
9a29542
fix(server): size a GNU long name by its own header in npm tarballs
github-actions[bot] Oct 10, 2026
574e4ca
fix(server): refuse npm tarball paths with a NUL byte
github-actions[bot] Oct 10, 2026
1308f78
fix(server): report an applied npm update whose plugin could not be e…
github-actions[bot] Oct 10, 2026
65966f6
fix(server): refuse an npm update to the files already installed
github-actions[bot] Oct 10, 2026
b09b51b
feat(web,mobile): manage plugins in Settings
github-actions[bot] Oct 4, 2026
ab5a245
docs(plugins): say plugins run under the server's account
github-actions[bot] Oct 7, 2026
9f62660
fix(mobile): ask to select an environment when none is selected for p…
github-actions[bot] Oct 10, 2026
61a2196
feat(web,mobile): fill in plugin settings from Settings
github-actions[bot] Oct 4, 2026
a0ebe84
feat(web,mobile): install and update plugins from npm in Settings
github-actions[bot] Oct 4, 2026
2bbd01c
fix(client-runtime): show a downloaded same-version plugin update in …
github-actions[bot] Oct 10, 2026
32eb7b0
feat(web,mobile): show what each plugin adds and what its capabilitie…
github-actions[bot] Oct 4, 2026
e21d19f
feat(server,web,mobile): let plugins show statuses on threads
github-actions[bot] Oct 4, 2026
bf6d3f0
fix(server): tell a plugin when the thread has no room for its status
github-actions[bot] Oct 6, 2026
2bda1b2
feat(server,web,mobile): let plugins send notifications
github-actions[bot] Oct 7, 2026
baaab21
fix(mobile): announce plugin notification banners to screen readers
github-actions[bot] Oct 6, 2026
86368fa
refactor(server): follow the Effect service rules in plugin notificat…
github-actions[bot] Oct 10, 2026
b2022c1
feat(server,web,mobile): let plugins add context to the turns you send
github-actions[bot] Oct 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
25 changes: 25 additions & 0 deletions apps/desktop/src/window/DesktopWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import * as ElectronApp from "../electron/ElectronApp.ts";
import * as DesktopRendererHistory from "../telemetry/DesktopRendererHistory.ts";
import { makeQuitShortcutHandler } from "./QuitHold.ts";
import { copyContextMenuImage } from "./ContextMenuImage.ts";
import { shouldVetoViewFrameNavigation } from "./pluginViewNavigation.ts";

const TITLEBAR_HEIGHT = 40;
// Matches --workspace-topbar-height in apps/web/src/index.css. Native macOS
Expand Down Expand Up @@ -652,6 +653,30 @@ export const make = Effect.gen(function* () {
void runPromise(electronShell.openExternal(url));
}
});
// Plugin views may not navigate themselves; see pluginViewNavigation.ts.
window.webContents.on("will-frame-navigate", (event) => {
if (event.isMainFrame) return;
if (
!shouldVetoViewFrameNavigation({
url: event.url,
frame: event.frame,
initiator: event.initiator,
mainFrame: window.webContents.mainFrame,
})
)
return;
event.preventDefault();
// Only where it tried to go: a path or query can carry the view's data.
const target = URL.parse(event.url);
void runPromise(
logWindowInfo(
"refused a plugin view navigation",
target === null
? { protocol: "invalid" }
: { protocol: target.protocol, host: target.host },
),
);
});

// Electron's windowMenu close role owns CmdOrCtrl+W. Holding the
// close-terminal shortcut can outlive the terminal that handled its first
Expand Down
79 changes: 79 additions & 0 deletions apps/desktop/src/window/pluginViewNavigation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, expect, it } from "vite-plus/test";

import { type NavigationFrame, shouldVetoViewFrameNavigation } from "./pluginViewNavigation.ts";

const frame = (
frameToken: string,
url: string,
origin: string,
parent: NavigationFrame | null,
): NavigationFrame => ({ frameToken, url, origin, parent });

const app = frame("app", "t3code-dev://app/", "t3code-dev://app", null);
const view = frame("view", "about:srcdoc", "null", app);
const wrapper = frame("wrapper", "about:srcdoc", "null", app);
const wrappedView = frame("inner", "about:srcdoc", "null", wrapper);

describe("shouldVetoViewFrameNavigation", () => {
it("refuses a view navigating itself, including through a policy wrapper", () => {
for (const target of [view, wrappedView]) {
for (const url of ["about:blank", "http://127.0.0.1:7391/page.html", "data:text/html,x"]) {
expect(
shouldVetoViewFrameNavigation({ url, frame: target, initiator: target, mainFrame: app }),
).toBe(true);
}
}
});

it("refuses a navigation with no known initiator", () => {
expect(
shouldVetoViewFrameNavigation({
url: "http://127.0.0.1:7391/redirect",
frame: view,
initiator: null,
mainFrame: app,
}),
).toBe(true);
});

it("lets the app mount, wrap and dispose views", () => {
const fresh = frame("fresh", "about:blank", "t3code-dev://app", app);
expect(
shouldVetoViewFrameNavigation({
url: "about:srcdoc",
frame: fresh,
initiator: app,
mainFrame: app,
}),
).toBe(false);
const freshInner = frame("fresh-inner", "about:blank", "null", wrapper);
expect(
shouldVetoViewFrameNavigation({
url: "about:srcdoc",
frame: freshInner,
initiator: wrapper,
mainFrame: app,
}),
).toBe(false);
expect(
shouldVetoViewFrameNavigation({
url: "about:blank",
frame: view,
initiator: app,
mainFrame: app,
}),
).toBe(false);
});

it("leaves frames outside views alone", () => {
const other = frame("other", "https://example.com/", "https://example.com", app);
expect(
shouldVetoViewFrameNavigation({
url: "https://example.com/next",
frame: other,
initiator: other,
mainFrame: app,
}),
).toBe(false);
});
});
35 changes: 35 additions & 0 deletions apps/desktop/src/window/pluginViewNavigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Main-process navigation veto for isolated plugin view frames.
*
* A plugin view runs in an opaque-origin `about:srcdoc` frame inside a
* policy wrapper frame of the same kind. A document's own CSP cannot stop its
* frame navigating, so the main process refuses any navigation of a frame in
* such a subtree unless the app's main frame started it. Loading
* `about:srcdoc` stays allowed so the wrapper can mount its view. Electron
* does not report `about:blank` or `data:` here: the wrapper's CSP refuses
* `data:`, and the host's ping liveness check ends a view that blanked itself.
*/

export interface NavigationFrame {
readonly url: string;
readonly origin: string;
readonly frameToken: string;
readonly parent: NavigationFrame | null;
}

const isViewDocument = (frame: NavigationFrame) =>
frame.url === "about:srcdoc" && frame.origin === "null";

export function shouldVetoViewFrameNavigation(input: {
readonly url: string;
readonly frame: NavigationFrame | null;
readonly initiator: NavigationFrame | null | undefined;
readonly mainFrame: NavigationFrame;
}): boolean {
let insideView = false;
for (let frame = input.frame; frame !== null; frame = frame.parent) {
if (isViewDocument(frame)) insideView = true;
}
if (!insideView || input.url === "about:srcdoc") return false;
return input.initiator?.frameToken !== input.mainFrame.frameToken;
}
15 changes: 15 additions & 0 deletions apps/mobile/src/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
type RenderFailureProps,
} from "./components/RenderErrorBoundary";
import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen";
import { PluginNotificationBannerHost } from "./features/plugins/PluginNotificationBannerHost";
import { useAgentNotificationNavigation } from "./features/agent-awareness/notificationNavigation";
import { ConnectOnboardingRouteScreen } from "./features/cloud/ConnectOnboardingRouteScreen";
import { useConnectOnboardingNavigation } from "./features/cloud/connectOnboardingNavigation";
Expand Down Expand Up @@ -105,6 +106,10 @@ import {
SettingsScheduledTaskNewRouteScreen,
SettingsScheduledTaskEditRouteScreen,
} from "./features/settings/SettingsScheduledTasksRouteScreen";
import {
SettingsPluginRouteScreen,
SettingsPluginsRouteScreen,
} from "./features/settings/SettingsPluginsRouteScreen";
import {
ScheduledTaskModelPickerRouteScreen,
ScheduledTaskBranchPickerRouteScreen,
Expand Down Expand Up @@ -344,6 +349,15 @@ const SettingsContentStack = createV5SheetStackNavigator({
headerTitleStyle: { fontSize: 16, fontWeight: "800" },
},
}),
SettingsPlugins: createNativeStackScreen({
screen: SettingsPluginsRouteScreen,
linking: "plugins",
options: { title: "Plugins" },
}),
SettingsPlugin: createNativeStackScreen({
screen: SettingsPluginRouteScreen,
options: { title: "Plugin" },
}),
SettingsScheduledTaskNew: createNativeStackScreen({
screen: SettingsScheduledTaskNewRouteScreen,
linking: "scheduled-tasks/new",
Expand Down Expand Up @@ -654,6 +668,7 @@ function RootStackLayout(props: {
{props.children}
<HardwareKeyboardCommandOverlay />
</AdaptiveWorkspaceLayout>
<PluginNotificationBannerHost />
</ExistingThreadSettingsRouteProvider>
</HardwareKeyboardCommandProvider>
);
Expand Down
32 changes: 31 additions & 1 deletion apps/mobile/src/features/keyboard/CommandPalette.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useNavigation } from "@react-navigation/native";
import type { EnvironmentThreadSearchMatch } from "@t3tools/client-runtime/state/thread-search";
import { THREAD_JUMP_KEYBINDING_COMMANDS } from "@t3tools/contracts";
import { AuthOrchestrationOperateScope, THREAD_JUMP_KEYBINDING_COMMANDS } from "@t3tools/contracts";
import { threadPullRequestSearchTerms } from "@t3tools/shared/threadPullRequests";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
Expand All @@ -25,13 +25,16 @@ import { cn } from "../../lib/cn";
import { scopedProjectKey, scopedThreadKey } from "../../lib/scopedEntities";
import { T3KeyboardCommands } from "../../native/T3KeyboardCommands";
import { useProjects, useThreadShell, useThreadShells } from "../../state/entities";
import { runPluginAction, usePluginActions } from "../../state/plugin-actions";
import { useThreadSearch } from "../../state/queries";
import { useEnvironmentScope } from "../../state/session";
import { useWorkspaceEnvironments } from "../../state/workspace";
import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry";
import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";
import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
import { ThreadSearchMatchExcerpt } from "../threads/thread-search-match";
import {
buildPluginActionPaletteItems,
filterCommandPaletteItems,
nextPaletteIndex,
type CommandPaletteItem,
Expand Down Expand Up @@ -66,6 +69,7 @@ const ACTION_ICONS: Record<string, AppSymbolName> = {
function itemIcon(item: CommandPaletteItem): AppSymbolName {
if (item.kind === "project") return "folder";
if (item.kind === "thread") return "text.bubble";
if (item.key.startsWith("plugin-action:")) return "cube";
return ACTION_ICONS[item.key] ?? "ellipsis";
}

Expand Down Expand Up @@ -146,6 +150,17 @@ export function CommandPalette(props: {
const activeThreadRef = useMemo(() => parseActiveThreadPath(props.pathname), [props.pathname]);
const activeThread = useThreadShell(activeThreadRef);
const environments = useWorkspaceEnvironments();
// Plugin actions belong to the open thread's environment, else the first connected one.
const pluginActionEnvironmentId =
activeThreadRef?.environmentId ??
environments.find((environment) => environment.connectionState === "connected")
?.environmentId ??
null;
const pluginActions = usePluginActions(pluginActionEnvironmentId);
const canRunPluginActions = useEnvironmentScope(
pluginActionEnvironmentId,
AuthOrchestrationOperateScope,
);
const { savedConnectionsById } = useSavedRemoteConnections();
const [query, setQuery] = useState("");
const [selection, setSelection] = useState<string | null>(null);
Expand Down Expand Up @@ -300,6 +315,18 @@ export function CommandPalette(props: {
})),
);
}
if (pluginActionEnvironmentId !== null) {
actions.push(
...buildPluginActionPaletteItems({
actions: pluginActions,
canOperate: canRunPluginActions,
environmentId: pluginActionEnvironmentId,
threadId: activeThread?.id ?? null,
projectId: activeThread?.projectId ?? null,
runAction: (input) => void runPluginAction(input),
}),
);
}
const projectItems: CommandPaletteItem[] = projects.map((project) => ({
key: `project:${scopedProjectKey(project.environmentId, project.id)}`,
kind: "project",
Expand Down Expand Up @@ -346,6 +373,9 @@ export function CommandPalette(props: {
activeThread,
activeThreadRef,
navigation,
canRunPluginActions,
pluginActionEnvironmentId,
pluginActions,
projects,
runCommand,
savedConnectionsById,
Expand Down
86 changes: 85 additions & 1 deletion apps/mobile/src/features/keyboard/commandPaletteItems.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { describe, expect, it } from "vite-plus/test";
import {
EnvironmentId,
PluginActionId,
ProjectId,
ThreadId,
type PluginAction,
} from "@t3tools/contracts";
import { describe, expect, it, vi } from "vite-plus/test";

import {
buildPluginActionPaletteItems,
filterCommandPaletteItems,
nextPaletteIndex,
type CommandPaletteItem,
Expand Down Expand Up @@ -74,3 +82,79 @@ describe("nextPaletteIndex", () => {
expect(nextPaletteIndex(0, 1, 0)).toBe(0);
});
});

describe("buildPluginActionPaletteItems", () => {
const environmentId = EnvironmentId.make("environment-1");
const thread = {
environmentId,
threadId: ThreadId.make("thread-1"),
projectId: ProjectId.make("project-1"),
};
const deploy: PluginAction = {
id: PluginActionId.make("installation-1:1:deploy"),
pluginId: "acme.deploy",
pluginName: "Deploy",
name: "deploy",
title: "Deploy this branch",
target: "thread",
placements: ["command-palette"],
};
const refresh: PluginAction = {
...deploy,
id: PluginActionId.make("installation-1:1:refresh"),
name: "refresh",
title: "Refresh caches",
target: "environment",
};

it("runs an offered action in the open thread's environment", () => {
const runAction = vi.fn();
const offered = buildPluginActionPaletteItems({
actions: [deploy],
canOperate: true,
...thread,
runAction,
});
expect(offered.map((item) => item.title)).toEqual(["Deploy this branch"]);

offered[0]?.run();

expect(runAction).toHaveBeenCalledWith({
environmentId: thread.environmentId,
action: deploy,
target: { _tag: "thread", threadId: thread.threadId },
});
});

it("offers environment actions when no thread is open", () => {
const runAction = vi.fn();
const offered = buildPluginActionPaletteItems({
actions: [deploy, refresh],
canOperate: true,
environmentId,
threadId: null,
projectId: null,
runAction,
});
expect(offered.map((item) => item.title)).toEqual(["Refresh caches"]);

offered[0]?.run();

expect(runAction).toHaveBeenCalledWith({
environmentId,
action: refresh,
target: { _tag: "environment" },
});
});

it("offers nothing to a connection that cannot operate the environment", () => {
expect(
buildPluginActionPaletteItems({
actions: [deploy],
canOperate: false,
...thread,
runAction: vi.fn(),
}),
).toEqual([]);
});
});
Loading
Loading