Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ extension AgentPaneModel {
]
/// The arguments a connect action runs with.
static let connectArguments: [String: [String: String]] = ["palette.openCloudPane": ["route": "/get"]]
/// The command palette pages any agent tab may open: the chats page (Show all, the + menu's
/// Search chats) and the History search page (the + menu's Search pages). They open the app's own
/// palette, as Cmd-K does, so they need no gesture credit.
static let paletteActions: Set<String> = ["agentPane.searchChats", "history.search"]
/// What runs when a connect is refused: signed out, cmux Cloud… starts sign-in.
static let connectFallbacks: [String: String] = ["newCloudMachine": "palette.auth.signIn"]

Expand All @@ -25,7 +29,7 @@ extension AgentPaneModel {
/// no fallback that ran, is an `unavailable` failure, so the page can say so; one that ran and
/// then stopped reports itself in the app (its refusal or toast).
func runAction(_ id: String) -> [String: Any] {
guard id == "newTab.page" || id == "agentPane.searchChats" || Self.connectActions.contains(id),
guard id == "newTab.page" || Self.paletteActions.contains(id) || Self.connectActions.contains(id),
let onRunAction else {
return Self.unsupported("action.run")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ struct AgentPaneConnectActionTests {
#expect(ran == ["cloudMove", "cloudBringBack"])
}

/// The + menu's Search chats and Search pages open the command palette's chats and History
/// search pages without a gesture; an action outside the pane's table stays refused.
@Test func paletteSearchPagesRunWithoutAGesture() async {
let model = AgentPaneModel(host: NoHost())
var ran: [String] = []
model.onRunAction = { id, args in ran.append(id); #expect(args.isEmpty); return true }
#expect(await model.respond(to: .runAction("agentPane.searchChats"))["ok"] as? Bool == true)
#expect(await model.respond(to: .runAction("history.search"))["ok"] as? Bool == true)
#expect(await model.respond(to: .runAction("searchFiles"))["ok"] as? Bool != true)
#expect(ran == ["agentPane.searchChats", "history.search"])
}

/// An action the app did not run (turned off, or not available for this chat) is a failure, so
/// the page can say so instead of nothing happening.
@Test func anActionTheAppDidNotRunIsAFailure() async {
Expand Down
17 changes: 17 additions & 0 deletions webviews/src/agent-session/acpmux/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import React, {
import { AnsweredBy, AuthorChip } from "./multiplayer/Faces";
import { LivePresenceStack, LiveTypingLine } from "./multiplayer/LivePresence";
import { flushSync } from "react-dom";
import { composerPlusSearchExtensions, PLUS_SEARCH_ACTIONS } from "./composerPlusRegistry";
import { CONNECT_ACTIONS, savedEnvironmentHost, type ConnectKind, type OffloadKind } from "./environments";
import { chiefEnvelope } from "./conversation/chiefEnvelope";
import { restoreForkSource } from "./handoff/restoreForkSource";
Expand Down Expand Up @@ -6296,6 +6297,19 @@ function AcpmuxPane() {
// Show all chats opens the command palette's chats page (agentPane.searchChats, decision K1:
// one palette). The host pushes the live bindings through applyShortcuts, so labels follow a rebind.
const showAllChats = () => void callNative("action.run", { id: "agentPane.searchChats" }).catch(() => undefined);
// The + menu's Search chats and Search pages (Leo: "the chatgpt desktop app, let's do their +
// symbol's capabiliities for our composer"): the command palette's chats page and its History
// search page, both app actions the pane may run (AgentPaneModel.paletteActions). A refusal is
// said as the composer's notice, never dropped.
const plusSearchExtensions = useMemo(() => composerPlusSearchExtensions(t), [t]);
const runPlusSearch = (id: string) => {
const name = id === PLUS_SEARCH_ACTIONS.pages ? t("composer.plusSearchPages") : t("sidebar.search");
void callNative("action.run", { id }).catch(() =>
directClient.current?.notice(translate("composer.offloadNotRun", { action: name })),
);
};
// New Tab shows no transcript, so the page says a refusal itself (NewTabScreen plusRefused).
const runNewTabPlusSearch = (id: string) => callNative("action.run", { id });
const [shortcuts, setShortcuts] = useState<ShortcutLabels>({});
// SSH… opens Connect to Machine; New Cloud Machine opens the host's create flow (Lawrence 2026-10-06).
const connectMachine = (kind: ConnectKind) =>
Expand Down Expand Up @@ -10265,6 +10279,8 @@ function AcpmuxPane() {
onMode={(modeId) => callNative("chat.mode", { modeId })}
// Without a folder there is nothing to search; the + menu leaves the item out.
searchFiles={fileRoot ? searchFiles : undefined}
plusExtensions={plusSearchExtensions}
onPlusCommand={runPlusSearch}
capture={captureScreen}
upload={uploadFiles}
// A chat on this Mac (as Sources' + below); the host refuses any other.
Expand Down Expand Up @@ -10623,6 +10639,7 @@ function AcpmuxPane() {
}
chips={registryChips ?? NewTabComposerChips}
focusField={newTab.focusesField !== false}
onPlusCommand={runNewTabPlusSearch}
{...withSubmitAfterHandshake(
newTabScreenActions({
callNative,
Expand Down
2 changes: 1 addition & 1 deletion webviews/src/agent-session/acpmux/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1955,7 +1955,7 @@ export function Composer({
? t("sidebar.sessions")
: section === "tabs"
? (t as unknown as (key: string) => string)("newTab.openTabs")
: t("files.search"),
: t("sidebar.searchPlaceholder"),
choices: entries.map((entry): Choice => ({
id: entry.id,
name: entry.name,
Expand Down
Loading
Loading