Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
1fe8efd
refactor(web): register the Diff side panel
saphid Oct 2, 2026
bcbd324
refactor(web): register the Preview side panel
github-actions[bot] Oct 4, 2026
6befef3
perf(web): keep the panel host stable across chat view renders
github-actions[bot] Oct 7, 2026
1b23a3e
fix(web): send a late annotation only to the thread it was picked in
github-actions[bot] Oct 7, 2026
16877e3
fix(web): lend the annotation sender only after the chat view commits
github-actions[bot] Oct 7, 2026
bb246d6
refactor(web): move the thread terminal panel and drawer out of ChatView
github-actions[bot] Oct 4, 2026
64cdb14
refactor(web): open the right-panel terminal through the panel host
github-actions[bot] Oct 4, 2026
22f34a8
fix(web): keep a local-checkout terminal launch off the thread's work…
github-actions[bot] Oct 6, 2026
e34c8a4
fix(web): keep a terminal the server opened on the checkout off the t…
github-actions[bot] Oct 7, 2026
f8da909
fix(web): keep device results in the thread that started them
github-actions[bot] Oct 4, 2026
aea3b7a
refactor(web): open the Device side panel through the panel host
github-actions[bot] Oct 4, 2026
df78494
refactor(web): open the pull request side panels through the panel host
github-actions[bot] Oct 4, 2026
d1554f5
refactor(web): open the Files side panel through the panel host
github-actions[bot] Oct 4, 2026
7f84c56
fix(web): drop Files actions that settle after leaving the thread
github-actions[bot] Oct 4, 2026
8136ad4
fix(web): open tree clicks in the thread the Files panel shows now
github-actions[bot] Oct 6, 2026
6d65c39
fix(web): keep a Files browser open across composer draft switches
github-actions[bot] Oct 6, 2026
3be1e97
fix(web): point tree clicks at the new thread as soon as it commits
github-actions[bot] Oct 10, 2026
3d2de4d
feat(pi): show extension statuses in the thread header
github-actions[bot] Oct 4, 2026
890b0c5
fix(pi): keep statuses when Pi refuses a rollback fork
github-actions[bot] Oct 6, 2026
ee45b12
feat(mobile): show Pi extension statuses under the thread header
github-actions[bot] Oct 4, 2026
d4e14af
feat(server): run consented local plugins in supervised child processes
github-actions[bot] Oct 7, 2026
967fa41
fix(server): answer every plugin result with a message the server can…
github-actions[bot] Oct 6, 2026
c98af8e
fix(server): drop what a plugin registered before its activation failed
github-actions[bot] Oct 6, 2026
0ebc19d
fix(server): exit a plugin child on a corrupt IPC line and cover its …
github-actions[bot] Oct 7, 2026
f732076
fix(server): hold a plugin's partial IPC line in one growing buffer
github-actions[bot] Oct 7, 2026
8031b1c
fix(server): hold plugin calls made during startup until enabled plug…
github-actions[bot] Oct 7, 2026
d099322
fix(server): count a plugin IPC line's newline against the read budget
github-actions[bot] Oct 7, 2026
b0e67fd
fix(server): restore enabled plugins before any management step at st…
github-actions[bot] Oct 10, 2026
af2e2f4
fix(server): let calls waiting on a plugin start hear how it ended
github-actions[bot] Oct 10, 2026
7861dd7
fix(server): refuse a plugin file swapped for a FIFO while digesting
github-actions[bot] Oct 10, 2026
b8617ec
chore(server): say why plugin host modules import Node builtins
github-actions[bot] Oct 10, 2026
1bc3012
fix(server): answer a plugin call cancelled before its handler started
github-actions[bot] Oct 10, 2026
c9f3193
test(server): start the cooperative handler before timing it out
github-actions[bot] Oct 10, 2026
21b8a3d
fix(server): close a dead plugin's stderr that a process it started h…
github-actions[bot] Oct 10, 2026
d5a0db8
test(server): answer a replayed request only once its card is projected
github-actions[bot] Oct 10, 2026
aefe6e5
feat(server): deliver finished runs to plugins from an acknowledged c…
github-actions[bot] Oct 4, 2026
a2c51b2
fix(server): let a run's queued checkpoint capture decide its finaliz…
github-actions[bot] Oct 6, 2026
c94efe0
fix(server): refuse the events capability without the proposed API op…
github-actions[bot] Oct 6, 2026
8f86852
fix(server): drop a plugin's event handlers when its activation fails
github-actions[bot] Oct 7, 2026
3fa503e
test(server): cover finalization of a run stopped through the ownersh…
github-actions[bot] Oct 7, 2026
6c9333a
feat(server): let agents list and call plugin tools through MCP
github-actions[bot] Oct 7, 2026
06e4fcb
fix(server): list no plugins to MCP callers outside a T3 thread
github-actions[bot] Oct 7, 2026
ff7f887
fix(server): drop a reused credential's reservation when its grant up…
github-actions[bot] Oct 7, 2026
93566f6
docs(plugins): say plugins run under the server's account
github-actions[bot] Oct 7, 2026
d57a1d6
fix(server): drop a reused credential's reservation when its reuse ch…
github-actions[bot] Oct 7, 2026
ce235d8
feat(server): typed plugin settings, write-only secrets and plugin st…
github-actions[bot] Oct 7, 2026
12983f3
docs(plugins): say plugins run under the server's account
github-actions[bot] Oct 7, 2026
dfb6a92
fix(server): refresh open plugin settings when the declared fields ch…
github-actions[bot] Oct 10, 2026
34383e7
fix(server): finish a plugin's host work before disable returns
github-actions[bot] Oct 10, 2026
910a13b
fix(server): wake host calls waiting on a plugin below the stream buffer
github-actions[bot] Oct 10, 2026
a9c53af
feat: offer plugin actions in the palette, slash menu and thread menus
github-actions[bot] Oct 7, 2026
bbfea1b
fix(contracts): keep a plugin action when only its placement is unknown
github-actions[bot] Oct 6, 2026
9192987
fix: match plugin action names in the slash menu regardless of case
github-actions[bot] Oct 6, 2026
2103ba6
docs(plugins): say the slash menu opens only at the start of a line
github-actions[bot] Oct 7, 2026
19a7f90
fix(web,mobile): offer plugin actions only to connections that can ru…
github-actions[bot] Oct 7, 2026
047b5aa
test(web): stub plugin actions in the thread action menu hook test
github-actions[bot] Oct 7, 2026
cc348fb
fix(web,mobile): check the live permission when a plugin action runs
github-actions[bot] Oct 7, 2026
f38868a
docs(plugins): say plugins run under the server's account
github-actions[bot] Oct 7, 2026
5e1ce8d
fix(mobile): offer environment plugin actions in the palette without …
github-actions[bot] Oct 7, 2026
4f7e07b
feat: show plugin views as isolated right-panel tabs on web and desktop
github-actions[bot] Oct 7, 2026
06a6f80
fix: keep plugin view rate limits and navigation logs safe
github-actions[bot] Oct 6, 2026
4632ecb
fix(server): refuse the views capability without the proposed API opt-in
github-actions[bot] Oct 6, 2026
e281442
fix(web): keep the side-panel plugin view list stable across renders
github-actions[bot] Oct 10, 2026
52bf99f
fix(server): type WebSocket handlers against the uninstrumented RPC g…
github-actions[bot] Oct 9, 2026
70fb198
fix(server): serve plugin view files whose names start with two dots
github-actions[bot] Oct 10, 2026
7d1e12a
fix(server): serve only plugin view bytes the approval digest covered
github-actions[bot] Oct 10, 2026
7f230bd
feat(server): install plugins from npm with integrity checks and stag…
github-actions[bot] Oct 7, 2026
5206d75
fix(server): accept plain http npm registries only on this machine
github-actions[bot] Oct 6, 2026
b6bc045
fix(server): hold npm registry redirects and saved registries to the …
github-actions[bot] Oct 7, 2026
cae5a0c
fix(server): read npm tarball headers the way tar writers mean them
github-actions[bot] Oct 10, 2026
6af8fcf
fix(server): list a staged npm update's tools, settings, and actions
github-actions[bot] Oct 10, 2026
f873d31
fix(server): size the npm unpack cap by archive entries, not files
github-actions[bot] Oct 10, 2026
6739297
fix(server): refuse a pax record that does not end in a newline
github-actions[bot] Oct 10, 2026
b015e65
refactor(server): follow the Effect service rules in npm plugin installs
github-actions[bot] Oct 10, 2026
f3f0706
fix(server): size a GNU long name by its own header in npm tarballs
github-actions[bot] Oct 10, 2026
cc7119c
fix(server): refuse npm tarball paths with a NUL byte
github-actions[bot] Oct 10, 2026
42494e1
fix(server): report an applied npm update whose plugin could not be e…
github-actions[bot] Oct 10, 2026
fec5fa9
fix(server): refuse an npm update to the files already installed
github-actions[bot] Oct 10, 2026
0d32e32
feat(web,mobile): manage plugins in Settings
github-actions[bot] Oct 4, 2026
caa9ec9
docs(plugins): say plugins run under the server's account
github-actions[bot] Oct 7, 2026
cc15aac
fix(mobile): ask to select an environment when none is selected for p…
github-actions[bot] Oct 10, 2026
cc1f8c5
feat(web,mobile): fill in plugin settings from Settings
github-actions[bot] Oct 4, 2026
8a79463
feat(web,mobile): install and update plugins from npm in Settings
github-actions[bot] Oct 4, 2026
1d7af3b
fix(client-runtime): show a downloaded same-version plugin update in …
github-actions[bot] Oct 10, 2026
2e1d06e
feat(web,mobile): show what each plugin adds and what its capabilitie…
github-actions[bot] Oct 4, 2026
072afff
feat(server,web,mobile): let plugins show statuses on threads
github-actions[bot] Oct 4, 2026
9e77cde
fix(server): tell a plugin when the thread has no room for its status
github-actions[bot] Oct 6, 2026
ec2202d
feat(server,web,mobile): let plugins send notifications
github-actions[bot] Oct 7, 2026
3081aa4
fix(mobile): announce plugin notification banners to screen readers
github-actions[bot] Oct 6, 2026
7d0a8ad
refactor(server): follow the Effect service rules in plugin notificat…
github-actions[bot] Oct 10, 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 @@ -33,6 +33,7 @@ import * as DesktopClientSettings from "../settings/DesktopClientSettings.ts";
import * as ElectronApp from "../electron/ElectronApp.ts";
import * as DesktopRendererHistory from "../telemetry/DesktopRendererHistory.ts";
import { makeQuitShortcutHandler } from "./QuitHold.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 @@ -647,6 +648,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 @@ -33,6 +33,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 @@ -95,6 +96,10 @@ import {
SettingsScheduledTaskNewRouteScreen,
SettingsScheduledTaskEditRouteScreen,
} from "./features/settings/SettingsScheduledTasksRouteScreen";
import {
SettingsPluginRouteScreen,
SettingsPluginsRouteScreen,
} from "./features/settings/SettingsPluginsRouteScreen";
import {
ScheduledTaskModelPickerRouteScreen,
ScheduledTaskBranchPickerRouteScreen,
Expand Down Expand Up @@ -326,6 +331,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 @@ -634,6 +648,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