Skip to content
Open
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: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
- name: Install
run: pnpm install --frozen-lockfile

- name: Package Linux AppImage
- name: Package Linux AppImage and deb
run: pnpm --filter @pi-gui/desktop run package:linux

- name: Verify packaged runtime dependencies
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,11 @@ jobs:
- name: Typecheck
run: pnpm typecheck

- name: Package Linux AppImage
- name: Package Linux AppImage and deb
run: |
cd apps/desktop
pnpm run build
pnpm exec electron-builder --linux AppImage \
pnpm exec electron-builder --linux AppImage deb \
--publish never \
"-c.extraMetadata.version=${GITHUB_REF_NAME#v}"

Expand All @@ -159,6 +159,7 @@ jobs:
with:
files: |
apps/desktop/release/*.AppImage
apps/desktop/release/*.deb
apps/desktop/release/latest-linux*.yml
prerelease: ${{ contains(github.ref_name, 'beta') || contains(github.ref_name, 'alpha') || contains(github.ref_name, 'rc') }}
generate_release_notes: true
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ These rules apply for the full session.
- Do not create or switch to new branches to start work unless the user explicitly asks; respect the current branch or worktree as intentional.
- Commit in small focused checkpoints; don’t batch unrelated changes.
- Run `simplify` before closing non-trivial implementation work.
- Avoid broad `grep` searches; keep them focused on specific directories or patterns to maintain performance.

## Product
- This repo is building a Codex-style desktop app for `pi`; preserve that product direction.
Expand Down
13 changes: 12 additions & 1 deletion apps/desktop/electron-builder.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
appId: com.pi-gui.desktop
productName: pi-gui
copyright: Copyright 2026 Matthew Lam
electronVersion: 34.5.8
electronVersion: 42.2.0

directories:
output: release
Expand Down Expand Up @@ -44,8 +44,19 @@ mac:
linux:
category: Development
executableName: pi-gui
maintainer: Matthew Lam <lochois@gmail.com>
target:
- target: AppImage
- target: deb

deb:
depends:
- libgtk-3-0
- libgbm1
- libnss3
- libxss1
- libasound2
- libatspi2.0-0

toolsets:
appimage: "1.0.2"
Expand Down
34 changes: 34 additions & 0 deletions apps/desktop/electron/app-store-timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,40 @@ export function appendQueuedUserMessage(
transcriptCache.set(key, transcript);
}

export function appendAssistantThinkingDelta(
transcriptCache: Map<string, TranscriptMessage[]>,
activeAssistantMessageBySession: Map<string, string>,
sessionRef: SessionRef,
text: string,
): void {
const key = sessionKey(sessionRef);
const transcript = [...(transcriptCache.get(key) ?? [])];
const activeId = activeAssistantMessageBySession.get(key);

if (activeId) {
const index = transcript.findIndex((message) => message.id === activeId);
const current = index >= 0 ? transcript[index] : undefined;
if (current?.kind === "message") {
transcript[index] = {
...(current as any),
reasoning: `${(current as any).reasoning ?? ""}${text}`,
};
} else {
const message = makeTranscriptMessage("assistant", "");
(message as any).reasoning = text;
transcript.push(message);
activeAssistantMessageBySession.set(key, message.id);
}
} else {
const message = makeTranscriptMessage("assistant", "");
(message as any).reasoning = text;
transcript.push(message);
activeAssistantMessageBySession.set(key, message.id);
}

transcriptCache.set(key, transcript);
}

export function appendAssistantDelta(
transcriptCache: Map<string, TranscriptMessage[]>,
activeAssistantMessageBySession: Map<string, string>,
Expand Down
95 changes: 89 additions & 6 deletions apps/desktop/electron/app-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
import {
applyTimelineEvent,
appendAssistantDelta,
appendAssistantThinkingDelta,
clearActiveAssistantMessage,
} from "./app-store-timeline";
import { applySessionEventState, updateSessionRecord } from "./app-store-session-state";
Expand Down Expand Up @@ -243,6 +244,18 @@ export class DesktopAppStore implements AppStoreInternals {
};
}

async getSessionContextUsage(sessionRef: SessionRef): Promise<import("../src/desktop-state").ContextUsage | undefined> {
const session = await this.driver.getSession(sessionRef);
const usage = session.getContextUsage();
const stats = session.getSessionStats();
return {
...usage,
input: stats.tokens.input,
output: stats.tokens.output,
cacheRead: stats.tokens.cacheRead,
};
}

/* ── Workspace methods (delegated) ─────────────────────── */

