Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
60a4d23
feat(agent): steer active turns with Alt+Enter
zszz3 Sep 12, 2026
cda9a18
chore(repo): sync upstream main while preserving local steering
zszz3 Sep 13, 2026
d5658a1
chore(repo): integrate the rebased steering contribution
zszz3 Sep 13, 2026
04d40ec
feat(search): find sessions by message content
zszz3 Sep 13, 2026
430eda5
fix(search): show and highlight the matching sentence
zszz3 Sep 13, 2026
234e56d
fix(search): open results in the original conversation
zszz3 Sep 13, 2026
677599b
fix(search): jump to matching text in the original transcript
zszz3 Sep 13, 2026
381f3d9
feat(tray): add bounded session shortcuts
zszz3 Sep 13, 2026
e7cdbb7
fix(search): unify transcript reading and resolve nested hits
zszz3 Sep 13, 2026
f4541b9
docs(search): record unified transcript reading ownership
zszz3 Sep 13, 2026
c8206bd
chore(repo): reconcile published search with local main
zszz3 Sep 13, 2026
64f7209
feat(sidebar): group pinned conversations above dated history
zszz3 Sep 13, 2026
de1520f
chore(sync): integrate latest upstream main
zszz3 Sep 13, 2026
1b0dcc0
chore(repo): reconcile global pins contribution with local main
zszz3 Sep 13, 2026
ff96cee
chore(repo): integrate search conflict resolution with local main
zszz3 Sep 14, 2026
dc08568
chore(repo): reconcile global pins conflict resolution with local main
zszz3 Sep 14, 2026
9dc64ec
chore(repo): retain concurrent main integration
zszz3 Sep 14, 2026
3e16dad
chore(repo): sync upstream main through v0.14.8
zszz3 Sep 14, 2026
b040499
chore(repo): sync upstream main with native Pi sessions
zszz3 Sep 15, 2026
56c5e91
feat(tray): share the session row budget across groups
zszz3 Sep 15, 2026
57f9734
refactor(desktop): hold the error-code union in one module
zszz3 Sep 15, 2026
8ea0199
Merge remote-tracking branch 'upstream/main'
zszz3 Sep 15, 2026
e858f72
docs(test): mirror the model-fallback index rows in Chinese
zszz3 Sep 15, 2026
5629a1f
Merge remote-tracking branch 'upstream/main'
zszz3 Sep 15, 2026
df1b4ab
feat(provider): support adaptive thinking protocols
zszz3 Sep 27, 2026
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
78 changes: 73 additions & 5 deletions apps/desktop/electron/main/bootstrap/app-lifecycle.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { app, BrowserWindow, Menu, nativeImage, nativeTheme, Tray } from "electron";
import {
app, BrowserWindow, Menu, nativeImage, nativeTheme, Tray,
type MenuItemConstructorOptions,
} from "electron";
import { existsSync } from "node:fs";
import { join } from "node:path";
import {
APP_MENU_COMMANDS,
APP_NAME,
IPC,
isThemeColorScheme,
traySessionTitle,
type AppMenuCommand,
type CloseBehavior,
type KeybindingOverrides,
type NativeMenuAction,
} from "@pi-desktop/shared";
import { catalogs, resolveLocale } from "@pi-desktop/i18n";
import { installApplicationMenu } from "../application-menu";
import { createTraySessions } from "../tray-sessions";
import type { HostProcess } from "../host-process";
import { createWindow, type WindowLifecycleState } from "./window";
import type { BrowserPane } from "../browser-view";
import type { Logger } from "../logger";
Expand Down Expand Up @@ -64,6 +70,8 @@ export type ApplicationLifecycleDependencies = {
applyPluginLauncherShortcut: (keybindings?: KeybindingOverrides) => void;
applySummonWindowShortcut: (keybindings?: KeybindingOverrides) => void;
broadcastPluginPanelEvent: (event: string, payload: unknown) => void;
getHost: () => HostProcess | null;
getRunningSessionIds: () => Iterable<string>;
};

