Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7ab1791
Add Siri App Intent to start Pipper thread (confirm-then-create)
maker-or Sep 5, 2026
1d0a7e2
Address review bots: deep-link consume, atomic catalog, availability …
maker-or Sep 5, 2026
42c5742
Apply formatter to src/App.tsx imports
maker-or Sep 5, 2026
3730013
Fix cold-launch deep links and idempotent Siri retries
maker-or Sep 5, 2026
f660e32
Fix React Doctor scan: full checkout history for PR baseline
maker-or Sep 5, 2026
ae3ccd7
Keep React Doctor advisory: don't fail PRs on scanner infra errors
maker-or Sep 5, 2026
75b492e
React Doctor: stop publishing commit status, keep advisory comments
maker-or Sep 5, 2026
a23c0b3
Merge origin/main into read-codebase-v2
maker-or Sep 6, 2026
a7537e7
Merge remote-tracking branch 'origin/main' into read-codebase-v2
maker-or Sep 7, 2026
08ddce0
Siri/Shortcuts: embed App Intents extension with dual-location catalog
maker-or Sep 7, 2026
3737b4a
Clarify download setup instructions
maker-or Sep 8, 2026
ba5a9f2
Address review-bot findings: Siri single-flight, fallback dirs, newes…
maker-or Sep 8, 2026
a8d9e40
Merge remote-tracking branch 'origin/main' into read-codebase-v2
maker-or Sep 9, 2026
c81ee4f
Merge origin/main into read-codebase-v2
maker-or Sep 9, 2026
947d3b0
Complete PipperIntents Siri integration
maker-or Sep 9, 2026
7925fa6
Complete PipperIntents Siri integration
maker-or Sep 11, 2026
0ef5bd1
Merge remote-tracking branch 'origin/main' into read-codebase-v2
maker-or Sep 11, 2026
34a541c
Fix Shortcuts thread creation: String IDs with pickers instead of App…
maker-or Sep 11, 2026
8667474
Fix Siri cold launch blocking the window; drop App Group; inline Spot…
maker-or Sep 11, 2026
baa3fe5
Set version to 0.0.23
maker-or Sep 11, 2026
a77ed0e
Use human-readable labels for Siri intent option values
maker-or Sep 11, 2026
dc0999c
Add Siri-first iOS companion app (PipperRemote) over the Tailscale re…
maker-or Sep 13, 2026
b637909
Merge remote-tracking branch 'origin/main' into read-codebase-v2
maker-or Sep 13, 2026
0fc4d81
Merge origin/main into read-codebase-v2
maker-or Sep 18, 2026
df1c872
Merge origin/main into read-codebase-v2
maker-or Sep 26, 2026
70d639b
Add idempotent remote requests, permissions, and isolated phone works…
maker-or Sep 27, 2026
7143b95
Merge remote-tracking branch 'origin/main' into read-codebase-v2
maker-or Sep 27, 2026
a0e5cb6
IOS Connection & Thread issue fix
Kalyan5252 Sep 27, 2026
63edda3
Merge remote-tracking branch 'origin/read-codebase-v2' into read-code…
maker-or 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
4 changes: 4 additions & 0 deletions .github/workflows/react-doctor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ jobs:
react-doctor:
runs-on: ubuntu-latest
steps:
# Full history: the scan derives the PR's changed files via merge-base.
# A shallow checkout leaves the base commit unreachable and the scan
# crashes before producing a report.
- uses: actions/checkout@v5
with:
persist-credentials: false
# Full history so the action can diff against the merge base and
# report only PR-introduced issues instead of every pre-existing one.
fetch-depth: 0
Expand Down
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,18 @@ startup-benchmark-results/

# Benchmark results
benchmarks

# Swift / Xcode (App Intents, native helpers)
.build/
DerivedData/
*.xcuserdata/
*.xcworkspace/xcuserdata/
**/xcuserdata/
native/pipper-intents/PreviewApp/build/
native/pipper-intents/preview-dist/
native/pipper-intents/PreviewApp/native/
native/pipper-remote-ios/build/
.swiftpm/

