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
2 changes: 2 additions & 0 deletions apps/desktop/electron/app-store-persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface PersistedUiState {
readonly appGlobalModelSettings?: ModelSettingsSnapshot;
readonly sidebarCollapsed?: boolean;
readonly allowMultiple?: boolean;
readonly enableTransparency?: boolean;
}

export interface LegacyPersistedUiState extends PersistedUiState {
Expand Down Expand Up @@ -74,6 +75,7 @@ export async function readPersistedUiState(uiStateFilePath: string): Promise<Leg
appGlobalModelSettings: toPersistedModelSettingsSnapshot(parsed.appGlobalModelSettings),
sidebarCollapsed: typeof parsed.sidebarCollapsed === "boolean" ? parsed.sidebarCollapsed : undefined,
allowMultiple: typeof parsed.allowMultiple === "boolean" ? parsed.allowMultiple : undefined,
enableTransparency: typeof parsed.enableTransparency === "boolean" ? parsed.enableTransparency : undefined,
composerAttachmentsBySession: parsed.composerAttachmentsBySession,
transcripts: parsed.transcripts,
};
Expand Down
47 changes: 42 additions & 5 deletions apps/desktop/electron/app-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export class DesktopAppStore implements AppStoreInternals {
private readonly getWindow: () => BrowserWindow | null;
private persistUiStateTimer: NodeJS.Timeout | undefined;
private readonly transcriptPersistTimers = new Map<string, NodeJS.Timeout>();
private readonly restoredSelectedSessionKeysAwaitingSelection = new Set<string>();
private initPromise: Promise<void> | undefined;
private selectionEpoch = 0;
private refreshStateDepth = 0;
Expand Down Expand Up @@ -493,6 +494,21 @@ export class DesktopAppStore implements AppStoreInternals {
return this.emit();
}

async setEnableTransparency(enabled: boolean): Promise<DesktopAppState> {
await this.initialize();
if (this.state.enableTransparency === enabled) {
return structuredClone(this.state);
}
this.state = {
...this.state,
enableTransparency: enabled,
lastError: undefined,
revision: this.state.revision + 1,
};
await this.persistUiState();
return this.emit();
}

async setAllowMultiple(allowMultiple: boolean): Promise<DesktopAppState> {
await this.initialize();
if (this.state.allowMultiple === allowMultiple) {
Expand Down Expand Up @@ -762,6 +778,7 @@ export class DesktopAppStore implements AppStoreInternals {
workspaceOrder: persisted.workspaceOrder ?? [],
sidebarCollapsed: persisted.sidebarCollapsed ?? this.state.sidebarCollapsed,
allowMultiple: persisted.allowMultiple ?? this.state.allowMultiple,
enableTransparency: persisted.enableTransparency ?? this.state.enableTransparency,
};
await this.migrateLegacyPersistence(persisted);
this.sessionState.lastViewedAtBySession.clear();
Expand Down Expand Up @@ -807,12 +824,18 @@ export class DesktopAppStore implements AppStoreInternals {
clearLastError: true,
refreshWorktrees: true,
hydrateSelectedSession: false,
markSelectedSessionViewed: false,
});
this.startSelectedSessionHydration(this.selectedSessionRef());
const restoredSessionRef = this.selectedSessionRef();
if (restoredSessionRef && persisted.selectedWorkspaceId && persisted.selectedSessionId) {
this.restoredSelectedSessionKeysAwaitingSelection.add(sessionKey(restoredSessionRef));
}
this.startSelectedSessionHydration(restoredSessionRef, { markViewed: false });
} catch (error) {
this.state = {
...createEmptyDesktopAppState(),
allowMultiple: persisted.allowMultiple ?? false,
enableTransparency: persisted.enableTransparency ?? false,
lastError: error instanceof Error ? error.message : String(error),
revision: 1,
};
Expand Down Expand Up @@ -1707,6 +1730,7 @@ export class DesktopAppStore implements AppStoreInternals {
appGlobalModelSettings: hasStoredModelSettings(this.state.globalModelSettings) ? this.state.globalModelSettings : undefined,
sidebarCollapsed: this.state.sidebarCollapsed || undefined,
allowMultiple: this.state.allowMultiple,
enableTransparency: this.state.enableTransparency,
};

await writePersistedUiState(this.uiStateFilePath, payload);
Expand Down Expand Up @@ -1892,6 +1916,7 @@ export class DesktopAppStore implements AppStoreInternals {
}

private applyFastSessionSelection(sessionRef: SessionRef): DesktopAppState {
this.restoredSelectedSessionKeysAwaitingSelection.delete(sessionKey(sessionRef));
this.state = {
...this.state,
selectedWorkspaceId: sessionRef.workspaceId,
Expand All @@ -1913,7 +1938,11 @@ export class DesktopAppStore implements AppStoreInternals {
return snapshot;
}

private async hydrateSelectedSessionAfterSelection(sessionRef: SessionRef, selectionEpoch: number): Promise<void> {
private async hydrateSelectedSessionAfterSelection(
sessionRef: SessionRef,
selectionEpoch: number,
options: { readonly markViewed?: boolean } = {},
): Promise<void> {
const runtimeMissing = !this.runtimeByWorkspace.has(sessionRef.workspaceId);
const [snapshot] = await Promise.all([
this.ensureSessionReady(sessionRef),
Expand All @@ -1932,19 +1961,24 @@ export class DesktopAppStore implements AppStoreInternals {

this.clearSessionError(sessionRef);
this.state = this.syncSelectedSessionHydrationState(this.state, sessionRef, snapshot, runtimeByWorkspace);
this.markSessionViewed(sessionRef);
if (options.markViewed ?? true) {
this.markSessionViewed(sessionRef);
}
this.schedulePersistUiState();
this.emit();
this.publishSelectedTranscriptFor(sessionRef);
}

private startSelectedSessionHydration(sessionRef: SessionRef | undefined): void {
private startSelectedSessionHydration(
sessionRef: SessionRef | undefined,
options: { readonly markViewed?: boolean } = {},
): void {
if (!sessionRef) {
return;
}

const selectionEpoch = ++this.selectionEpoch;
void this.hydrateSelectedSessionAfterSelection(sessionRef, selectionEpoch).catch((error: unknown) => {
void this.hydrateSelectedSessionAfterSelection(sessionRef, selectionEpoch, options).catch((error: unknown) => {
void this.handleSelectedSessionHydrationError(sessionRef, selectionEpoch, error);
});
}
Expand Down Expand Up @@ -1984,6 +2018,9 @@ export class DesktopAppStore implements AppStoreInternals {
if (!isSessionActivelyViewed(this.state, sessionRef, this.getWindow())) {
return false;
}
if (this.restoredSelectedSessionKeysAwaitingSelection.has(sessionKey(sessionRef))) {
return false;
}

return this.markSessionViewed(sessionRef);
}
Expand Down
14 changes: 13 additions & 1 deletion apps/desktop/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,16 @@ function readClipboardImageAttachment(): ComposerImageAttachment | null {

function createWindow(): BrowserWindow {
const backgroundTestMode = windowTestMode === "background";
const enableTransparency = store ? store.state.enableTransparency : false;
const window = new BrowserWindow({
width: 1480,
height: 980,
minWidth: 1200,
minHeight: 760,
backgroundColor: "#f3f4f8",
transparent: enableTransparency,
vibrancy: process.platform === "darwin" && enableTransparency ? "under-window" : undefined,
titleBarStyle: "hiddenInset",
backgroundColor: enableTransparency ? "#00000000" : "#f3f4f8",
trafficLightPosition: { x: 18, y: 18 },
show: false,
icon: appIcon,
Expand Down Expand Up @@ -593,6 +596,15 @@ app.whenReady().then(async () => {
ipcMain.handle(desktopIpc.setAllowMultiple, (_event, allowMultiple: boolean) =>
store.setAllowMultiple(allowMultiple),
);
ipcMain.handle(desktopIpc.setEnableTransparency, async (_event, enabled: boolean) => {
const nextState = await store.setEnableTransparency(enabled);
if (mainWindow && !mainWindow.isDestroyed()) {
if (process.platform === "darwin") {
mainWindow.setVibrancy(enabled ? "under-window" : null);
}
}
return nextState;
});
ipcMain.handle(desktopIpc.terminalEnsurePanel, (event, workspaceId: string, terminalScopeId: string, size) => {
return getTerminalService().ensurePanel(event.sender, workspaceId, terminalScopeId, size);
});
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ contextBridge.exposeInMainWorld("piApp", {
ipcRenderer.invoke(desktopIpc.setIntegratedTerminalShell, shellPath) as Promise<DesktopAppState>,
setAllowMultiple: (allowMultiple: boolean) =>
ipcRenderer.invoke(desktopIpc.setAllowMultiple, allowMultiple) as Promise<DesktopAppState>,
setEnableTransparency: (enabled: boolean) =>
ipcRenderer.invoke(desktopIpc.setEnableTransparency, enabled) as Promise<DesktopAppState>,
ensureTerminalPanel: (workspaceId: string, terminalScopeId: string, size?: Partial<TerminalSize>) =>
ipcRenderer.invoke(desktopIpc.terminalEnsurePanel, workspaceId, terminalScopeId, size) as Promise<TerminalPanelSnapshot>,
createTerminalSession: (workspaceId: string, terminalScopeId: string, size?: Partial<TerminalSize>) =>
Expand Down
74 changes: 31 additions & 43 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,8 @@ export default function App() {
const handledComposerSyncNonceRef = useRef(0);
const [showJumpToLatest, setShowJumpToLatest] = useState(false);
const [showDiffPanel, setShowDiffPanel] = useState(false);
const [openTerminalSessionKeys, setOpenTerminalSessionKeys] = useState<ReadonlySet<string>>(() => new Set());
const [takeoverTerminalSessionKeys, setTakeoverTerminalSessionKeys] = useState<ReadonlySet<string>>(() => new Set());
const [openTerminalSessionKey, setOpenTerminalSessionKey] = useState("");
const [takeoverTerminalSessionKey, setTakeoverTerminalSessionKey] = useState("");
const [terminalHeight, setTerminalHeight] = useState(340);
const [diffFileRequest, setDiffFileRequest] = useState<DiffPanelFileRequest | null>(null);
const [timelinePaneMountVersion, setTimelinePaneMountVersion] = useState(0);
Expand Down Expand Up @@ -240,6 +240,12 @@ export default function App() {
return unsub;
}, []);

useEffect(() => {
if (snapshot) {
document.documentElement.classList.toggle("enable-transparency", snapshot.enableTransparency);
}
}, [snapshot?.enableTransparency]);

useEffect(() => {
const piApi = window.piApp;
if (!piApi?.onNotificationPermissionStatusChanged) {
Expand Down Expand Up @@ -372,8 +378,8 @@ export default function App() {
const editingQueuedMessageId = snapshot?.editingQueuedMessageId;
const runningLabel = useRunningLabel(selectedSession?.status === "running" ? selectedSession.runningSince : undefined);
const selectedSessionKey = selectedWorkspace && selectedSession ? `${selectedWorkspace.id}:${selectedSession.id}` : "";
const isTerminalVisibleForSelectedThread = Boolean(selectedSessionKey) && openTerminalSessionKeys.has(selectedSessionKey);
const isTerminalTakeoverForSelectedThread = Boolean(selectedSessionKey) && takeoverTerminalSessionKeys.has(selectedSessionKey);
const isTerminalVisibleForSelectedThread = Boolean(selectedSessionKey) && openTerminalSessionKey === selectedSessionKey;
const isTerminalTakeoverForSelectedThread = Boolean(selectedSessionKey) && takeoverTerminalSessionKey === selectedSessionKey;
const activeTranscript =
selectedTranscript &&
selectedWorkspace &&
Expand All @@ -394,10 +400,14 @@ export default function App() {
: [];
useEffect(() => {
if (snapshot && snapshot.workspaces.length === 0) {
setOpenTerminalSessionKeys(new Set());
setTakeoverTerminalSessionKeys(new Set());
setOpenTerminalSessionKey("");
setTakeoverTerminalSessionKey("");
}
}, [snapshot]);
useEffect(() => {
setOpenTerminalSessionKey("");
setTakeoverTerminalSessionKey("");
}, [selectedSessionKey]);
const selectedExtensionDock = useMemo(() => buildExtensionDockModel(selectedExtensionUi), [selectedExtensionUi]);
const displayedSessionTitle = selectedExtensionUi?.title ?? selectedSession?.title ?? "";
const activeExtensionDialog = selectedExtensionUi?.pendingDialogs[0];
Expand All @@ -416,21 +426,13 @@ export default function App() {
if (!selectedSessionKey) {
return;
}
if (openTerminalSessionKeys.has(selectedSessionKey)) {
setOpenTerminalSessionKeys((current) => {
const next = new Set(current);
next.delete(selectedSessionKey);
return next;
});
setTakeoverTerminalSessionKeys((current) => {
const next = new Set(current);
next.delete(selectedSessionKey);
return next;
});
if (openTerminalSessionKey === selectedSessionKey) {
setOpenTerminalSessionKey("");
setTakeoverTerminalSessionKey("");
return;
}
setOpenTerminalSessionKeys((current) => new Set(current).add(selectedSessionKey));
}, [openTerminalSessionKeys, selectedSessionKey]);
setOpenTerminalSessionKey(selectedSessionKey);
}, [openTerminalSessionKey, selectedSessionKey]);
const focusNewThreadComposer = () => {
window.requestAnimationFrame(() => {
newThreadComposerRef.current?.focus();
Expand Down Expand Up @@ -1282,34 +1284,14 @@ export default function App() {
isTakeover={isTerminalTakeoverForSelectedThread}
onHeightChange={(nextHeight) => {
setTerminalHeight(nextHeight);
setTakeoverTerminalSessionKeys((current) => {
const next = new Set(current);
next.delete(selectedSessionKey);
return next;
});
setTakeoverTerminalSessionKey((current) => (current === selectedSessionKey ? "" : current));
}}
onToggleTakeover={() => {
setTakeoverTerminalSessionKeys((current) => {
const next = new Set(current);
if (next.has(selectedSessionKey)) {
next.delete(selectedSessionKey);
} else {
next.add(selectedSessionKey);
}
return next;
});
setTakeoverTerminalSessionKey((current) => (current === selectedSessionKey ? "" : selectedSessionKey));
}}
onHide={() => {
setOpenTerminalSessionKeys((current) => {
const next = new Set(current);
next.delete(selectedSessionKey);
return next;
});
setTakeoverTerminalSessionKeys((current) => {
const next = new Set(current);
next.delete(selectedSessionKey);
return next;
});
setOpenTerminalSessionKey((current) => (current === selectedSessionKey ? "" : current));
setTakeoverTerminalSessionKey((current) => (current === selectedSessionKey ? "" : current));
focusComposer();
}}
/>
Expand Down Expand Up @@ -1704,6 +1686,8 @@ export default function App() {
};

const handleSelectSession = (target: { workspaceId: string; sessionId: string }) => {
setOpenTerminalSessionKey("");
setTakeoverTerminalSessionKey("");
void updateSnapshot(api, setSnapshot, () => api.selectSession(target)).then(() => {
focusComposer();
});
Expand Down Expand Up @@ -1917,6 +1901,7 @@ export default function App() {
integratedTerminalShell={snapshot.integratedTerminalShell}
allowMultiple={snapshot.allowMultiple}
themeMode={themeMode}
enableTransparency={snapshot.enableTransparency}
onLoginProvider={handleLoginProvider}
onLogoutProvider={handleLogoutProvider}
onSetProviderApiKey={handleSetProviderApiKey}
Expand All @@ -1932,6 +1917,9 @@ export default function App() {
onSetThemeMode={handleSetThemeMode}
onSetThinkingLevel={handleSetThinkingLevel}
onToggleSkillCommands={handleToggleSkillCommands}
onSetEnableTransparency={(enabled) => {
void updateSnapshot(api, setSnapshot, () => api.setEnableTransparency(enabled));
}}
/>
</SecondarySurface>
);
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/desktop-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export interface DesktopAppState {
readonly modelSettingsScopeMode: ModelSettingsScopeMode;
readonly globalModelSettings: ModelSettingsSnapshot;
readonly sidebarCollapsed: boolean;
readonly enableTransparency: boolean;
readonly revision: number;
readonly lastError?: string;
}
Expand Down Expand Up @@ -219,6 +220,7 @@ export function createEmptyDesktopAppState(): DesktopAppState {
enabledModelPatterns: [],
},
sidebarCollapsed: false,
enableTransparency: false,
revision: 0,
};
}
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const desktopIpc = {
setNotificationPreferences: "pi-gui:set-notification-preferences",
setIntegratedTerminalShell: "pi-gui:set-integrated-terminal-shell",
setAllowMultiple: "pi-gui:set-allow-multiple",
setEnableTransparency: "pi-gui:set-enable-transparency",
terminalEnsurePanel: "pi-gui:terminal-ensure-panel",
terminalCreateSession: "pi-gui:terminal-create-session",
terminalSetActiveSession: "pi-gui:terminal-set-active-session",
Expand Down Expand Up @@ -275,6 +276,7 @@ export interface PiDesktopApi {
setNotificationPreferences(preferences: Partial<NotificationPreferences>): Promise<DesktopAppState>;
setIntegratedTerminalShell(shell: string): Promise<DesktopAppState>;
setAllowMultiple(allowMultiple: boolean): Promise<DesktopAppState>;
setEnableTransparency(enabled: boolean): Promise<DesktopAppState>;
ensureTerminalPanel(
workspaceId: string,
terminalScopeId: string,
Expand Down
Loading
Loading