async addWorkspace(path: string): Promise<DesktopAppState> {
Expand Down Expand Up @@ -493,7 +506,6 @@ export class DesktopAppStore implements AppStoreInternals {
await this.persistUiState();
return this.emit();
}

async setEnableTransparency(enabled: boolean): Promise<DesktopAppState> {
await this.initialize();
if (this.state.enableTransparency === enabled) {
Expand All @@ -509,11 +521,23 @@ export class DesktopAppStore implements AppStoreInternals {
return this.emit();
}

async setModelSettingsScopeMode(modelSettingsScopeMode: ModelSettingsScopeMode): Promise<DesktopAppState> {
async setAllowMultiple(allowMultiple: boolean): Promise<DesktopAppState> {
await this.initialize();
if (this.state.modelSettingsScopeMode === modelSettingsScopeMode) {
if (this.state.allowMultiple === allowMultiple) {
return this.emit();
}
this.state = {
...this.state,
allowMultiple,
lastError: undefined,
revision: this.state.revision + 1,
};
await this.persistUiState();
return this.emit();
}

async setModelSettingsScopeMode(modelSettingsScopeMode: ModelSettingsScopeMode): Promise<DesktopAppState> {
await this.initialize();
if (modelSettingsScopeMode === "app-global") {
await this.restoreGlobalModelSettings(this.state.globalModelSettings);
}
Expand Down Expand Up @@ -762,6 +786,7 @@ export class DesktopAppStore implements AppStoreInternals {
lastViewedAtBySession: persisted.lastViewedAtBySession ?? {},
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);
Expand Down Expand Up @@ -978,6 +1003,7 @@ export class DesktopAppStore implements AppStoreInternals {
composerAttachments: this.resolveComposerAttachments(selectedWorkspaceId, selectedSessionId),
queuedComposerMessages: this.resolveQueuedComposerMessages(selectedWorkspaceId, selectedSessionId),
editingQueuedMessageId: this.resolveEditingQueuedMessageId(selectedWorkspaceId, selectedSessionId),
selectedSessionContextUsage: (selectedWorkspaceId && selectedSessionId) ? await this.getSessionContextUsage({ workspaceId: selectedWorkspaceId, sessionId: selectedSessionId }).catch(() => undefined) : undefined,
lastError: this.resolveSelectedSessionError(selectedWorkspaceId, selectedSessionId, options.clearLastError),
revision: this.state.revision + 1,
};
Expand Down Expand Up @@ -1037,13 +1063,13 @@ export class DesktopAppStore implements AppStoreInternals {
if (this.sessionState.loadedTranscriptKeys.has(key)) {
return;
}

const cachedTranscript = await this.readPersistedTranscript(key);
const transcript = cachedTranscript
? await this.resolveLoadedTranscript(sessionRef, cachedTranscript)
: await this.driver.getTranscript(sessionRef);

if (!cachedTranscript || cachedTranscript.format === "legacy") {

await this.writePersistedTranscript(key, transcript);
}

Expand Down Expand Up @@ -1366,6 +1392,44 @@ export class DesktopAppStore implements AppStoreInternals {
}

switch (event.type) {
case "assistantThinkingStarted": {
const key = sessionKey(event.sessionRef);
this.sessionState.reasoningStreamingMessageIdBySession.set(key, null);
this.state = {
...this.state,
reasoningStreamingMessageIdBySession: {
...this.state.reasoningStreamingMessageIdBySession,
[key]: null,
},
};
break;
}
case "assistantThinkingFinished": {
const key = sessionKey(event.sessionRef);
this.sessionState.reasoningStreamingMessageIdBySession.set(key, null);
this.state = {
...this.state,
reasoningStreamingMessageIdBySession: {
...this.state.reasoningStreamingMessageIdBySession,
[key]: null,
},
};
break;
}
case "assistantThinkingDelta": {
const key = sessionKey(event.sessionRef);
appendAssistantThinkingDelta(this.sessionState.transcriptCache, this.sessionState.activeAssistantMessageBySession, event.sessionRef, event.text);
const activeId = this.sessionState.activeAssistantMessageBySession.get(key);
this.sessionState.reasoningStreamingMessageIdBySession.set(key, activeId ?? null);
this.state = {
...this.state,
reasoningStreamingMessageIdBySession: {
...this.state.reasoningStreamingMessageIdBySession,
[key]: activeId ?? null,
},
};
break;
}
case "assistantDelta":
appendAssistantDelta(this.sessionState.transcriptCache, this.sessionState.activeAssistantMessageBySession, event.sessionRef, event.text);
break;
Expand Down Expand Up @@ -1438,6 +1502,18 @@ export class DesktopAppStore implements AppStoreInternals {
);
this.markSessionViewedIfActivelyViewed(event.sessionRef);
this.state = this.syncDerivedSessionState(this.state, event.sessionRef);

// Update context usage for real-time counter
if (
(event.type === "assistantDelta" ||
event.type === "assistantThinkingDelta" ||
event.type === "toolFinished" ||
(event.type as string) === "sessionCompact")
) {
const usage = await this.getSessionContextUsage(event.sessionRef);
this.state = { ...this.state, selectedSessionContextUsage: usage };
}

if (shouldFollowSessionMutation && event.type !== "sessionClosed") {
this.applyFastSessionSelection(event.sessionRef);
if (!refreshedFollowedSession) {
Expand Down Expand Up @@ -1908,6 +1984,7 @@ export class DesktopAppStore implements AppStoreInternals {
composerDraftSyncSource: "selection",
composerDraftSyncNonce: this.state.composerDraftSyncNonce + 1,
composerAttachments: this.resolveComposerAttachments(sessionRef.workspaceId, sessionRef.sessionId),
selectedSessionContextUsage: undefined,
lastError: undefined,
revision: this.state.revision + 1,
};
Expand Down Expand Up @@ -1936,13 +2013,19 @@ export class DesktopAppStore implements AppStoreInternals {
return;
}

const runtimeByWorkspace = runtimeMissing ? await this.serializeRuntimeStateForCurrentWorkspaces() : undefined;
const [runtimeByWorkspace, selectedSessionContextUsage] = await Promise.all([
runtimeMissing ? this.serializeRuntimeStateForCurrentWorkspaces() : Promise.resolve(undefined),
this.getSessionContextUsage(sessionRef).catch(() => undefined),
]);
if (!this.isCurrentSelectionEpoch(sessionRef, selectionEpoch)) {
return;
}

this.clearSessionError(sessionRef);
this.state = this.syncSelectedSessionHydrationState(this.state, sessionRef, snapshot, runtimeByWorkspace);
this.state = {
...this.syncSelectedSessionHydrationState(this.state, sessionRef, snapshot, runtimeByWorkspace),
selectedSessionContextUsage,
};
if (options.markViewed ?? true) {
this.markSessionViewed(sessionRef);
}
Expand Down
Loading
Loading