export function createApplicationLifecycle({
Expand Down Expand Up @@ -96,7 +104,17 @@ export function createApplicationLifecycle({
applyPluginLauncherShortcut,
applySummonWindowShortcut,
broadcastPluginPanelEvent,
getHost,
getRunningSessionIds,
}: ApplicationLifecycleDependencies) {
const traySessions = createTraySessions({
getHost,
getRunningSessionIds,
isQuitting: () => state.quitting,
onChanged: () => updateTrayMenu(),
logger,
});
let trayActivationGeneration = 0;

function applyDevelopmentBranding() {
if (process.platform !== "darwin" || !isDevelopmentBuild || !app.dock) return;
Expand Down Expand Up @@ -149,12 +167,56 @@ export function createApplicationLifecycle({
});
}

function updateTrayMenu(locale = app.getLocale()) {
async function activateTraySession(sessionId: string | null) {
const generation = ++trayActivationGeneration;
await ensureWindow();
const window = state.mainWindow;
if (!window || window.isDestroyed() || !(await waitForMenuRenderer(window))) return;
if (generation !== trayActivationGeneration) return;
if (window.isMinimized()) window.restore();
window.show();
window.focus();
if (sessionId) await traySessions.refresh();
if (
generation !== trayActivationGeneration ||
window !== state.mainWindow ||
window.isDestroyed()
) return;
if (sessionId && !traySessions.canActivate(sessionId)) return;
sendToRenderer(IPC.event.traySessionActivated, { sessionId });
}

function dispatchTrayActivation(sessionId: string | null) {
void activateTraySession(sessionId).catch((error) => {
logger.app("diagnostics", "error", "tray session activation failed", {
data: String(error),
});
});
}

function updateTrayMenu(locale = appearanceState.updaterLocale || app.getLocale()) {
if (!state.tray) return;
const labels = catalogs[resolveLocale(locale)].tray;
const catalog = catalogs[resolveLocale(locale)];
const labels = catalog.tray;
const template: MenuItemConstructorOptions[] = [
{ label: labels.open, click: restoreMainWindow },
];
for (const group of traySessions.getGroups()) {
template.push({ type: "separator" }, { label: labels[group.kind], enabled: false });
for (const session of group.sessions) {
const title = traySessionTitle(session.title, catalog.chat.untitledTask);
template.push({
label: process.platform === "darwin" ? title : title.replace(/&/g, "&&"),
click: () => dispatchTrayActivation(session.id),
});
}
if (group.hasMore) {
template.push({ label: labels.viewMore, click: () => dispatchTrayActivation(null) });
}
}
state.tray.setContextMenu(
Menu.buildFromTemplate([
{ label: labels.open, click: restoreMainWindow },
...template,
{ type: "separator" },
{ label: labels.quit, click: () => app.quit() },
]),
Expand Down Expand Up @@ -186,9 +248,14 @@ export function createApplicationLifecycle({

state.tray = new Tray(icon);
state.tray.setToolTip(APP_NAME);
state.tray.on("click", restoreMainWindow);
// macOS single-click opens its attached menu without focusing/reading a conversation.
if (process.platform !== "darwin") state.tray.on("click", restoreMainWindow);
state.tray.on("double-click", restoreMainWindow);
// User access retries a transient Host read failure without a polling timer.
state.tray.on("mouse-enter", () => { void traySessions.refresh(); });
state.tray.on("right-click", () => { void traySessions.refresh(); });
updateTrayMenu();
void traySessions.refresh();
}


Expand Down Expand Up @@ -526,6 +593,7 @@ export function createApplicationLifecycle({
}

return {
traySessions,
applyDevelopmentBranding,
hasVisibleWindow,
restoreMainWindow,
Expand Down
39 changes: 39 additions & 0 deletions apps/desktop/electron/main/error-codes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* The desktop error-code union and the helpers that read an error's shape.
*
* These live outside `index.ts` so every main-process module can import them
* directly. Both the union and `isHostUnavailable` were previously copied into
* `index.ts` and `runtime/session-launch.ts`, and `describeError` was threaded
* down to the IPC and plugin layers as an injected function.
*/

import { ErrorCodes as SharedErrorCodes } from "@pi-desktop/shared";

// The shared error-code union is reconciled in the shared lane. Keep desktop
// source type-safe while that lane is temporarily staged at main.
export const ErrorCodes = {
...SharedErrorCodes,
COMMAND_SHELL_INVALID: "COMMAND_SHELL_INVALID",
SHELL_NOT_FOUND: "SHELL_NOT_FOUND",
PLAN_EXECUTION_INTERRUPTED: "PLAN_EXECUTION_INTERRUPTED",
PLAN_PERMISSION_MODE_REQUIRED: "PLAN_PERMISSION_MODE_REQUIRED",
} as const;

/** One-line message for an error of unknown shape, for user-facing lists. */
export function describeError(error: unknown): string {
if (error instanceof Error) return error.message.slice(0, 300);
return String(error).slice(0, 300);
}

/**
* True when a rejection only says the host transport is gone (D080): the call
* lost a race with shutdown, a crash, or a supervised restart. Every such
* rejection carries `HOST_UNAVAILABLE`, whether it was refused before it was
* sent or was in flight when the transport closed.
*/
export function isHostUnavailable(error: unknown): boolean {
return (
(error as { errorCode?: string } | null | undefined)?.errorCode ===
ErrorCodes.HOST_UNAVAILABLE
);
}
35 changes: 5 additions & 30 deletions apps/desktop/electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "electron";
import { join } from "node:path";
import { homedir } from "node:os";
import { describeError, isHostUnavailable } from "./error-codes";
import {
applyNetworkProxyFromAppSettings,
currentNetworkProxy,
Expand All @@ -17,7 +18,6 @@ import {
APP_ID,
APP_NAME,
APP_VERSION,
ErrorCodes as SharedErrorCodes,
IPC,
IPC_WHITELIST,
KEYBOARD_SHORTCUTS,
Expand Down Expand Up @@ -161,16 +161,6 @@ import { registerPluginUiIpc } from "./ipc/plugin-ui-ipc";
import { registerSkillsIpc } from "./ipc/skills-ipc";
import { stripWinLongPrefix } from "./path-utils";

// The shared error-code union is reconciled in the shared lane. Keep desktop
// source type-safe while that lane is temporarily staged at main.
const ErrorCodes = {
...SharedErrorCodes,
COMMAND_SHELL_INVALID: "COMMAND_SHELL_INVALID",
SHELL_NOT_FOUND: "SHELL_NOT_FOUND",
PLAN_EXECUTION_INTERRUPTED: "PLAN_EXECUTION_INTERRUPTED",
PLAN_PERMISSION_MODE_REQUIRED: "PLAN_PERMISSION_MODE_REQUIRED",
} as const;

// A closed stdout/stderr (Linux AppImage, GUI launch without a TTY) must not
// surface as Electron's "Uncaught Exception: write EPIPE" dialog. The same
// default dialog must not appear for a stray uncaughtException (non-ASCII
Expand Down Expand Up @@ -795,27 +785,9 @@ function setCurrentWorkspacePath(path: string | null): void {
}
}

/** One-line message for an error of unknown shape, for user-facing lists. */
function describeError(error: unknown): string {
if (error instanceof Error) return error.message.slice(0, 300);
return String(error).slice(0, 300);
}

/**
* True when a rejection only says the host transport is gone (D080): the call
* lost a race with shutdown, a crash, or a supervised restart. Every such
* rejection carries `HOST_UNAVAILABLE`, whether it was refused before it was
* sent or was in flight when the transport closed.
*/
function isHostUnavailable(error: unknown): boolean {
return (
(error as { errorCode?: string } | null | undefined)?.errorCode ===
ErrorCodes.HOST_UNAVAILABLE
);
}

/** Pull the user's MCP server records from host-core into the local runtime. */
function sendToRenderer(channel: string, payload: unknown) {
applicationLifecycle?.traySessions.observeEvent(channel, payload);
if (channel === IPC.event.pluginChanged) {
applicationLifecycle?.applyNativeThemeSource({
theme: applicationAppearanceState.appThemePreference,
Expand Down Expand Up @@ -900,6 +872,8 @@ const applicationAppearanceState: ApplicationAppearanceState = {
};

applicationLifecycle = createApplicationLifecycle({
getHost: () => host,
getRunningSessionIds: () => activeTurns.keys(),
state: windowLifecycleState,
appState: applicationLifecycleState,
appearanceState: applicationAppearanceState,
Expand Down Expand Up @@ -1245,6 +1219,7 @@ const { bootHostStatus, runtimeArch, bootBackends } = runtimeLifecycle;

function registerIpc() {
return registerIpcHandlers({
traySessions: applicationLifecycle!.traySessions,
ipcMain,
getMainWindow: () => mainWindow,
getHost: () => host,
Expand Down
13 changes: 11 additions & 2 deletions apps/desktop/electron/main/ipc/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ import { registerWindowIpc } from "./window-ipc";
import { createComposerTemplateLoader, registerWorkspaceIpc } from "./workspace-ipc";
import { registerComposerIpc } from "./composer-ipc";
import type { IpcRegistrar } from "./types";
import type { createTraySessions } from "../tray-sessions";

export type RegisterIpcDependencies = {
ipcMain: IpcMain;
getMainWindow: () => BrowserWindow | null;
getHost: () => HostProcess | null;
traySessions: ReturnType<typeof createTraySessions>;
getSidecar: () => AgentSidecar | null;
getAgentHostBridge: () => AgentHostBridge | null;
getNotificationViewingSessionId: () => string | null;
Expand Down Expand Up @@ -99,6 +101,7 @@ export function registerIpcHandlers(dependencies: RegisterIpcDependencies) {
applyCloseBehavior,
getCloseBehavior,
markMenuRendererReady,
traySessions,
executeNativeMenuAction,
scheduledRunsBySession,
isDevelopmentBuild,
Expand Down Expand Up @@ -135,8 +138,13 @@ export function registerIpcHandlers(dependencies: RegisterIpcDependencies) {

const ipcHandlers = new Map<string, (...args: any[]) => Promise<any>>();
const handle = (channel: string, fn: (...args: any[]) => Promise<any>) => {
ipcHandlers.set(channel, fn);
ipcMain.handle(channel, async (_event, ...args) => wrap(() => fn(...args)));
const handler = async (...args: any[]) => {
const result = await fn(...args);
traySessions.observeInvoke(channel);
return result;
};
ipcHandlers.set(channel, handler);
ipcMain.handle(channel, async (_event, ...args) => wrap(() => handler(...args)));
};
const handleWithEvent = (
channel: string,
Expand Down Expand Up @@ -241,6 +249,7 @@ export function registerIpcHandlers(dependencies: RegisterIpcDependencies) {
loadComposerTemplatesCached,
});
registerWindowIpc({
setTraySessionPreferences: traySessions.setPreferences,
registrar,
getMainWindow,
getWorkPanelReservationWidth,
Expand Down
16 changes: 16 additions & 0 deletions apps/desktop/electron/main/ipc/window-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
isThemeColorScheme,
NATIVE_MENU_ACTIONS,
WINDOW_CONTROL_ACTIONS,
parseTraySessionPreferences,
type NativeMenuAction,
type WindowControlAction,
type TraySessionPreferences,
} from "@pi-desktop/shared";
import {
emptyWorkPanelReservationState,
Expand All @@ -29,6 +31,7 @@ export type WindowIpcDependencies = {
getCloseBehavior: () => "ask" | "tray" | "quit";
markMenuRendererReady: (window: BrowserWindow) => boolean;
executeNativeMenuAction: (action: NativeMenuAction) => unknown;
setTraySessionPreferences: (preferences: TraySessionPreferences) => Promise<void>;
};

/** Register renderer-drawn window chrome and work-panel geometry channels. */
Expand All @@ -43,9 +46,22 @@ export function registerWindowIpc({
getCloseBehavior,
markMenuRendererReady,
executeNativeMenuAction,
setTraySessionPreferences,
}: WindowIpcDependencies): void {
const { handle, handleWithEvent } = registrar;

handleWithEvent(IPC.invoke.traySetSessionPreferences, async (event, input: unknown) => {
registrar.assertMainWindowSender(event);
const preferences = parseTraySessionPreferences(input);
if (!preferences) {
throw Object.assign(new Error("invalid tray session preferences"), {
errorCode: ErrorCodes.INVALID_ARGUMENT,
});
}
await setTraySessionPreferences(preferences);
return { ok: true };
});

handle(IPC.invoke.windowSetWorkPanelReservation, async (input: unknown = {}) => {
const requested = parseWorkPanelReservationWidth(input);
if (requested === null) {
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/electron/main/mcp-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ export const MCP_CONTROL_BLOCKED_CHANNEL_KEYS = [
"composerPasteFiles",
"clipboardRecordPaste",
"menuRendererReady",
"traySetSessionPreferences",
"providersCreate",
"providersUpdate",
"providersDelete",
Expand Down
Loading