Skip to content

Commit 659ba1c

Browse files
authored
Merge branch 'main' into posthog-code/delete-seat-billing
2 parents c0a330b + c505944 commit 659ba1c

10 files changed

Lines changed: 80 additions & 115 deletions

File tree

packages/agent/src/adapters/codex-app-server/mapping.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,9 @@ function describeTool(item: AppServerItem): ToolDescriptor | null {
410410
output: dynamicToolText(item.contentItems),
411411
};
412412
case "collabAgentToolCall":
413+
if (item.tool === "wait" || item.tool === "closeAgent") {
414+
return null;
415+
}
413416
return {
414417
title: collabAgentTitle(item),
415418
kind: "other",

packages/core/src/billing/usageDisplay.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,11 +264,27 @@ describe("formatResetTime", () => {
264264
expected: "Resets shortly",
265265
},
266266
])("$name", ({ resetAt, expected }) => {
267-
const result = formatResetTime(resetAt, NOW);
267+
const result = formatResetTime(resetAt, { now: NOW });
268268
if (expected instanceof RegExp) {
269269
expect(result).toMatch(expected);
270270
} else {
271271
expect(result).toBe(expected);
272272
}
273273
});
274+
275+
it("swaps the leading phrase when a custom label is given", () => {
276+
const opts = { now: NOW, label: "Billing period ends" };
277+
expect(formatResetTime(isoAt(30 * 60 * 1000), opts)).toBe(
278+
"Billing period ends in 30m",
279+
);
280+
expect(formatResetTime(isoAt(4 * 3600 * 1000), opts)).toBe(
281+
"Billing period ends in 4h",
282+
);
283+
expect(formatResetTime(isoAt(-60_000), opts)).toBe(
284+
"Billing period ends shortly",
285+
);
286+
expect(formatResetTime(isoAt(30 * 86400 * 1000), opts)).toMatch(
287+
/^Billing period ends [A-Za-z]+ \d+ at /,
288+
);
289+
});
274290
});

packages/core/src/billing/usageDisplay.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,14 @@ export function isUsageExceeded(usage: UsageOutput): boolean {
8888

8989
export function formatResetTime(
9090
resetAtIso: string,
91-
now: number = Date.now(),
91+
{ now = Date.now(), label = "Resets" }: { now?: number; label?: string } = {},
9292
): string {
9393
const parsed = Date.parse(resetAtIso);
9494
const ms = Number.isNaN(parsed) ? 0 : Math.max(0, parsed - now);
9595

9696
const totalMinutes = Math.ceil(ms / 60_000);
97-
if (totalMinutes <= 0) return "Resets shortly";
98-
if (totalMinutes < 60) return `Resets in ${totalMinutes}m`;
97+
if (totalMinutes <= 0) return `${label} shortly`;
98+
if (totalMinutes < 60) return `${label} in ${totalMinutes}m`;
9999

100100
const totalHours = ms / 3_600_000;
101101
if (totalHours < 24) {
@@ -106,8 +106,8 @@ export function formatResetTime(
106106
minutes = 0;
107107
}
108108
return minutes === 0
109-
? `Resets in ${hours}h`
110-
: `Resets in ${hours}h ${minutes}m`;
109+
? `${label} in ${hours}h`
110+
: `${label} in ${hours}h ${minutes}m`;
111111
}
112112

113113
const target = new Date(now + ms);
@@ -120,5 +120,5 @@ export function formatResetTime(
120120
minute: "2-digit",
121121
timeZoneName: "short",
122122
});
123-
return `Resets ${date} at ${time}`;
123+
return `${label} ${date} at ${time}`;
124124
}

packages/ui/src/features/agent-applications/agent-builder/AgentBuilderDock.tsx

