Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b35e918
feat(channels): ship production thread and feed UX
k11kirky Jul 15, 2026
c9cb075
fix(channels): retain mention styling and Quill guidance
k11kirky Jul 15, 2026
f723ed9
fix(channels): satisfy thread scroll dependencies
k11kirky Jul 15, 2026
2bcea32
feat(agent): instruction-level rtk adoption for codex sessions
tatoalo Jul 16, 2026
7ffce8a
fix(canvas): render agent status as thread content
k11kirky Jul 16, 2026
8d4d42e
feat(canvas): distinguish agent-directed thread messages
k11kirky Jul 16, 2026
45298c3
fix(agent): persist Codex prompt token usage
tatoalo Jul 16, 2026
83fc1f9
fix(canvas): normalize forwarded agent prompts
k11kirky Jul 16, 2026
021a701
fix(canvas): deduplicate forwarded thread prompts
k11kirky Jul 16, 2026
2f661a1
refactor(canvas): align thread UX with architecture
k11kirky Jul 16, 2026
927acb0
fix(billing): explain the merged usage limit as included allowance + …
adboio Jul 16, 2026
eb76666
fix(canvas): keep agent status out of thread history
k11kirky Jul 16, 2026
f2e8cad
fix(canvas): align thread mention rendering
k11kirky Jul 16, 2026
82b3e9c
feat(billing): segment the usage bar into included vs spend-limit por…
adboio Jul 16, 2026
2a85d33
chore(visual): update storybook baselines
posthog[bot] Jul 16, 2026
b6faccc
fix(canvas): address thread feed review findings
k11kirky Jul 16, 2026
abc5d81
Merging 45298c3c726e7b6a9f5a0193058f3cbeb8f1b106 into trunk-temp/pr-3…
trunk-io[bot] Jul 16, 2026
b84565d
Merging b6faccc979dc62f2a9ff0a69a643e85a74bdb5d6 into trunk-temp/pr-3…
trunk-io[bot] Jul 16, 2026
4486ee0
Merging 2bcea3294f341ba9cbdc19b40fa7598e78281b35 into trunk-temp/pr-3…
trunk-io[bot] Jul 16, 2026
2a3bdfe
Merging 2a85d33bf1bcd7edc48293219ea0880a65b3efa8 into trunk-temp/pr-3…
trunk-io[bot] Jul 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
93 changes: 93 additions & 0 deletions .claude/skills/quill-code/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
name: quill-code
description: Edit the @posthog/quill design system locally and consume the change in this repo (posthog-code) before it is published to npm. Use when changing quill components/primitives/tokens, when a quill change must be tested inside the Code app, or when the user mentions quill, the design system, the .local-quill tarball, or the @posthog/quill pnpm override.
---

# quill-code

`@posthog/quill` is **not** in this repo. It is a published catalog dependency whose
source lives in the main PostHog monorepo at `../posthog/packages/quill`. To test an
unpublished quill change inside this repo (posthog-code), you build quill, pack it to a
tarball, and point a pnpm `overrides` entry at that tarball. This is a **temporary
local-dev** state — revert before merging (see below).

## Quill layout (where to edit)

`../posthog/packages/quill` is the **workspace** (`@posthog/quill-workspace`). It
contains sub-packages, each a layer of the design system:

- `packages/primitives/src` — base components (Card, Badge, Button, Progress, …)
- `packages/components/src` — composed components (DataTable, DateTimePicker, Metric)
- `packages/blocks/src`**product-level blocks** (e.g. `ExperimentCard`). Add the
file here and export it from `packages/blocks/src/index.ts`.
- `packages/quill/src` — the **aggregate** that re-exports all layers as `@posthog/quill`.

A new export in any sub-package flows to `@posthog/quill` automatically on build.

## The loop (every quill change)

1. Edit/add the component in the right sub-package (above) and export it from that
package's `src/index.ts`.
2. Re-sync into this repo manually — build → pack → point the override → reinstall:

```bash
QUILL_DIR="${QUILL_DIR:-../posthog/packages/quill/packages/quill}"

# a. Build the WHOLE quill workspace (two levels up from the aggregate), so the
# sub-packages rebuild BEFORE the aggregate bundles them.
( cd "$QUILL_DIR/../.." && pnpm build )

# b. Pack into .local-quill/ under a UNIQUE filename. pnpm pins a tarball by
# integrity, so a stable name caches stale across re-syncs — drop old local
# tarballs first, then rename the packed file to a unique local name.
rm -f .local-quill/posthog-quill-local-*.tgz
( cd "$QUILL_DIR" && npm pack --pack-destination "$(git rev-parse --show-toplevel)/.local-quill" )
mv .local-quill/posthog-quill-[0-9]*.tgz ".local-quill/posthog-quill-local-$(git rev-parse --short HEAD)-$$.tgz"

# c. Point the override at the new tarball, then reinstall.
# Edit pnpm-workspace.yaml so overrides['@posthog/quill'] = file:./.local-quill/<new file>
pnpm install
```

> Building only the aggregate (`packages/quill/packages/quill`) re-bundles the
> sub-packages' **stale** `dist/`, so edits to primitives/components/blocks are
> silently dropped. Always build at the workspace root.
3. Verify in the Code app (`pnpm dev`, or the `test-electron-app` skill). Repeat from 1.

After every quill edit you **must** re-run the sync — the app consumes the tarball, not
the quill source, so unsynced edits are invisible here.

If quill lives elsewhere, set `QUILL_DIR=/abs/path/to/posthog/packages/quill/packages/quill`.

## Why a tarball, not `link:`

`link:` symlinks into the mono's `node_modules` and drags in its **React 18** types,
colliding with this repo's **React 19** (dual-React → broken typecheck +
invalid-hook-call at runtime). The tarball is copied into this repo's store and deduped
against React 19. The filename is **content-hashed** because pnpm pins a tarball by
integrity, so a stable filename gets cached stale across re-syncs.

## The override (what the script rewrites)

In `pnpm-workspace.yaml`, under `overrides:`:

```yaml
'@posthog/quill': file:./.local-quill/posthog-quill-local-<hash>.tgz
```
There is also a permanent pin you should leave alone:
```yaml
'@posthog/quill>@base-ui/react': ^1.3.0 # quill ships a broken catalog: dep; do not remove
```
## Reverting (before merge)
The override is local-dev only. Once the quill change is published to npm:
1. Bump the catalog version in `pnpm-workspace.yaml` (`'@posthog/quill': 0.3.0-beta.x`)
to the published version.
2. Restore the override line to point back at the catalog, or remove the `file:` override.
3. `pnpm install`.