# Local script output
.tmp-check/
4 changes: 2 additions & 2 deletions build/entitlements.mac.plist
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>
26 changes: 26 additions & 0 deletions contracts/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,41 @@ export interface RemoteReport {
/** False when worktree creation failed and the task ran in project root. */
isolated: boolean;
isolationNote: string | null;
permissions: RemotePermission[];
request: RemoteRequestStatus | null;
}

export interface RemotePermission {
id: string;
title: string;
detail: string | null;
options: Array<{ optionId: string; name: string; kind: string }>;
}

export interface RemoteRequestStatus {
id: string;
threadId: string | null;
state: "preparing" | "running" | "completed" | "failed" | "interrupted";
error: string | null;
updatedAt: number;
}

export interface RemoteDiagnostics {
paired: true;
agentReady: boolean;
availableAgents: number;
projects: number;
}

export interface RemoteCreateThreadInput {
requestId: string;
projectId: string;
/** Agent/model id from the desktop registry; null = desktop default. */
modelId?: string | null;
prompt: string;
}

export interface RemotePromptInput {
requestId: string;
prompt: string;
}
15 changes: 12 additions & 3 deletions electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ appId: com.maker-or.omni
productName: Pipper Code (Alpha)
directories:
output: release
afterPack: scripts/after-pack.js
files:
- out/**/*
- package.json
Expand All @@ -10,9 +11,10 @@ asarUnpack:
- "**/node_modules/node-pty/**/*"
mac:
artifactName: pipper-${version}-${arch}.${ext}
# Keep local and CI artifacts unsigned. The Sleepless daemon authenticates
# unsigned builds using the exact bundled executable path and peer user.
identity: null
# Use an ad-hoc signature so macOS can load the embedded App Intents
# extension. The Sleepless daemon still authenticates the exact bundled
# executable path and peer user; no developer certificate is required.
identity: "-"
icon: build/icon.png
target:
- target: dmg
Expand All @@ -22,13 +24,20 @@ mac:
hardenedRuntime: true
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.inherit.plist
# The App Intents extension is signed explicitly by scripts/after-pack.js
# with its App Sandbox + App Group entitlements. Do not replace that
# signature with Electron's generic inherited entitlements.
signIgnore:
- PipperIntents\.appex
extraFiles:
- from: native/sleepless/dist/omni-sleeplessctl
to: MacOS/omni-sleeplessctl
- from: native/sleepless/dist/omni-sleeplessd
to: Resources/sleepless/omni-sleeplessd
- from: native/sleepless/com.maker-or.omni.sleeplessd.plist
to: Library/LaunchDaemons/com.maker-or.omni.sleeplessd.plist
- from: native/pipper-intents/dist/PipperIntents.appex
to: Extensions/PipperIntents.appex
binaries:
- Contents/MacOS/omni-sleeplessctl
- Contents/Resources/sleepless/omni-sleeplessd
Expand Down
70 changes: 61 additions & 9 deletions electron/agent-connection-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ export class AgentConnectionManager {
threadDisplayTitle: (threadId) => this.threadDisplayTitle(threadId),
});
private readonly prompts = new PromptScheduler();
private readonly abortGenerations = new Map<string, number>();
private readonly terminalManager: TerminalManager;
private broadcaster!: RendererBroadcaster;
private lifecycle!: ConnectionLifecycle;
Expand Down Expand Up @@ -1444,9 +1445,10 @@ export class AgentConnectionManager {
this.emit({
type: "thread-tool-calls",
threadId: runtime.threadId,
toolCalls: changedToolCall
? { [updateToolCallId]: changedToolCall }
: runtime.slice.toolCalls,
toolCalls:
changedToolCall && updateToolCallId
? { [updateToolCallId]: changedToolCall }
: runtime.slice.toolCalls,
replace: !changedToolCall,
});
} else {
Expand All @@ -1470,6 +1472,19 @@ export class AgentConnectionManager {
return this.permissions.handle(params, requestId ?? null);
}

getRemotePermissions(threadId: string) {
return this.permissions.listForThread(threadId);
}

respondToRemotePermission(
threadId: string,
decisionId: string,
optionId?: string,
cancelled = false,
) {
return this.permissions.respondForThread(threadId, decisionId, optionId, cancelled);
}

respondToPermission(response: {
sessionId: string;
requestId?: string | number;
Expand Down Expand Up @@ -2346,7 +2361,7 @@ export class AgentConnectionManager {
agentId?: string | null,
worktreePath?: string | null,
initialModelId?: string | null,
opts?: { background?: boolean },
opts?: { background?: boolean; requireWorktree?: boolean },
): Promise<Thread> {
return this.enqueueThreadActivation(() =>
this.createThreadInternal(
Expand All @@ -2368,7 +2383,7 @@ export class AgentConnectionManager {
agentId?: string | null,
worktreePath?: string | null,
initialModelId?: string | null,
opts?: { background?: boolean },
opts?: { background?: boolean; requireWorktree?: boolean },
): Promise<Thread> {
const project = getProject(projectId);
if (!project) throw new Error(`Project not found: ${projectId}`);
Expand All @@ -2377,6 +2392,11 @@ export class AgentConnectionManager {
// stale/invalid path is never persisted as this thread's worktree.
const cwd = this.resolveThreadCwd(worktreePath, project.path);
const boundWorktree = cwd === project.path ? null : cwd;
if (opts?.requireWorktree && !boundWorktree) {
throw new Error(
"An isolated workspace is required. Restore the worktree on your Mac before retrying.",
);
}

const targetAgentId = agentId ?? this.preferredAgentId;
// Background creation must not flip the desktop-active agent: spawning
Expand All @@ -2385,6 +2405,9 @@ export class AgentConnectionManager {
const live = opts?.background
? await this.acquireConnection(targetAgentId)
: await this.ensureConnection(targetAgentId);
if (opts?.requireWorktree && !isLiveWorktree(cwd, project.path)) {
throw new Error("The isolated workspace is no longer available. No task was started.");
}
const created = await this.sessionNew(live, cwd);
this.registerWorkspaceRoot(created.sessionId, cwd);

Expand Down Expand Up @@ -2628,17 +2651,41 @@ export class AgentConnectionManager {
return thread;
}

async sendPrompt(input: AcpPromptInput, opts?: { background?: boolean }): Promise<void> {
async sendPrompt(
input: AcpPromptInput,
opts?: { background?: boolean; requireWorktree?: boolean },
): Promise<void> {
return this.sendPromptInternal(input, true, opts);
}

private async sendPromptInternal(
input: AcpPromptInput,
appendUserMessage: boolean,
opts?: { background?: boolean },
opts?: { background?: boolean; requireWorktree?: boolean },
): Promise<void> {
const threadId = input.threadId ?? this.activeThreadId;
if (!threadId) throw new Error("No active thread");
const abortGeneration = this.abortGenerations.get(threadId) ?? 0;
const assertIsolation = () => {
if ((this.abortGenerations.get(threadId) ?? 0) !== abortGeneration) {
throw new Error("Task stopped before the agent was ready.");
}
if (!opts?.requireWorktree) return;
const thread = getThread(threadId);
const project = thread ? getProject(thread.project_id) : null;
const runtime = this.sessions.get(threadId);
if (
!thread?.worktree_path ||
!project ||
!isLiveWorktree(thread.worktree_path, project.path) ||
(runtime && runtime.cwd !== thread.worktree_path)
) {
throw new Error(
"This thread has no live isolated workspace. Restore its worktree on your Mac before sending more work.",
);
}
};
assertIsolation();
if (!this.sessions.has(threadId)) {
// Background senders (phone) must not leave desktop focus behind on a
// restored thread: remember the active thread and put it back after.
Expand Down Expand Up @@ -2695,6 +2742,7 @@ export class AgentConnectionManager {
throw new Error("A prompt is already in flight; choose follow-up or steer to queue it.");
}

assertIsolation();
const caps = live.agentCapabilities.promptCapabilities;
const blocks = assemblePromptBlocks({
message: input.message,
Expand Down Expand Up @@ -2806,8 +2854,12 @@ export class AgentConnectionManager {
}

async abort(): Promise<void> {
const threadId = this.activeThreadId;
return this.abortThread(this.activeThreadId);
}

async abortThread(threadId: string | null): Promise<void> {
if (!threadId) return;
this.abortGenerations.set(threadId, (this.abortGenerations.get(threadId) ?? 0) + 1);
const runtime = this.sessions.get(threadId);
const owner = runtime ? this.connectionForAgent(runtime.agentId) : null;
if (!runtime || !owner) return;
Expand All @@ -2822,7 +2874,7 @@ export class AgentConnectionManager {
this.subagents.cancelRunsForParent(runtime.agentSessionId);
// Cascade cancel to ACP agent terminals (session/cancel → terminal/kill).
// Kill keeps terminalIds valid for final output queries; release is agent-owned.
this.terminalManager.killRunning();
this.terminalManager.killRunning(runtime.agentSessionId);
}

async setConfigOption(configId: string, value: string | boolean): Promise<SessionConfigOption[]> {
Expand Down
14 changes: 7 additions & 7 deletions electron/agents/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"icon": "cursor",
"docsUrl": "https://cursor.com/docs/cli/acp",
"authHint": "Run `agent login` in your terminal (or set CURSOR_API_KEY) before connecting.",
"installHint": "Install Cursor CLI, then ensure `agent` is on your PATH (often ~/.local/bin/agent).",
"installHint": "Install the Cursor CLI (no GUI needed): `curl https://cursor.com/install -fsS | bash`, then ensure `agent` is on your PATH.",
"installKind": "binary",
"detectCommands": ["agent"]
},
Expand All @@ -23,8 +23,8 @@
"args": [],
"icon": "openai-codex",
"docsUrl": "https://github.com/agentclientprotocol/codex-acp",
"authHint": "Sign in with ChatGPT or provide CODEX_API_KEY / OPENAI_API_KEY.",
"installHint": "npm install -g @agentclientprotocol/codex-acp (or use npx on first launch)",
"authHint": "Run `codex` once and choose Sign in with ChatGPT (or set CODEX_API_KEY / OPENAI_API_KEY).",
"installHint": "Install the Codex CLI (no GUI needed): `curl -fsSL https://chatgpt.com/codex/install.sh | sh`, then run `codex` once to sign in. Pipper connects over ACP automatically.",
"installKind": "npx",
"npmPackage": "@agentclientprotocol/codex-acp",
"detectCommands": ["codex-acp"]
Expand All @@ -38,8 +38,8 @@
"args": [],
"icon": "anthropic",
"docsUrl": "https://github.com/agentclientprotocol/claude-agent-acp",
"authHint": "Authenticate Claude Code / set ANTHROPIC_API_KEY before connecting.",
"installHint": "npm install -g @agentclientprotocol/claude-agent-acp (or use npx on first launch)",
"authHint": "Run `claude` once and complete the login prompt (or set ANTHROPIC_API_KEY).",
"installHint": "Install the Claude CLI (no GUI needed): `curl -fsSL https://claude.ai/install.sh | bash` (native install auto-updates). Pipper connects over ACP automatically.",
"installKind": "npx",
"npmPackage": "@agentclientprotocol/claude-agent-acp",
"detectCommands": ["claude-agent-acp"]
Expand Down Expand Up @@ -98,8 +98,8 @@
"args": ["agent", "stdio"],
"icon": "xai",
"docsUrl": "https://www.npmjs.com/package/@xai-official/grok",
"authHint": "Run `grok login` to sign in with your xAI account before connecting.",
"installHint": "npm install -g @xai-official/grok (or use npx on first launch)",
"authHint": "Run `grok login` to sign in via browser (or `grok login --device-auth` on headless machines).",
"installHint": "Install the Grok CLI (no GUI needed): `curl -fsSL https://x.ai/cli/install.sh | bash`, then verify with `grok --version`.",
"installKind": "npx",
"npmPackage": "@xai-official/grok",
"detectCommands": ["grok"]
Expand Down
28 changes: 21 additions & 7 deletions electron/agents/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const BUILTIN_ACP_AGENTS: AcpAgentDescriptor[] = [
docsUrl: "https://cursor.com/docs/cli/acp",
authHint: "Run `agent login` in your terminal (or set CURSOR_API_KEY) before connecting.",
installHint:
"Install Cursor CLI, then ensure `agent` is on your PATH (often ~/.local/bin/agent).",
"Install the Cursor CLI (no GUI needed): `curl https://cursor.com/install -fsS | bash`, then ensure `agent` is on your PATH.",
installKind: "binary",
detectCommands: ["agent"],
},
Expand All @@ -60,8 +60,10 @@ export const BUILTIN_ACP_AGENTS: AcpAgentDescriptor[] = [
args: [],
icon: "openai-codex",
docsUrl: "https://github.com/agentclientprotocol/codex-acp",
authHint: "Sign in with ChatGPT or provide CODEX_API_KEY / OPENAI_API_KEY.",
installHint: "npm install -g @agentclientprotocol/codex-acp (or use npx on first launch)",
authHint:
"Run `codex` once and choose Sign in with ChatGPT (or set CODEX_API_KEY / OPENAI_API_KEY).",
installHint:
"Install the Codex CLI (no GUI needed): `curl -fsSL https://chatgpt.com/codex/install.sh | sh`, then run `codex` once to sign in. Pipper connects over ACP automatically.",
installKind: "npx",
npmPackage: "@agentclientprotocol/codex-acp",
detectCommands: ["codex-acp"],
Expand All @@ -75,9 +77,9 @@ export const BUILTIN_ACP_AGENTS: AcpAgentDescriptor[] = [
args: [],
icon: "anthropic",
docsUrl: "https://github.com/agentclientprotocol/claude-agent-acp",
authHint: "Authenticate Claude Code / set ANTHROPIC_API_KEY before connecting.",
authHint: "Run `claude` once and complete the login prompt (or set ANTHROPIC_API_KEY).",
installHint:
"npm install -g @agentclientprotocol/claude-agent-acp (or use npx on first launch)",
"Install the Claude CLI (no GUI needed): `curl -fsSL https://claude.ai/install.sh | bash` (native install auto-updates). Pipper connects over ACP automatically.",
installKind: "npx",
npmPackage: "@agentclientprotocol/claude-agent-acp",
detectCommands: ["claude-agent-acp"],
Expand Down Expand Up @@ -106,8 +108,10 @@ export const BUILTIN_ACP_AGENTS: AcpAgentDescriptor[] = [
args: ["agent", "stdio"],
icon: "xai",
docsUrl: "https://www.npmjs.com/package/@xai-official/grok",
authHint: "Run `grok login` to sign in with your xAI account before connecting.",
installHint: "npm install -g @xai-official/grok (or use npx on first launch)",
authHint:
"Run `grok login` to sign in via browser (or `grok login --device-auth` on headless machines).",
installHint:
"Install the Grok CLI (no GUI needed): `curl -fsSL https://x.ai/cli/install.sh | bash`, then verify with `grok --version`.",
installKind: "npx",
npmPackage: "@xai-official/grok",
detectCommands: ["grok"],
Expand Down Expand Up @@ -475,6 +479,16 @@ export function resolveAgentSpawn(agent: AcpAgentDescriptor): {
// Drop ambient provider credentials for isolated accounts so the child can't
// authenticate as the machine's default login instead of the chosen account.
for (const name of agent.unsetEnv ?? []) delete env[name];
// `bun run --bun` prepends a temp dir whose `node` symlinks to bun. npx-based
// agents are `#!/usr/bin/env node` scripts; run under bun, npm derives its
// global prefix from the bun binary (~/.bun) and fails with ENOENT on
// ~/.bun/lib. Strip the shim so agents run under real node.
const pathKey = process.platform === "win32" ? "Path" : "PATH";
const sep = process.platform === "win32" ? ";" : ":";
env[pathKey] = (env[pathKey] ?? env.PATH ?? "")
.split(sep)
.filter((dir) => !/[\\/]bun-node-[^\\/]+$/.test(dir))
.join(sep);

if (agent.id === "pipper-mock" || agent.installKind === "mock") {
const mockPath = join(registryDir, "mock-agent.mjs");
Expand Down
Loading
Loading