Lines changed: 12 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import { useAgentChatPendingApproval } from "../hooks/useAgentChatPendingApprova
2323
import { agentIngressBaseUrl } from "../utils/ingress";
2424
import { AgentBuilderMcpConnectDialog } from "./AgentBuilderMcpConnectDialog";
2525
import { AgentBuilderSecretForm } from "./AgentBuilderSecretForm";
26-
import { AgentBuilderSeedDialog } from "./AgentBuilderSeedDialog";
2726
import {
2827
AGENT_BUILDER_CHAT_ID,
2928
AGENT_BUILDER_SLUG,
@@ -322,37 +321,22 @@ export function AgentBuilderDock() {
322321
setPendingMcpConnect(null);
323322
}
324323

325-
// Edit-with-AI hand-offs: send the seeded prompt once when a new seed lands.
326-
// An empty dock starts immediately; if a chat is already in progress, confirm
327-
// whether to start fresh or continue (so a deliberate "New agent" / "Edit with
328-
// AI" doesn't silently wipe or append onto an unrelated conversation).
324+
// Contextual hand-offs ("New agent" / "Edit with AI" / …): prefill the
325+
// seeded prompt into the composer when a new seed lands — never send it. The
326+
// user reviews and hits send, so opening the dock doesn't fire a chat on its
327+
// own. Prefilling is non-destructive, so no start-fresh-vs-continue prompt is
328+
// needed: it drops into whatever chat is open, and the header "New chat" (+)
329+
// clears first if the user wants a fresh conversation.
329330
const lastSeedRef = useRef(0);
330-
const [seedConfirm, setSeedConfirm] = useState<string | null>(null);
331+
const [draft, setDraft] = useState<{ text: string; token: number } | null>(
332+
null,
333+
);
331334
useEffect(() => {
332335
if (!seed || seed.seq === lastSeedRef.current) return;
333336
lastSeedRef.current = seed.seq;
334337
consumeSeed(seed.seq);
335-
if (chat.messages.length === 0) {
336-
chat.send(seed.prompt);
337-
} else {
338-
setSeedConfirm(seed.prompt);
339-
}
340-
}, [seed, chat, consumeSeed]);
341-
342-
function seedStartFresh() {
343-
if (!seedConfirm) return;
344-
setPendingSecret(null);
345-
setPendingMcpConnect(null);
346-
chat.newChat();
347-
setLastSession(null);
348-
chat.send(seedConfirm);
349-
setSeedConfirm(null);
350-
}
351-
function seedContinue() {
352-
if (!seedConfirm) return;
353-
chat.send(seedConfirm);
354-
setSeedConfirm(null);
355-
}
338+
setDraft({ text: seed.prompt, token: seed.seq });
339+
}, [seed, consumeSeed]);
356340

