Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
1 change: 1 addition & 0 deletions .kilo/skills/specs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ to the user if instructions or changes will cause deviations from the original i

| Spec | Governs |
|---|---|
| `.specs/gastown-usage-based-billing.md` | Gastown container pricing, payer attribution, metering lifecycle, and budget enforcement |
| `.specs/kiloclaw-billing.md` | KiloClaw billing, pricing, invoicing, usage metering, payment flows |
| `.specs/kiloclaw-billing-lifecycle.md` | KiloClaw billing lifecycle — credit-renewal orchestration safety |
| `.specs/kiloclaw-composio.md` | KiloClaw Composio credential provisioning, injection, and sharing |
Expand Down
525 changes: 525 additions & 0 deletions .specs/gastown-usage-based-billing.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ Manage shared web env var additions and rotations with `pnpm web:env set <VARIAB
- `NEXT_PUBLIC_CLOUD_AGENT_NEXT_WS_URL` - WebSocket URL for Cloud Agent Next from the browser. [PUBLIC]
- `CLOUD_AGENT_R2_ATTACHMENTS_BUCKET_NAME` - R2 bucket for cloud agent file attachments. [SERVER]
- `GASTOWN_SERVICE_URL` - URL for the Gastown service. [SERVER]
- `GASTOWN_BILLING_ENABLED` - Enables Gastown container usage billing _enforcement_: admission checks and low-balance stops. Does not control metering — usage is always reported to the meter whenever the `CONTAINER_USAGE` binding is present. Enabled in the Gastown Wrangler development environment and defaults to `false` in production. [SERVER]
- `GASTOWN_BILLING_ANNOUNCEMENT_ENABLED` - Set to exactly `true` to show the upcoming usage-based container billing announcement on Gastown town overview pages. Always enabled when Next.js runs in development and defaults to off otherwise. [SERVER]
- `NEXT_PUBLIC_GASTOWN_URL` - Client-side base URL for Gastown. [PUBLIC]
- `O11Y_SERVICE_URL` - URL for the observability (O11Y) service. [SERVER]
- `O11Y_KILO_GATEWAY_CLIENT_SECRET` - Client secret for the O11Y Kilo Gateway. `[SECRET]`
Expand Down
18 changes: 18 additions & 0 deletions apps/web/src/app/(app)/gastown/[townId]/TownOverviewPageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
Layers,
MessageSquare,
Copy,
Info,
} from 'lucide-react';
import { toast } from 'sonner';
import { formatDistanceToNow } from 'date-fns';
Expand All @@ -38,6 +39,7 @@ import { motion, AnimatePresence } from 'motion/react';
import type { GastownOutputs } from '@/lib/gastown/trpc';
import { AdminViewingBanner } from '@/components/gastown/AdminViewingBanner';
import { DrainStatusBanner } from '@/components/gastown/DrainStatusBanner';
import { Banner } from '@/components/shared/Banner';
import { SidebarTrigger } from '@/components/ui/sidebar';
import { useConfirm } from '@/components/ui/confirm';

Expand All @@ -49,6 +51,7 @@ type TownOverviewPageClientProps = {
basePath?: string;
/** When set, integration queries use org-scoped endpoints. */
organizationId?: string;
billingAnnouncementEnabled: boolean;
};

const ROLE_ICONS: Record<string, typeof Bot> = {
Expand Down Expand Up @@ -94,6 +97,7 @@ export function TownOverviewPageClient({
townId,
basePath: basePathOverride,
organizationId,
billingAnnouncementEnabled,
}: TownOverviewPageClientProps) {
const townBasePath = basePathOverride ?? `/gastown/${townId}`;
const router = useRouter();
Expand Down Expand Up @@ -215,6 +219,20 @@ export function TownOverviewPageClient({
<AdminViewingBanner townId={townId} />
<div className="px-6">
<DrainStatusBanner townId={townId} />
{billingAnnouncementEnabled && (
<Banner color="blue" className="my-4 rounded-lg p-3">
<Banner.Icon>
<Info aria-hidden="true" />
</Banner.Icon>
<Banner.Content>
<Banner.Title>Usage-based container billing is coming</Banner.Title>
<Banner.Description>
Charges will use Kilo credits only while the town container is running. We'll share
more details before billing is activated.
</Banner.Description>
</Banner.Content>
</Banner>
)}
</div>
{/* Top bar — sticky */}
<div className="sticky top-0 z-10 flex items-center justify-between border-b border-white/[0.06] bg-[oklch(0.1_0_0)] px-6 py-3">
Expand Down
8 changes: 7 additions & 1 deletion apps/web/src/app/(app)/gastown/[townId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getUserFromAuthOrRedirect } from '@/lib/user/server';
import { notFound } from 'next/navigation';
import { isGastownEnabled } from '@/lib/gastown/feature-flags';
import { GASTOWN_BILLING_ANNOUNCEMENT_ENABLED } from '@/lib/config.server';
import { TownOverviewPageClient } from './TownOverviewPageClient';

export default async function TownOverviewPage({
Expand All @@ -15,5 +16,10 @@ export default async function TownOverviewPage({
return notFound();
}

return <TownOverviewPageClient townId={townId} />;
return (
<TownOverviewPageClient
townId={townId}
billingAnnouncementEnabled={GASTOWN_BILLING_ANNOUNCEMENT_ENABLED}
/>
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { OrganizationByPageLayout } from '@/components/organizations/OrganizationByPageLayout';
import { TownOverviewPageClient } from '@/app/(app)/gastown/[townId]/TownOverviewPageClient';
import { GASTOWN_BILLING_ANNOUNCEMENT_ENABLED } from '@/lib/config.server';

export default async function OrgTownOverviewPage({
params,
Expand All @@ -13,7 +14,12 @@ export default async function OrgTownOverviewPage({
params={params}
fullBleed
render={() => (
<TownOverviewPageClient townId={townId} basePath={basePath} organizationId={id} />
<TownOverviewPageClient
townId={townId}
basePath={basePath}
organizationId={id}
billingAnnouncementEnabled={GASTOWN_BILLING_ANNOUNCEMENT_ENABLED}
/>
)}
/>
);
Expand Down
16 changes: 15 additions & 1 deletion apps/web/src/components/gastown/MayorChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function MayorChat({ townId }: MayorChatProps) {
const trpc = useGastownTRPC();
const queryClient = useQueryClient();
const [collapsed, setCollapsed] = useState(false);
const billingQuery = useQuery(trpc.gastown.getBillingStatus.queryOptions({ townId }));

// Eagerly ensure mayor agent + container on mount
const ensureMayor = useMutation(
Expand Down Expand Up @@ -48,10 +49,16 @@ export function MayorChat({ townId }: MayorChatProps) {
// Reset on townId change so ensureMayor fires for each town
const ensuredTownRef = useRef<string | null>(null);
useEffect(() => {
if (
!billingQuery.isSuccess ||
billingQuery.data.enforcing ||
billingQuery.data.runPolicy === 'paused_by_user'
)
return;
if (ensuredTownRef.current === townId) return;
ensuredTownRef.current = townId;
ensureMayor.mutate({ townId });
}, [townId]);
}, [billingQuery.data, billingQuery.isSuccess, townId]);

// Poll mayor status to get agentId
const statusQuery = useQuery({
Expand All @@ -69,6 +76,13 @@ export function MayorChat({ townId }: MayorChatProps) {
const { terminalRef, connected, status, fitAddonRef } = useXtermPty({
townId,
agentId: mayorAgentId,
enabled:
billingQuery.isSuccess &&
billingQuery.data.runPolicy === 'automatic' &&
(!billingQuery.data.enforcing ||
Comment thread
jrf0110 marked this conversation as resolved.
billingQuery.data.state === 'running' ||
billingQuery.data.state === 'warning' ||
billingQuery.data.state === 'starting'),
retries: 10,
retryDelay: 3_000,
});
Expand Down
Loading
Loading