Do not commit a `file:./.local-quill/...` override or the `.local-quill/` tarballs.
20 changes: 20 additions & 0 deletions apps/code/snapshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,26 @@ snapshots:
hash: v1.k4693efd2.b76d39ed8142d4449cb5befcde538bb11a6bffcbed66e81bdc3aa8e2746b7e06.GSIfBsgE-3RYuqlLUpluZ87C6-KjBUaTj2eu_a5elZ8
autoresearch-runtime-stats--with-context-usage--light:
hash: v1.k4693efd2.18889b5eda942cd19b0f7be00a87925d632529ca4111fac180d6339f7d34d1ca.X3s9IXrx2-TdQaJEjf1td3hwvNzeIp3Xq3uQh9Gwt5A
billing-usagemeter--exceeded--dark:
hash: v1.k4693efd2.dc026c5cace4e44c4cc713b95654e278d7bf0439a4862e9ceea39a033974815d.m5Be4dAOwwmt-UlEnhbhrKiym1rlGUiGZ4oyUonUoVg
billing-usagemeter--exceeded--light:
hash: v1.k4693efd2.850bf1e451a18981d9e3be653837dd282c6be289ac9c0826ff6a981ac67e1e38.1GwpfJ_4dUlMO7TXp8AOM5YmJuItGGB50ihKkkeIFgg
billing-usagemeter--free-tier--dark:
hash: v1.k4693efd2.164d54cac22cfa05dbbf25fae1a62e77c496b1981dcaff6b8e1ab07fc802f5e0.Gh75uI1vHbGWAgKun41BYoX6LZBYX_FVI7Qu-rFlDV4
billing-usagemeter--free-tier--light:
hash: v1.k4693efd2.5e220fe66fac4c54b4cb9f5ac6ec1054e0b709374fee2c58fdba782f5b2888df.W5la88YA6uOSc1tg30raKaRy12plPGxXl5o7uowL2gc
billing-usagemeter--subscribed-past-included--dark:
hash: v1.k4693efd2.ac987b7184aaa4f8895d9b75c6b7e1177efcb9a21cca0668caf533ee0d5eaab3.2Mq2GLXV83ebM8LKhTQY8csv6BeskWv5yPu8NSAQ20Y
billing-usagemeter--subscribed-past-included--light:
hash: v1.k4693efd2.0c4a37beaceb37baab57e6da98321b236c20127731fd5eaeb61677de4580c204.fZ2eDGlkEijZjHQyatiHtP39EFYT6EO1Vn_f8Tz7b0E
billing-usagemeter--subscribed-with-breakdown--dark:
hash: v1.k4693efd2.f74f8f76e51ee14e427423e1757859e3f001b13e6a43d1e7310b5b0cbbb570b5.eMwK2csA86oBarnAya99W87MfP6Z6PMfdjwbCmnPrw4
billing-usagemeter--subscribed-with-breakdown--light:
hash: v1.k4693efd2.1335dba8bf16980c987882641619e114584343ec50bb69b3c01e63507a13756b._v7_6tsVMmW3WixSeFN-puHQVeIxf9AAGxFZ8Ni97YQ
billing-usagemeter--zero-spend-limit--dark:
hash: v1.k4693efd2.1d199b2c4bba8034cb18fb5b49866b64a6eb3311591add5f58ec288710dce2e9.FCc9egSaO1Onih417yciSYKNROe8zqDZyyS6xuXCZGw
billing-usagemeter--zero-spend-limit--light:
hash: v1.k4693efd2.b50c18736bbde45fc89f4c0ac7fd616286e5c1ecc2a56910fffca13eaa13d8a2.31z4F0JmvMeR4yqk5pjs_OQ3MypnM2miZI9al4wuc8c
components-permissions-permissionselector--create-new-file--dark:
hash: v1.k4693efd2.c54203a4e636b83b3d24d7ed9c4ace8659db87cd8f231d9dc2ecc03320e31646.epDm7LebiLzlp0uuZBrE-Obt_anAn0xsE8bHFnm5vos
components-permissions-permissionselector--create-new-file--light:
Expand Down
2 changes: 2 additions & 0 deletions apps/code/src/renderer/desktop-contributions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { agentChatCoreModule } from "@posthog/core/agent-chat/agentChat.module";
import { autoresearchCoreModule } from "@posthog/core/autoresearch/autoresearch.module";
import { billingCoreModule } from "@posthog/core/billing/billing.module";
import { taskThreadCoreModule } from "@posthog/core/canvas/taskThread.module";
import { inboxCoreModule } from "@posthog/core/inbox/inbox.module";
import { githubConnectModule } from "@posthog/core/integrations/githubConnect.module";
import { localMcpCoreModule } from "@posthog/core/local-mcp/local-mcp.module";
Expand Down Expand Up @@ -37,6 +38,7 @@ export function registerDesktopContributions(): void {
autoresearchCoreModule,
billingUiModule,
billingCoreModule,
taskThreadCoreModule,
browserTabsUiModule,
cloneUiModule,
connectivityUiModule,
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/web-container.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import "reflect-metadata";
import { TypedContainer } from "@inversifyjs/strongly-typed";
import { taskThreadCoreModule } from "@posthog/core/canvas/taskThread.module";
import { setRootContainer } from "@posthog/di/container";
import { ROOT_LOGGER, type RootLogger } from "@posthog/di/logger";
import {
Expand Down Expand Up @@ -96,4 +97,6 @@ container.bind(MCP_SANDBOX_PROXY_URL).toConstantValue(() => {
return sandboxProxyUrl;
});

container.load(taskThreadCoreModule);

setRootContainer(container);
9 changes: 6 additions & 3 deletions packages/agent/src/adapters/claude/session/rtk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,17 @@ import { gitSubcommand } from "../git-command";

// Commands RTK compresses faithfully and that have no side effects, so wrapping
// them changes only how much output reaches the model, never what runs.
const RTK_PLAIN_COMMANDS = new Set(["grep", "find", "ls"]);
// Exported so the instruction-level Codex guidance advertises the same set.
export const RTK_PLAIN_COMMANDS = new Set(["grep", "find", "ls"]);

// Git subcommands whose output is worth compressing and that RTK handles
// faithfully. The criterion is compressible output, NOT read-only: RTK never
// changes what runs, so a mutating form (`git tag -d`, `git remote add`,
// `git reflog expire`) still executes its write — its output is just shorter.
// Excludes commit/push: negligible output to compress, and the cloud
// signed-commit guard keys on a leading `git` token that `rtk git …` would hide.
const GIT_COMPRESSIBLE_SUBCOMMANDS = new Set([
// Exported so the instruction-level Codex guidance advertises the same set.
export const GIT_COMPRESSIBLE_SUBCOMMANDS = new Set([
"status",
"diff",
"log",
Expand All @@ -44,7 +46,8 @@ const GIT_COMPRESSIBLE_SUBCOMMANDS = new Set([
// wrapping only its head would change the meaning of the rest.
const SHELL_OPERATORS = /[|&;<>`\n]|\$\(/;

function shQuote(value: string): string {
// Exported so the instruction-level Codex guidance quotes the prefix the same way.
export function shQuote(value: string): string {
if (/^[\w./-]+$/.test(value)) return value;
return `'${value.replace(/'/g, `'\\''`)}'`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1961,9 +1961,23 @@ describe("CodexAppServerAgent", () => {
} as unknown as PromptRequest);

// The single turn/completed resolves both the original and the folded prompt.
stub.emit("thread/tokenUsage/updated", {
tokenUsage: {
last: {
totalTokens: 45,
inputTokens: 30,
cachedInputTokens: 5,
outputTokens: 10,
},
},
});
stub.emit("turn/completed", { turn: { status: "completed" } });
expect((await first).stopReason).toBe("end_turn");
expect((await second).stopReason).toBe("end_turn");
const [firstResult, secondResult] = await Promise.all([first, second]);
expect(firstResult).toMatchObject({
stopReason: "end_turn",
usage: { totalTokens: 45 },
});
expect(secondResult).toEqual({ stopReason: "end_turn" });

const steer = stub.requests.find((r) => r.method === "turn/steer");
expect(steer?.params).toMatchObject({
Expand Down Expand Up @@ -2126,7 +2140,19 @@ describe("CodexAppServerAgent", () => {
},
});
stub.emit("turn/completed", { turn: { status: "completed" } });
await done;
const result = await done;

expect(result).toEqual({
stopReason: "end_turn",
usage: {
inputTokens: 60,
outputTokens: 30,
cachedReadTokens: 10,
cachedWriteTokens: 0,
thoughtTokens: 5,
totalTokens: 100,
},
});

const turnComplete = extNotifications.find(
(n) => n.method === "_posthog/turn_complete",
Expand Down Expand Up @@ -2818,6 +2844,16 @@ describe("CodexAppServerAgent", () => {
text: "The implementation plan is ready.",
},
});
stub.emit("thread/tokenUsage/updated", {
tokenUsage: {
last: {
totalTokens: 30,
inputTokens: 20,
outputTokens: 10,
reasoningOutputTokens: 2,
},
},
});
stub.emit("turn/completed", {
turn: { id: "turn_1", status: "completed" },
});
Expand All @@ -2826,10 +2862,31 @@ describe("CodexAppServerAgent", () => {
await waitUntil(
() => stub.requests.filter((r) => r.method === "turn/start").length >= 2,
);
stub.emit("thread/tokenUsage/updated", {
tokenUsage: {
last: {
totalTokens: 50,
inputTokens: 35,
cachedInputTokens: 5,
outputTokens: 10,
reasoningOutputTokens: 3,
},
},
});
stub.emit("turn/completed", {
turn: { id: "turn_2", status: "completed" },
});
expect((await done).stopReason).toBe("end_turn");
expect(await done).toEqual({
stopReason: "end_turn",
usage: {
inputTokens: 55,
outputTokens: 20,
cachedReadTokens: 5,
cachedWriteTokens: 0,
thoughtTokens: 5,
totalTokens: 80,
},
});

// The approval renders as the plan-approval UI (switch_mode + the plan text).
expect(permissionRequests).toHaveLength(1);
Expand Down
Loading
Loading