357341
return (
358342
<Flex
@@ -432,6 +416,7 @@ export function AgentBuilderDock() {
432416
placeholder={placeholder}
433417
emptyState={<AgentBuilderEmptyState page={page} onPick={chat.send} />}
434418
emptyHint="Ask the agent builder to inspect, debug, or edit your agents. It can see what you're looking at and walk you there."
419+
draft={draft ?? undefined}
435420
belowConversation={
436421
pendingApproval ? (
437422
<AgentChatPendingApprovalCard
@@ -459,14 +444,6 @@ export function AgentBuilderDock() {
459444
/>
460445
)}
461446

462-
<AgentBuilderSeedDialog
463-
open={seedConfirm != null}
464-
prompt={seedConfirm ?? ""}
465-
onStartFresh={seedStartFresh}
466-
onContinue={seedContinue}
467-
onCancel={() => setSeedConfirm(null)}
468-
/>
469-
470447
<AgentBuilderMcpConnectDialog
471448
pending={pendingMcpConnect}
472449
busy={mcpConnectBusy}

packages/ui/src/features/agent-applications/agent-builder/AgentBuilderSeedDialog.tsx

Lines changed: 0 additions & 58 deletions
This file was deleted.

packages/ui/src/features/agent-applications/agent-builder/agentBuilderStore.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export type AgentBuilderPageContext =
2727
| { kind: "agent-chat"; slug: string }
2828
| { kind: "unknown" };
2929

30-
/** A pending "Edit with AI" hand-off: open the dock and send `prompt`. */
30+
/** A pending contextual hand-off: open the dock and prefill the composer with
31+
* `prompt` (the user reviews and sends — it is never auto-sent). */
3132
export interface AgentBuilderSeed {
3233
/** Monotonic id so a consumer can mark exactly one seed handled. */
3334
seq: number;
@@ -113,7 +114,7 @@ interface AgentBuilderStore {
113114
setVisible: (visible: boolean) => void;
114115
setFollowMode: (followMode: boolean) => void;
115116
setPage: (page: AgentBuilderPageContext) => void;
116-
/** Open the dock and queue a prompt to send. */
117+
/** Open the dock and prefill the composer with a prompt (not sent). */
117118
startAgentBuilder: (prompt: string, agentSlug?: string | null) => void;
118119
/** Mark a seed handled (no-op if a newer seed has since replaced it). */
119120
consumeSeed: (seq: number) => void;

packages/ui/src/features/agent-applications/components/AgentChatSurface.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ import {
88
import type { AcpMessage } from "@posthog/shared";
99
import { ThreadView } from "@posthog/ui/features/sessions/components/ThreadView";
1010
import { Flex, Text, Tooltip } from "@radix-ui/themes";
11-
import { type KeyboardEvent, type ReactNode, useState } from "react";
11+
import {
12+
type KeyboardEvent,
13+
type ReactNode,
14+
useEffect,
15+
useRef,
16+
useState,
17+
} from "react";
1218

1319
/**
1420
* The conversation + composer half of a deployed-agent chat, shared by the
@@ -28,6 +34,7 @@ export function AgentChatSurface({
2834
composerDisabledReason,
2935
scrollX = true,
3036
placeholder = "Message this agent…",
37+
draft,
3138
onSend,
3239
onCancel,
3340
}: {
@@ -50,6 +57,10 @@ export function AgentChatSurface({
5057
scrollX?: boolean;
5158
/** Composer placeholder. */
5259
placeholder?: string;
60+
/** When set, prefills the composer with `text` (without sending). Bump
61+
* `token` each time a new draft should repopulate the input — the same text
62+
* re-applies on a fresh token so re-triggering a seeded prompt works. */
63+
draft?: { text: string; token: number };
5364
onSend: (text: string) => void;
5465
onCancel: () => void;
5566
}) {
@@ -84,6 +95,7 @@ export function AgentChatSurface({
8495
isStreaming={isStreaming}
8596
placeholder={placeholder}
8697
disabledReason={composerDisabledReason}
98+
draft={draft}
8799
onSend={onSend}
88100
onCancel={onCancel}
89101
/>
@@ -95,17 +107,33 @@ function Composer({
95107
isStreaming,
96108
placeholder,
97109
disabledReason,
110+
draft,
98111
onSend,
99112
onCancel,
100113
}: {
101114
isStreaming: boolean;
102115
placeholder: string;
103116
disabledReason?: string;
117+
draft?: { text: string; token: number };
104118
onSend: (text: string) => void;
105119
onCancel: () => void;
106120
}) {
107121
const [text, setText] = useState("");
108122
const parked = !!disabledReason;
123+
const textareaRef = useRef<HTMLTextAreaElement>(null);
124+
125+
// Prefill (but don't send) when a new draft lands — a seeded prompt drops into
126+
// the composer for the user to review, edit, and send. Keyed on `token` so the
127+
// same prompt re-applies on a fresh trigger. Never clobber text the user has
128+
// already typed but not sent: if the composer is non-empty, leave it as-is
129+
// (still focusing it) so a seeded prompt is genuinely non-destructive.
130+
const lastDraftToken = useRef<number | null>(null);
131+
useEffect(() => {
132+
if (!draft || draft.token === lastDraftToken.current) return;
133+
lastDraftToken.current = draft.token;
134+
setText((current) => (current.trim() ? current : draft.text));
135+
textareaRef.current?.focus();
136+
}, [draft]);
109137

110138
function submit() {
111139
if (parked) return;
@@ -132,6 +160,7 @@ function Composer({
132160
<div className="shrink-0 px-3 pt-2 pb-3">
133161
<InputGroup className="h-auto cursor-text bg-card focus-within:ring-1 focus-within:ring-purple-9">
134162
<InputGroupTextarea
163+
ref={textareaRef}
135164
value={text}
136165
onChange={(e) => setText(e.target.value)}
137166
onKeyDown={onKeyDown}

packages/ui/src/features/billing/UsageButton.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,10 @@ export function UsageButton() {
6868
meter.kind === "dollars"
6969
? `${formatUsdAmount(meter.usedUsd)} of ${formatUsdAmount(meter.limitUsd)} used`
7070
: `${percent}% used`;
71-
const resetLabel = formatResetTime(
72-
meter.kind === "dollars" ? meter.resetAt : meter.bucket.reset_at,
73-
);
71+
const resetLabel =
72+
meter.kind === "dollars"
73+
? formatResetTime(meter.resetAt, { label: "Billing period ends" })
74+
: formatResetTime(meter.bucket.reset_at);
7475
const breakdownLabel =
7576
meter.kind === "dollars" && meter.breakdown
7677
? formatUsageBreakdown(meter.breakdown)

packages/ui/src/features/settings/sections/PlanUsageSettings.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,6 @@ import { getBillingUrl } from "@posthog/ui/utils/urls";
3030
import { Badge, Button, Callout, Flex, Spinner, Text } from "@radix-ui/themes";
3131
import { useEffect } from "react";
3232

33-
/**
34-
* Plan & usage under usage-based billing: no seats or plans to manage in-app —
35-
* payment methods, spend limits, and org-level usage live on the PostHog
36-
* billing page. The free-tier meters show the per-user allowance for
37-
* confirmed free-tier orgs; subscribed orgs have no per-user caps to meter.
38-
*/
3933
export function PlanUsageSettings() {
4034
const billingEnabled = useFeatureFlag(BILLING_FLAG);
4135
const spendAnalysisEnabled = useSpendAnalysisEnabled();
@@ -163,7 +157,9 @@ export function PlanUsageSettings() {
163157
</Flex>
164158

165159
<Flex direction="column" gap="3">
166-
<Text className="font-medium text-(--gray-9) text-sm">Usage</Text>
160+
<Text className="font-medium text-(--gray-9) text-sm">
161+
Organization usage
162+
</Text>
167163
{usageLoading ? (
168164
<Flex
169165
align="center"
@@ -178,7 +174,7 @@ export function PlanUsageSettings() {
178174
label={freeTier ? "Monthly free usage" : "Usage this period"}
179175
percent={meter.percent}
180176
valueLabel={`${formatUsdAmount(meter.usedUsd)} of ${formatUsdAmount(meter.limitUsd)}${freeTier ? " included" : ""}`}
181-
detail={`${meter.exceeded ? "Limit exceeded. " : ""}${formatResetTime(meter.resetAt)}`}
177+
detail={`${meter.exceeded ? "Limit exceeded. " : ""}${formatResetTime(meter.resetAt, { label: "Billing period ends" })}`}
182178
breakdown={
183179
meter.breakdown
184180
? { ...meter.breakdown, usedUsd: meter.usedUsd }

packages/ui/src/features/usage/components/SpendAnalysisSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function SpendAnalysisSection() {
3838
<Flex direction="column" gap="3">
3939
<Flex align="center" justify="between">
4040
<Text className="font-medium text-(--gray-9) text-sm">
41-
Spend analysis
41+
Personal spend analysis
4242
</Text>
4343
<Flex align="center" gap="4">
4444
<WindowSelector value={spendWindow} onChange={setSpendWindow} />

0 commit comments

Comments
 (0)