Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
82a32c4
feat(agents): scaffold agent-applications area
benjackwhite Jun 16, 2026
41160a4
feat(agents): agent_platform API + read surface for applications
benjackwhite Jun 16, 2026
685e5bf
feat(agents): typed SSE event union + transcript content parts
benjackwhite Jun 16, 2026
7a548e8
feat(agents): stored transcript → ACP message mapper
benjackwhite Jun 16, 2026
d755575
feat(agents): live SSE event → ACP message mapper
benjackwhite Jun 16, 2026
fa7787e
feat(agents): render stored session transcripts via ConversationView
benjackwhite Jun 16, 2026
8666da6
feat(agents): split Agents into Scouts and Applications tabs
benjackwhite Jun 16, 2026
363cd97
docs: add temporary agent-applications port plan
benjackwhite Jun 16, 2026
a2fa911
feat(agents): per-agent tab shell + approvals pane
benjackwhite Jun 16, 2026
0f15929
feat(agents): sessions list, session KPIs + cron badge, and a logs tab
benjackwhite Jun 16, 2026
a376b61
feat(agents): approvals master/detail with embedded session + refresh…
benjackwhite Jun 16, 2026
1e5c1b9
feat(agents): filesystem-style Configuration explorer
benjackwhite Jun 16, 2026
aed8929
feat(agents): config explorer trigger richness — Slack setup, endpoin…
benjackwhite Jun 16, 2026
c1d8f97
feat(agents): inline secret set/rotate/clear in the config explorer
benjackwhite Jun 16, 2026
32e8749
fix(agents): guard secret Clear behind a confirm; hide input for set …
benjackwhite Jun 16, 2026
55dbb1b
feat(agents): revision bar with picker + lifecycle (M9)
benjackwhite Jun 16, 2026
0856ea5
feat(agents): observability surface — fleet KPIs on Applications over…
benjackwhite Jun 16, 2026
615dd6e
docs: mark M7 observability done in agent-applications plan
benjackwhite Jun 16, 2026
3062c53
docs: bring agent-applications plan up to date
benjackwhite Jun 16, 2026
22caee6
feat(agents): Memory tab — file browser + BM25 search + tables (M11)
benjackwhite Jun 16, 2026
7c041ed
docs: mark Memory done; defer the global approvals queue
benjackwhite Jun 16, 2026
84a741c
docs: defer M13 Connections (legacy idea, not needed now)
benjackwhite Jun 16, 2026
c0688cf
feat(agents): live chat preview — test a deployed agent's chat trigger
benjackwhite Jun 16, 2026
47bbac9
docs(agents): mark live chat preview done; stage the M-Concierge mile…
benjackwhite Jun 16, 2026
a9a9943
feat(agents): concierge dock — always-on chat that drives /code/agents
benjackwhite Jun 16, 2026
1272d31
feat(agents): concierge set_secret punch-out (C4) + Quill composer
benjackwhite Jun 16, 2026
e8f32b5
refactor(agents): rename the concierge to "Agent Builder"
benjackwhite Jun 16, 2026
5bfecce
feat(agents): live-now panel + fleet approvals queue (M6)
dmarticus Jun 16, 2026
78f4e89
fix(auth): seed org map from /api/users/@me/ when token lacks scoped …
dmarticus Jun 16, 2026
a92f024
docs: stamp M6 commit SHA + clear stale forward-references
dmarticus Jun 16, 2026
4dbf364
feat(agents): gate agent-platform behind a flag + agent builder polish
benjackwhite Jun 17, 2026
c455583
fix(agents): 'Agent Builder unavailable' empty-state title spacing
benjackwhite Jun 17, 2026
167e164
feat(agents): rotating builder composer placeholder + dedupe EmptyState
benjackwhite Jun 17, 2026
cda3b11
feat(agents): approval deep-link handler (posthog-code://approval/<id>)
benjackwhite Jun 17, 2026
38761ad
feat(agents): report the user's current project to the agent builder
benjackwhite Jun 17, 2026
91bd39a
fix(agents): unblock CI on agent-applications port
benjackwhite Jun 17, 2026
e829351
refactor(agents): drop dead stats chain + share AgentDetailEmptyState
dmarticus Jun 16, 2026
cc70be4
feat(agents): in-chat approvals — decide inline in the chat preview (…
dmarticus Jun 16, 2026
5a89e73
fix formatting
dmarticus Jun 16, 2026
42ffc5d
feat(auth): request agent_approvals:write scope; bump OAUTH_SCOPE_VER…
dmarticus Jun 16, 2026
a03ee55
feat(agents): visible confirmation after deciding an approval
dmarticus Jun 16, 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
367 changes: 367 additions & 0 deletions AGENT_APPLICATIONS_PLAN.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions apps/code/src/main/di/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ import type {
SLACK_INTEGRATION_SERVICE,
} from "@posthog/core/integrations/identifiers";
import type { SlackIntegrationService } from "@posthog/core/integrations/slack";
import type { ApprovalLinkService } from "@posthog/core/links/approval-link";
import type {
APPROVAL_LINK_SERVICE,
INBOX_LINK_SERVICE,
NEW_TASK_LINK_SERVICE,
SCOUT_LINK_SERVICE,
Expand Down Expand Up @@ -234,6 +236,7 @@ import type { WorkspaceServerService } from "../services/workspace-server/servic
import type { rendererStore } from "../utils/store";
import type {
APP_LIFECYCLE_SERVICE as MAIN_APP_LIFECYCLE_SERVICE,
APPROVAL_LINK_SERVICE as MAIN_APPROVAL_LINK_SERVICE,
ARCHIVE_REPOSITORY as MAIN_ARCHIVE_REPOSITORY,
AUTH_PREFERENCE_REPOSITORY as MAIN_AUTH_PREFERENCE_REPOSITORY,
AUTH_SERVICE as MAIN_AUTH_SERVICE,
Expand Down Expand Up @@ -401,10 +404,12 @@ export interface MainBindings {
[MAIN_INBOX_LINK_SERVICE]: InboxLinkService;
[MAIN_SCOUT_LINK_SERVICE]: ScoutLinkService;
[MAIN_NEW_TASK_LINK_SERVICE]: NewTaskLinkService;
[MAIN_APPROVAL_LINK_SERVICE]: ApprovalLinkService;
[TASK_LINK_SERVICE]: TaskLinkService;
[INBOX_LINK_SERVICE]: InboxLinkService;
[SCOUT_LINK_SERVICE]: ScoutLinkService;
[NEW_TASK_LINK_SERVICE]: NewTaskLinkService;
[APPROVAL_LINK_SERVICE]: ApprovalLinkService;

// Watcher registry
[MAIN_WATCHER_REGISTRY_SERVICE]: WatcherRegistryService;
Expand Down
7 changes: 7 additions & 0 deletions apps/code/src/main/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ import { GIT_DIFF_SOURCE } from "@posthog/core/git-pr/identifiers";
import { handoffModule } from "@posthog/core/handoff/handoff.module";
import { HANDOFF_HOST } from "@posthog/core/handoff/identifiers";
import { integrationsModule } from "@posthog/core/integrations/integrations.module";
import { ApprovalLinkService } from "@posthog/core/links/approval-link";
import {
APPROVAL_LINK_SERVICE,
INBOX_LINK_SERVICE,
NEW_TASK_LINK_SERVICE,
SCOUT_LINK_SERVICE,
Expand Down Expand Up @@ -246,6 +248,7 @@ import { rendererStore } from "../utils/store";
import type { MainBindings } from "./bindings";
import {
APP_LIFECYCLE_SERVICE as MAIN_APP_LIFECYCLE_SERVICE,
APPROVAL_LINK_SERVICE as MAIN_APPROVAL_LINK_SERVICE,
ARCHIVE_REPOSITORY as MAIN_ARCHIVE_REPOSITORY,
AUTH_PREFERENCE_REPOSITORY as MAIN_AUTH_PREFERENCE_REPOSITORY,
AUTH_SERVICE as MAIN_AUTH_SERVICE,
Expand Down Expand Up @@ -620,6 +623,10 @@ container.bind(MAIN_SCOUT_LINK_SERVICE).to(ScoutLinkService);
container.bind(SCOUT_LINK_SERVICE).toService(MAIN_TOKENS.ScoutLinkService);
container.bind(MAIN_NEW_TASK_LINK_SERVICE).to(NewTaskLinkService);
container.bind(NEW_TASK_LINK_SERVICE).toService(MAIN_TOKENS.NewTaskLinkService);
container.bind(MAIN_APPROVAL_LINK_SERVICE).to(ApprovalLinkService);
container
.bind(APPROVAL_LINK_SERVICE)
.toService(MAIN_TOKENS.ApprovalLinkService);
container.load(watcherRegistryModule);
container
.bind(MAIN_WATCHER_REGISTRY_SERVICE)
Expand Down
4 changes: 4 additions & 0 deletions apps/code/src/main/di/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ export const SCOUT_LINK_SERVICE = Symbol.for(
export const NEW_TASK_LINK_SERVICE = Symbol.for(
"posthog.host.main.new-task-link.service",
);
export const APPROVAL_LINK_SERVICE = Symbol.for(
"posthog.host.main.approval-link.service",
);
export const WATCHER_REGISTRY_SERVICE = Symbol.for(
"posthog.host.main.watcher-registry.service",
);
Expand Down Expand Up @@ -155,6 +158,7 @@ export const MAIN_TOKENS = Object.freeze({
InboxLinkService: INBOX_LINK_SERVICE,
ScoutLinkService: SCOUT_LINK_SERVICE,
NewTaskLinkService: NEW_TASK_LINK_SERVICE,
ApprovalLinkService: APPROVAL_LINK_SERVICE,
WatcherRegistryService: WATCHER_REGISTRY_SERVICE,
ProvisioningService: PROVISIONING_SERVICE,
WorkspaceService: WORKSPACE_SERVICE,
Expand Down
2 changes: 2 additions & 0 deletions apps/code/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
SLACK_INTEGRATION_SERVICE,
} from "@posthog/core/integrations/identifiers";
import type { SlackIntegrationService } from "@posthog/core/integrations/slack";
import type { ApprovalLinkService } from "@posthog/core/links/approval-link";
import type { InboxLinkService } from "@posthog/core/links/inbox-link";
import type { NewTaskLinkService } from "@posthog/core/links/new-task-link";
import type { ScoutLinkService } from "@posthog/core/links/scout-link";
Expand Down Expand Up @@ -226,6 +227,7 @@ async function initializeServices(): Promise<void> {
container.get<InboxLinkService>(MAIN_TOKENS.InboxLinkService);
container.get<ScoutLinkService>(MAIN_TOKENS.ScoutLinkService);
container.get<NewTaskLinkService>(MAIN_TOKENS.NewTaskLinkService);
container.get<ApprovalLinkService>(MAIN_TOKENS.ApprovalLinkService);
container.get<GitHubIntegrationService>(GITHUB_INTEGRATION_SERVICE);
container.get<SlackIntegrationService>(SLACK_INTEGRATION_SERVICE);
container.get<ExternalAppsService>(MAIN_TOKENS.ExternalAppsService);
Expand Down
17 changes: 16 additions & 1 deletion docs/DEEP-LINKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ PostHog Code registers custom URL schemes so the desktop app can be opened with
| Development | `posthog-code-dev://` |
| Legacy (production only) | `twig://`, `array://` |

All schemes route through the same dispatcher. The host portion of the URL selects the handler (`task`, `inbox`, `scout`, `new`, `plan`, `issue`, `callback`, `integration`, `slack-integration`, `mcp-oauth-complete`).
All schemes route through the same dispatcher. The host portion of the URL selects the handler (`task`, `inbox`, `scout`, `approval`, `new`, `plan`, `issue`, `callback`, `integration`, `slack-integration`, `mcp-oauth-complete`).

If the app is not running, the OS launches it and the link is queued until the renderer is ready. If the app is minimised, it is restored and focused before the link is handled.

Expand Down Expand Up @@ -116,6 +116,20 @@ posthog-code://scout/error-tracking
posthog-code://scout/error-tracking?finding=abc123
```

### `posthog-code://approval/<requestId>`

Open the agent fleet approvals inbox focused on a specific tool-approval request.
Emitted by the agent-runner on a gated tool call so non-PostHog-Code clients
(Slack, MCP) can land on the approval; the request id alone resolves it.

| Segment / Parameter | Required | Description |
|---|---|---|
| `<requestId>` | Yes | Agent tool-approval request id (e.g. `ar_...`). |

```
posthog-code://approval/ar_abc123
```

## OAuth callback links

These are issued by external services and consumed by the app. You should not need to construct them yourself, but they are documented for completeness.
Expand Down Expand Up @@ -180,6 +194,7 @@ In development the same payload is delivered to `http://localhost:8238/mcp-oauth
| `task` | [packages/core/src/links/task-link.ts](../packages/core/src/links/task-link.ts) |
| `inbox` | [packages/core/src/links/inbox-link.ts](../packages/core/src/links/inbox-link.ts) |
| `scout` | [packages/core/src/links/scout-link.ts](../packages/core/src/links/scout-link.ts) |
| `approval` | [packages/core/src/links/approval-link.ts](../packages/core/src/links/approval-link.ts) |
| `new`, `plan`, `issue` | [packages/core/src/links/new-task-link.ts](../packages/core/src/links/new-task-link.ts) |
| `callback` | [packages/core/src/oauth/oauth.ts](../packages/core/src/oauth/oauth.ts) |
| `integration` | [packages/core/src/integrations/github.ts](../packages/core/src/integrations/github.ts) |
Expand Down
29 changes: 29 additions & 0 deletions docs/LOCAL-DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,35 @@ Open devtools in the dev build and type:

Source: `apps/code/src/renderer/features/inbox/devtools/inboxDemoConsole.ts`.

## Feature flags in local dev

Feature flags are read through posthog-js, configured by the `VITE_POSTHOG_*`
vars in `.env`. By default these point at PostHog's internal analytics instance,
so flags you create locally never resolve in the dev build (and flag-gated UI —
e.g. the agent-platform surface behind the `agent-platform` flag — stays hidden).

To point the flags/analytics client at your local PostHog so locally-synced
flags take effect:

```bash
# In your PostHog repo: create + enable all frontend-defined flags locally
python manage.py sync_feature_flags

# In this repo: rewrite VITE_POSTHOG_* to your local instance, then restart dev
pnpm posthog:local
pnpm dev
```

`pnpm posthog:local` auto-reads the project API key from a sibling `../posthog`
checkout (or pass it: `pnpm posthog:local phc_xxx`, or set `POSTHOG_DIR`). This
only affects the analytics/flags client — the data API still uses the **Dev**
region you pick at login.

> One-off override without changing `.env`: the dev build exposes the client on
> `window.posthog`, so you can run
> `posthog.featureFlags.override({ "agent-platform": true })` in the renderer
> console (clear with `posthog.featureFlags.override(false)`).

## Troubleshooting

### "Invalid client_id" error during OAuth
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"dev:git": "pnpm --filter @posthog/git dev",
"dev:code": "pnpm --filter code start",
"app:cdp": "node scripts/electron-cdp.mjs",
"posthog:local": "node scripts/use-local-posthog.mjs",
"build": "turbo build",
"build:deps": "turbo build --filter=@posthog/code^...",
"package": "turbo build && pnpm --filter code package",
Expand Down
155 changes: 155 additions & 0 deletions packages/api-client/src/agent-analytics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { describe, expect, it } from "vitest";
import {
type AgentAnalyticsRaw,
buildAgentAnalyticsQueries,
EMPTY_AGENT_ANALYTICS,
type HogQLGrid,
shapeAgentAnalytics,
} from "./agent-analytics";

const grid = (results: unknown[][]): HogQLGrid => ({ results, columns: [] });

// A 14-day daily series where every day is identical, so prior(7) === recent(7)
// → zero deltas. Columns: [day, cost, sessions, errors, generations].
function flatDaily(): unknown[][] {
return Array.from({ length: 14 }, (_, i) => [
`2026-06-${String(i + 1).padStart(2, "0")}T00:00:00`,
2, // cost
5, // sessions
1, // errors
10, // generations
]);
}

describe("buildAgentAnalyticsQueries", () => {
it("scopes to agent-platform origin only when no application id", () => {
const q = buildAgentAnalyticsQueries();
expect(q.kpi).toContain("$ai_origin = 'agent_platform_runner'");
expect(q.kpi).not.toContain("$agent_application_id =");
expect(q.kpi).toContain("event = '$ai_generation'");
expect(q.toolErrors).toContain("event = '$ai_span'");
});

it("narrows to a single application id when given", () => {
const q = buildAgentAnalyticsQueries("app-uuid-123");
expect(q.kpi).toContain(
"properties.$agent_application_id = 'app-uuid-123'",
);
expect(q.byModel).toContain(
"properties.$agent_application_id = 'app-uuid-123'",
);
});
});

describe("shapeAgentAnalytics", () => {
it("returns an empty board for empty grids", () => {
const out = shapeAgentAnalytics({});
expect(out.empty).toBe(true);
expect(out.kpis).toEqual(EMPTY_AGENT_ANALYTICS.kpis);
expect(out.byAgent).toEqual([]);
expect(out.deltas).toEqual({
spend: null,
sessions: null,
failureRatePoints: null,
});
});

it("derives KPIs incl. failure rate from generations", () => {
const raw: Partial<AgentAnalyticsRaw> = {
// cost, sessions, errors, generations, p95
kpi: grid([[12.5, 8, 3, 12, 4.2]]),
};
const out = shapeAgentAnalytics(raw);
expect(out.kpis.spendUsd).toBe(12.5);
expect(out.kpis.sessions).toBe(8);
expect(out.kpis.failureRate).toBeCloseTo(3 / 12);
expect(out.kpis.p95LatencyS).toBe(4.2);
expect(out.empty).toBe(false);
});

it("coerces numeric strings (HogQL returns decimals as strings)", () => {
const out = shapeAgentAnalytics({
kpi: grid([["1.50", "4", "0", "4", "2"]]),
});
expect(out.kpis.spendUsd).toBe(1.5);
expect(out.kpis.sessions).toBe(4);
expect(out.kpis.failureRate).toBe(0);
});

it("builds a 14-day daily series with zero deltas for a flat trend", () => {
const out = shapeAgentAnalytics({ daily: grid(flatDaily()) });
expect(out.daily.labels).toHaveLength(14);
expect(out.daily.spend).toHaveLength(14);
expect(out.daily.failureRate.every((r) => r === 0.1)).toBe(true);
// prior 7 === recent 7 → 0% change, and failure-rate delta is 0pp.
expect(out.deltas.spend).toBe(0);
expect(out.deltas.sessions).toBe(0);
expect(out.deltas.failureRatePoints).toBe(0);
});

it("computes a positive spend delta when recent exceeds prior", () => {
// 7 days at cost 1, then 7 days at cost 3 → +200%.
const days = Array.from({ length: 14 }, (_, i) => [
`2026-06-${String(i + 1).padStart(2, "0")}T00:00:00`,
i < 7 ? 1 : 3,
1,
0,
1,
]);
const out = shapeAgentAnalytics({ daily: grid(days) });
expect(out.deltas.spend).toBeCloseTo(200);
});

it("maps per-agent rows and resolves names via the id→name map", () => {
const raw: Partial<AgentAnalyticsRaw> = {
// agent_id, sessions, generations, cost, tokens, errors, p95
perAgent: grid([
["11111111-2222-3333-4444-555566667777", 5, 10, 4, 2000, 2, 1.5],
["aaaa", 1, 4, 0.5, 100, 0, 0.2],
]),
};
const names = new Map([
["11111111-2222-3333-4444-555566667777", "Support Bot"],
]);
const out = shapeAgentAnalytics(raw, names);
expect(out.byAgent[0]).toMatchObject({
name: "Support Bot",
sessions: 5,
spendUsd: 4,
tokens: 2000,
p95LatencyS: 1.5,
});
expect(out.byAgent[0].failureRate).toBeCloseTo(2 / 10);
// Unknown id falls back to a short id.
expect(out.byAgent[1].name).toBe("aaaa");
});

it("maps model spend and tool error rates", () => {
const out = shapeAgentAnalytics({
byModel: grid([["claude-opus-4-8", 9.99, 42]]),
toolErrors: grid([
["search", 20, 4],
["fetch", 5, 0],
]),
});
expect(out.byModel[0]).toEqual({
model: "claude-opus-4-8",
spendUsd: 9.99,
calls: 42,
});
expect(out.toolErrors[0].errorRate).toBeCloseTo(4 / 20);
expect(out.toolErrors[1].errorRate).toBe(0);
});

it("ignores non-array rows defensively", () => {
const out = shapeAgentAnalytics({
kpi: grid([[1, 1, 0, 1, 1]]),
perAgent: {
results: [null, "oops"] as unknown as unknown[][],
columns: [],
},
});
expect(out.byAgent).toEqual([]);
expect(out.empty).toBe(false);
});
});
Loading
Loading