Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
1794265
fix(agent-runtime): keep project instructions for out-of-root paths
525300887039 Sep 24, 2026
f62a7f9
perf(host-core): keep history out of the append and search critical path
Sep 29, 2026
aa51b43
perf(host-core): answer a message update without copying the transcript
Sep 30, 2026
54a3e93
perf(host-core): answer the streaming guard from the index, not the t…
Sep 30, 2026
8d57c6f
perf(host-core): bound the compaction chain a message window carries
Sep 30, 2026
6fae5e8
perf(host-core): cache the transcript line layout beside the file
Oct 1, 2026
af701de
fix(tools): preserve unmatched text in partial-line legacy edits
GalaxyXieyu Oct 5, 2026
c5e7b4a
Merge af701dea55956e6229176dea4e59bab934c36fcc into c1afefea7bb8156a3…
GalaxyXieyu Oct 5, 2026
dabc110
fix(live-voice): let Realtime use a plain-HTTP user endpoint
hxj04121-lab Oct 5, 2026
52d1a97
Merge remote-tracking branch 'origin/main' into HEAD
vastsa Oct 5, 2026
3c20175
fix(tools): document legacy terminal newline behavior
vastsa Oct 5, 2026
ea87db1
Merge pull request #1420 from GalaxyXieyu/fix/legacy-edit-partial-line
vastsa Oct 5, 2026
06156eb
Merge branch 'main' into fix/realtime-loopback-http
vastsa Oct 5, 2026
8d301ac
Merge 06156ebe16a7f6613cd9bf6c61b09b110f01e88c into ea87db1d05bcb7b88…
hxj04121-lab Oct 5, 2026
9f625da
test(live-voice): verify plaintext websocket transport
vastsa Oct 5, 2026
424e82b
Merge pull request #1422 from hxj04121-lab/fix/realtime-loopback-http
vastsa Oct 5, 2026
6f56edf
fix(host-core): preserve transcript durability in PR #1202
vastsa Oct 5, 2026
e23cc93
Merge pull request #1202 from moapop/perf/host-core-persistence-hot-p…
vastsa Oct 5, 2026
966d6af
Merge remote-tracking branch 'origin/pr-991' into codex/pr991-instruc…
vastsa Oct 5, 2026
6ab074e
test(agent-runtime): cover out-of-root instruction fallback
vastsa Oct 5, 2026
924a03a
Merge pull request #991 from 525300887039/fix/968-instruction-root-fa…
vastsa Oct 5, 2026
c54a520
fix(plugins): fail a panel that never becomes ready (#998 item 5)
Blue-Berrys Oct 6, 2026
a5fd4dc
test(plugins): pin the ready budget in the panel-window contract
Blue-Berrys Oct 6, 2026
03bb673
Merge remote-tracking branch 'origin/main' into fix/plugin-panel-load…
Blue-Berrys Oct 6, 2026
a88caeb
perf(host-core): run sync tool bodies off the tokio workers
GalaxyXieyu Oct 6, 2026
0473bc2
fix(network): apply system proxy to providers and marketplace
vastsa Oct 6, 2026
038564a
Merge pull request #1430 from GalaxyXieyu/perf/host-core-sync-tools-o…
vastsa Oct 6, 2026
b4b4d87
Merge remote-tracking branch 'origin/main' into codex/review-pr1426-2…
vastsa Oct 6, 2026
dee29c6
fix(plugins): complete panel load timeout contract
vastsa Oct 6, 2026
c20ad57
Merge pull request #1426 from Blue-Berrys/fix/plugin-panel-load-failure
vastsa Oct 6, 2026
70a12e5
fix(network): respect HTTP PAC proxy semantics
vastsa Oct 6, 2026
2cb69ab
Merge remote-tracking branch 'origin/main' into fix/system-proxy-all-…
vastsa Oct 6, 2026
875fd41
Merge pull request #1432 from vastsa/fix/system-proxy-all-transports-…
vastsa Oct 6, 2026
7934897
feat(plugins): add OAuth provider callbacks
vastsa Oct 6, 2026
974882b
test(plugins): cover OAuth provider user flow
vastsa Oct 6, 2026
eff8e65
perf(host-core): replace session updated_at index with composite index
xiaobaZeo Oct 6, 2026
96c7a89
refactor(plugins): isolate OAuth manifest validation
vastsa Oct 6, 2026
05f1e8c
fix(mcp): scope user stdio clients by workspace
vastsa Oct 6, 2026
ee7f2f5
Merge pull request #1436 from vastsa/fix/user-mcp-workspace-cwd-1425
vastsa Oct 6, 2026
6e83361
test(settings): cover plugin OAuth account menu
vastsa Oct 6, 2026
d42f5c5
Merge remote-tracking branch 'origin/main' into codex/feat-plugin-pro…
vastsa Oct 6, 2026
6994316
test(e2e): run trusted extensions in Linux CI
vastsa Oct 6, 2026
e7f5559
fix(ci): install xauth for Electron E2E
vastsa Oct 6, 2026
370825b
fix(ci): install Electron binary for E2E
vastsa Oct 6, 2026
15c2bc2
test(e2e): expose Electron startup diagnostics
vastsa Oct 6, 2026
234dd66
fix(ci): configure Chromium sandbox helper
vastsa Oct 6, 2026
282ebda
test(e2e): reopen model settings before sign-out
vastsa Oct 6, 2026
46cabbf
fix(e2e): read OAuth IPC result envelope
vastsa Oct 6, 2026
85f9335
test(e2e): restore model binding after OAuth flow
vastsa Oct 6, 2026
84e3c50
test(e2e): capture binding after model setup
vastsa Oct 6, 2026
31ed61b
Merge pull request #1433 from vastsa/codex/feat-plugin-provider-oauth
vastsa Oct 6, 2026
6881a2b
feat(images): serve image generation from a signed-in ChatGPT account
vastsa Oct 6, 2026
fa2bd83
fix(files): open a scratch path another session wrote
vastsa Oct 6, 2026
2f313d8
fix(skill-market): pin accepted addresses for mixed DNS
vastsa Oct 6, 2026
722d0b3
docs(spec): sync Chinese network policy decision
vastsa Oct 6, 2026
048e4d3
Merge pull request #1437 from vastsa/fix/skill-market-ula-dns
vastsa Oct 6, 2026
cfef0dc
docs(plugins): send plugin publishing to the plugin center
vastsa Oct 6, 2026
d403c96
docs(plugins): finish moving the marketplace docs to the plugin center
vastsa Oct 6, 2026
bfb17c1
fix(renderer): page large tool output
vastsa Oct 6, 2026
e1d0d05
fix(ui): retain native SVG props on icon wrappers
vastsa Oct 6, 2026
682a09a
fix(settings): hide cloud sync from packaged builds
vastsa Oct 6, 2026
7144592
fix(docs): correct localized marketplace ADR link
vastsa Oct 6, 2026
78d51a9
docs(zh-CN): point the marketplace ADR link at the site root
vastsa Oct 6, 2026
9d874a7
test(desktop): cover paged streaming tool previews
vastsa Oct 6, 2026
e864a45
Merge pull request #1440 from vastsa/fix/hide-cloud-sync-packaged
vastsa Oct 6, 2026
ff65281
merge(main): refresh renderer freeze candidate
vastsa Oct 6, 2026
ac7f931
test(desktop): await plugin OAuth notification events
vastsa Oct 6, 2026
176e1cf
Merge pull request #1439 from vastsa/codex/large-output-renderer-freeze
vastsa Oct 6, 2026
ff30654
fix(desktop): keep the AskTool question body in one text column
vastsa Oct 7, 2026
399f676
fix(composer): keep an inline image where the user placed it
vastsa Oct 7, 2026
2e74898
Merge pull request #1442 from vastsa/fix/inline-image-order
vastsa Oct 7, 2026
8148b33
chore(release): prepare v0.17.0
vastsa Oct 7, 2026
72b5e82
Merge pull request #1443 from vastsa/chore/release-0.17.0
vastsa Oct 7, 2026
6f6f189
fix(host-core): preserve BOM-marked UTF-16 text through Read and Edit
AR307 Oct 7, 2026
7a03669
fix(agent-runtime): isolate delegate mutation recovery and failure re…
AR307 Oct 7, 2026
fb48a3f
fix(desktop): finish regenerate archival before host shutdown
AR307 Oct 7, 2026
393c08c
chore(host-core): integrate latest main for PR 1435
vastsa Oct 7, 2026
d7c03cf
fix(host-core): align schema v22 checks
vastsa Oct 7, 2026
6a83667
feat(jev): integrate TypeSafe classifier
vastsa Oct 6, 2026
a4826db
chore(docs): remove generated PR summary artifact
vastsa Oct 7, 2026
8881c97
test(jev): cover opt-in classifier integration
vastsa Oct 7, 2026
5055ab4
chore(docs): remove generated PR summary artifact
vastsa Oct 7, 2026
753650f
chore(docs): remove generated PR summary artifact
vastsa Oct 7, 2026
176a2e1
Merge pull request #1435 from vastsa/worktree-fix+session-index
vastsa Oct 7, 2026
2a8803c
chore: refresh PR 1445 against current main
vastsa Oct 7, 2026
fd35486
Merge remote-tracking branch 'origin/main' into codex/jev-integration
vastsa Oct 7, 2026
f8d8339
fix(jev): use localized password labels
vastsa Oct 7, 2026
a46e1a3
Merge pull request #1445 from AR307/codex/pr-utf16-tools
vastsa Oct 7, 2026
3421092
chore: refresh PR 1446 against current main
vastsa Oct 7, 2026
d15567f
Merge remote-tracking branch 'origin/main' into codex/jev-integration
vastsa Oct 7, 2026
7a1c0c7
Merge pull request #1446 from AR307/codex/pr-delegate-mutation-recovery
vastsa Oct 7, 2026
df7f146
Merge remote-tracking branch 'origin/main' into codex/pr1447-quit-arc…
vastsa Oct 7, 2026
24d7496
Merge remote-tracking branch 'origin/main' into codex/jev-integration
vastsa Oct 7, 2026
1e07bad
Merge pull request #1448 from vastsa/codex/jev-integration
vastsa Oct 7, 2026
d963f11
Merge remote-tracking branch 'origin/main' into codex/pr1447-quit-arc…
vastsa Oct 7, 2026
278e929
Merge pull request #1447 from AR307/codex/pr-quit-terminal-persistence
vastsa Oct 7, 2026
a528c26
fix(settings): accept the image model a signed-in vendor account offers
vastsa Oct 7, 2026
e84dc23
Merge pull request #1455 from vastsa/fix/image-model-selection
vastsa Oct 7, 2026
81c4fb2
docs(plugins): document the renderer slot contract
vastsa Oct 7, 2026
7a47846
Merge pull request #1456 from vastsa/docs/plugin-renderer-slot-contract
vastsa Oct 7, 2026
d135b7a
feat(settings): add Jev from the service chooser with a checked key
vastsa Oct 7, 2026
4f999fe
Merge pull request #1457 from vastsa/feat/jev-service-setup
vastsa Oct 7, 2026
44e46e0
fix(settings): show the Jev card only once Jev has been added
vastsa Oct 7, 2026
52b52f7
Merge pull request #1458 from vastsa/fix/jev-card-hidden-until-added
vastsa Oct 7, 2026
7cd532e
merge(sync): upstream main through 52b52f7
Nick-JuRan Oct 8, 2026
d406e94
fix(sync): keep upstream's scopeDelegateTools parameter order
Oct 8, 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
49 changes: 49 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,52 @@ jobs:

- name: Test
run: cargo test -p host-core --locked

trusted-extensions-e2e:
name: Trusted extensions Electron E2E
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7

- uses: pnpm/action-setup@v6

- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm

- uses: dtolnay/rust-toolchain@stable

- uses: Swatinem/rust-cache@v2

- name: Install virtual display
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends --yes xauth xvfb

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Install Electron binary
run: pnpm --filter @pi-desktop/desktop exec install-electron

- name: Configure Chromium SUID sandbox
run: |
sandbox=apps/desktop/node_modules/electron/dist/chrome-sandbox
sudo chown root:root "$sandbox"
sudo chmod 4755 "$sandbox"

- name: Build workspace packages
run: pnpm build:js

- name: Build Desktop bundle
run: pnpm --filter @pi-desktop/desktop build

- name: Build host-core
run: cargo build -p host-core --locked

- name: Run trusted extensions E2E
env:
DEBUG_E2E: "1"
run: xvfb-run --auto-servernum pnpm test:e2e:trusted-extensions
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ resolver = "2"
[workspace.package]
edition = "2021"
license = "LGPL-3.0-or-later"
version = "0.16.1"
version = "0.17.0"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Local-first · Model-agnostic · Plugin-powered · macOS / Windows / Linux

</div>

> **Current release line: 0.16.x (Early Preview).**
> **Current release line: 0.17.x (Early Preview).**

---

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Subagent 与 Worker Session 可以承担独立任务并行工作。
> [!NOTE]
> **PI-Desktop 目前仍处于 Early Preview。** 已可用于真实开发工作流,部分 API、插件接口与桌面能力仍在持续演进。

> **当前发布线:0.16.x(Early Preview)。**
> **当前发布线:0.17.x(Early Preview)。**

---

Expand Down
31 changes: 29 additions & 2 deletions apps/desktop/electron/main/bootstrap/shutdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type ShutdownDependencies = {
activeTurns: Map<string, string>;
persistenceOutbox: PersistenceOutbox;
inflightCheckpointer: InflightCheckpointer;
flushEventPersistence: () => Promise<void>;
pluginPanels: Pick<PluginPanelHost, "closeAll">;
plugins: Pick<PluginRuntime, "disposeAll">;
userMcp: Pick<UserMcpRuntime, "disposeAll">;
Expand All @@ -49,6 +50,7 @@ export type ShutdownDependencies = {
confirmQuitDialog: () => Promise<boolean>;
disposePowerSaveBlockers: () => void;
liveCallService?: Pick<LiveCallService, "endForLifecycle">;
disposeSystemProxyRelay?: () => Promise<void>;
};

/** Register the last-window and before-quit resource lifecycle handlers. */
Expand All @@ -61,6 +63,7 @@ export function registerShutdownHandlers({
activeTurns,
persistenceOutbox,
inflightCheckpointer,
flushEventPersistence,
pluginPanels,
plugins,
userMcp,
Expand All @@ -72,6 +75,7 @@ export function registerShutdownHandlers({
confirmQuitDialog,
disposePowerSaveBlockers,
liveCallService,
disposeSystemProxyRelay,
}: ShutdownDependencies): void {
app.on("window-all-closed", () => {
// The D216 tray is resident on every platform, so its presence says nothing
Expand Down Expand Up @@ -121,6 +125,9 @@ export function registerShutdownHandlers({
}

state.quitting = true;
// Persist the confirmed shutdown before any awaited cleanup so a forced
// process termination still leaves an observable lifecycle boundary.
logger.app("lifecycle", "info", "app shutdown");
disposePowerSaveBlockers();
state.tray?.destroy();
state.tray = null;
Expand Down Expand Up @@ -162,8 +169,28 @@ export function registerShutdownHandlers({
pluginPanels.closeAll(),
pluginViews.dispose(),
]);
logger.app("lifecycle", "info", "app shutdown");
await pluginSurfacesShutdown;
// Stop event production before draining the terminal writes. A finished
// turn can already have left activeTurns while its branch archive is
// still awaiting the host; closing host-core first loses that archive.
const sidecarShutdown = getSidecar()?.dispose();
await Promise.allSettled([sidecarShutdown]);
await inflightCheckpointer.flushAll();
let persistenceTimer: ReturnType<typeof setTimeout> | undefined;
try {
const settled = await Promise.race([
flushEventPersistence().then(() => true),
new Promise<boolean>((resolve) => {
persistenceTimer = setTimeout(() => resolve(false), QUIT_TURN_SETTLE_BUDGET_MS);
}),
]);
if (!settled) {
logger.app("lifecycle", "warn", "quit before event persistence settled");
}
} finally {
if (persistenceTimer) clearTimeout(persistenceTimer);
}
await persistenceOutbox.flush(getHost);
const hostShutdown = getHost()?.dispose();
const mcpShutdown = getMcpControl()?.stop();
updater.dispose();
Expand All @@ -175,7 +202,6 @@ export function registerShutdownHandlers({
mcpOAuth?.disposeAll();
browserHost.dispose();
inflightCheckpointer.dispose();
const sidecarShutdown = getSidecar()?.dispose();

try {
await hostShutdown;
Expand All @@ -188,6 +214,7 @@ export function registerShutdownHandlers({
mcpShutdown,
remoteHostsShutdown,
]);
await disposeSystemProxyRelay?.();
})();

const releaseQuit = () => {
Expand Down
44 changes: 41 additions & 3 deletions apps/desktop/electron/main/chat-ref-resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,25 @@ export type ChatRefRoots = {
*/
project?: readonly FsChatRefProjectRoot[] | null;
scratch?: string | null;
/**
* Roots an absolute reference may name without being searched by shorthand.
*
* A generated image lives in the scratch store of the session that produced
* it, and the user clicks it from whatever conversation they are reading —
* usually a later one. Every read guard (`fsRead`, `fsOpen`, the image
* reader, `fsReveal`) already accepts the whole scratch store, so completing
* against the session's own store alone reported a restriction for a file the
* app can open. These entries widen the absolute-path check only: a shorthand
* still searches the session's own root, so it never lands in another
* session's files.
*/
containment?: readonly ChatRefContainmentRoot[] | null;
attachments?: string | null;
};

/** A root an absolute reference may name, though no shorthand searches it. */
export type ChatRefContainmentRoot = { kind: FsChatRefRoot; path: string };

/** A root to search, and — for a project folder — which folder it is. */
type ChatRefRootEntry = {
kind: FsChatRefRoot;
Expand Down Expand Up @@ -147,6 +163,23 @@ function orderedRoots(roots: ChatRefRoots): ChatRefRootEntry[] {
return list;
}

/**
* The containment-only roots, in the same vocabulary as the searched ones.
*
* They answer absolute references and nothing else: the shorthand walk below
* keeps searching `orderedRoots`, so a reference is never completed from
* another session's store.
*/
function containmentEntries(roots: ChatRefRoots): ChatRefRootEntry[] {
const list: ChatRefRootEntry[] = [];
for (const root of roots.containment ?? []) {
const path = String(root?.path ?? "").trim();
if (!path) continue;
list.push({ kind: root.kind, path: resolve(path) });
}
return list;
}

async function isRegularFile(target: string): Promise<boolean> {
try {
return (await stat(target)).isFile();
Expand Down Expand Up @@ -203,7 +236,7 @@ export async function isChatRefOutsideRoots(ref: string, roots: ChatRefRoots): P
const cleaned = cleanRef(ref);
if (!isAbsolute(cleaned)) return true;
const absolutePath = resolve(cleaned);
const rootList = await canonicalRoots(orderedRoots(roots));
const rootList = await canonicalRoots([...orderedRoots(roots), ...containmentEntries(roots)]);
if (!couldShareVolume(absolutePath, rootList)) return true;
const targetPath = await canonicalPathOrMissingTail(absolutePath);
if (!targetPath) return !rootList.some((root) => relativeInside(root.path, absolutePath) !== null);
Expand Down Expand Up @@ -343,7 +376,7 @@ export async function resolveChatFileRef(
const parsed = parseChatRef(ref);
if (!parsed) return null;
const rootList = orderedRoots(roots);
if (rootList.length === 0) return null;
if (rootList.length === 0 && !(roots.containment ?? []).length) return null;

const cleanedPosixRef = toPosix(cleanRef(ref));
const isAttachmentRef = /^attachments(?:\/|$)/i.test(cleanedPosixRef);
Expand All @@ -353,7 +386,12 @@ export async function resolveChatFileRef(
const cleaned = cleanRef(ref);
if (!isAbsolute(cleaned)) return null;
const absolutePath = resolve(cleaned);
const canonicalRootList = await canonicalRoots(rootList);
// An absolute reference may also name a store the app reads but no shorthand
// searches, such as an image another session generated.
const canonicalRootList = await canonicalRoots([
...rootList,
...containmentEntries(roots),
]);
if (!couldShareVolume(absolutePath, canonicalRootList)) return null;
let targetPath: string;
try {
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { join } from "node:path";
import {
applyNetworkProxyFromAppSettings,
currentNetworkProxy,
disposeSystemProxyRelay,
ensureSystemProxyRelay,
testNetworkProxy,
} from "./network-proxy";
import {
Expand Down Expand Up @@ -281,6 +283,7 @@ const vendorOAuth = new VendorOAuth({
openExternal: async (url) => {
await safeOpenExternal(url);
},
getPluginOAuthBridge: () => pluginServices.plugins,
log: (level, message, data) => logger.app("provider", level, message, { data }),
onAccountModels: (id, models) => modelsDevCatalog.setAccountModels(id, models),
onAccountRemoved: (id) => modelsDevCatalog.deleteAccount(id),
Expand Down Expand Up @@ -773,6 +776,7 @@ const { startHost } = createHostRuntime({
importLegacyScheduled,
superviseRestart,
isQuitting: () => mainState.quitting,
ensureSystemProxyRelay,
});

runtimeLifecycle = createRuntimeLifecycle({
Expand Down Expand Up @@ -1030,6 +1034,7 @@ registerShutdownHandlers({
activeTurns,
persistenceOutbox,
inflightCheckpointer,
flushEventPersistence: eventPersistence.flush,
pluginPanels,
plugins,
userMcp,
Expand All @@ -1041,6 +1046,7 @@ registerShutdownHandlers({
confirmQuitDialog,
disposePowerSaveBlockers,
liveCallService,
disposeSystemProxyRelay,
});

registerApplicationActivation({
Expand Down
17 changes: 14 additions & 3 deletions apps/desktop/electron/main/ipc/agent-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,16 +295,20 @@ export function registerAgentIpc({
Boolean(req.attachments?.length),
)
: null;
// The inline text the user's draft carried decides both what the message
// shows and where each image block sits in the prompt.
const steerContent = mcpExpansion?.expanded ?? req.content;
const prepared = await preparePromptAttachments(
dataDir, req.sessionId, context.projectPath, req.attachments ?? [], context.supportsVision,
dataDir, req.sessionId, context.projectPath, req.attachments ?? [],
context.supportsVision, steerContent,
);
const session = await host.call<{ session?: { messages?: UiMessage[] } }>("session.get", {
id: req.sessionId, messageLimit: 1,
});
const message: UiMessage = {
id: durableUserMessageId(req.messageId, session.session?.messages ?? []),
role: "user",
content: mcpExpansion?.expanded ?? req.content,
content: steerContent,
...(mcpExpansion ? { command: mcpExpansion.command } : {}),
status: "complete",
createdAt: new Date().toISOString(),
Expand All @@ -316,11 +320,12 @@ export function registerAgentIpc({
// never turn into a normal prompt or alter the next turn's configuration.
return sidecar.call<{ accepted: boolean; turnId: string }>("agent.steer", {
sessionId: req.sessionId, expectedTurnId: req.expectedTurnId, message,
content: appendPromptFallbackPaths(mcpExpansion?.expanded ?? req.content, prepared),
content: appendPromptFallbackPaths(steerContent, prepared),
...(mcpExpansion ? { mcpServerIds: mcpExpansion.mcpServerIds, mcpToolNames: mcpExpansion.mcpToolNames } : {}),
attachments: prepared.filter((attachment) => attachment.inlineData).map((attachment) => ({
path: attachment.message.ref, name: attachment.message.name, kind: attachment.message.kind,
mimeType: attachment.message.mimeType, size: attachment.message.size, data: attachment.inlineData,
...(attachment.message.inlinePath ? { inlinePath: attachment.message.inlinePath } : {}),
})),
});
});
Expand Down Expand Up @@ -560,6 +565,9 @@ export function registerAgentIpc({
: undefined,
req.attachments ?? [],
supportsVision,
// The text the durable message will hold: an image chip the Composer
// left inline keeps its place in the prompt and in the transcript.
promptContent,
);
} catch (error) {
await finishTurn(req.sessionId, "error", (error as any)?.errorCode, {
Expand Down Expand Up @@ -677,6 +685,9 @@ export function registerAgentIpc({
mimeType: attachment.message.mimeType,
size: attachment.message.size,
data: attachment.inlineData,
...(attachment.message.inlinePath
? { inlinePath: attachment.message.inlinePath }
: {}),
})),
// A referenced conversation crosses the sidecar as quoted text for
// this turn; the durable record above keeps it for later turns.
Expand Down
27 changes: 27 additions & 0 deletions apps/desktop/electron/main/ipc/jev-ipc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* The Jev key check channel.
*
* One job, separate from the provider channels: Jev owns no provider row, and
* its credential lives under a fixed Host secret reference rather than a
* provider id. The handler only asks TypeSafe; it stores nothing, so a key
* that fails the check never reaches the secret store.
*/
import { IPC } from "@pi-desktop/shared";
import { probeJevApiKey } from "../jev-probe";
import type { Logger } from "../logger";
import type { IpcRegistrar } from "./types";

export type JevIpcDependencies = {
registrar: IpcRegistrar;
logger: Pick<Logger, "app">;
};

export function registerJevIpc({ registrar, logger }: JevIpcDependencies): void {
registrar.handle(IPC.invoke.jevTest, async (apiKey: unknown) => {
const result = await probeJevApiKey(typeof apiKey === "string" ? apiKey : "");
logger.app("provider", result.ok ? "info" : "warn", "TypeSafe Jev key check", {
data: { ok: result.ok, status: result.status },
});
return result;
});
}
2 changes: 2 additions & 0 deletions apps/desktop/electron/main/ipc/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { searchMcpMarket } from "../mcp-registry-catalog";
import { registerNotificationIpc } from "./notification-ipc";
import { registerPluginIpc } from "./plugin-ipc";
import { registerPluginUiIpc } from "./plugin-ui-ipc";
import { registerJevIpc } from "./jev-ipc";
import { registerProviderIpc } from "./provider-ipc";
import { registerScheduledIpc } from "./scheduled-ipc";
import { registerSessionIpc } from "./session-ipc";
Expand Down Expand Up @@ -299,6 +300,7 @@ export function registerIpcHandlers(dependencies: RegisterIpcDependencies) {
bindingForModel,
onProviderInvalidated: (providerId) => liveCallService?.invalidateProvider(providerId),
});
registerJevIpc({ registrar, logger });
const loadComposerTemplatesCached = createComposerTemplateLoader(logger);
const composerCommandService = registerComposerIpc({
registrar,
Expand Down
Loading
Loading