Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f05d2f5
feat(wasteland): squash staging-v3 work into single commit
jrf0110 May 13, 2026
fde04ca
feat(wasteland): three-card upstream picker + create-new-upstream end…
jrf0110 May 14, 2026
9f5c4d4
WIP wasm libwl
jrf0110 May 14, 2026
018f350
wip(wasteland): worker-side bootstrap + UI refactor + DoltHub read fa…
jrf0110 May 16, 2026
65af2ce
feat(wl-sdk): scaffold @kilocode/wl-sdk package
jrf0110 May 16, 2026
658a5af
feat(wl-sdk): port EscapeSQL/EscapeLIKE from wasteland Go SDK
jrf0110 May 16, 2026
0d4ecfc
feat(wl-sdk): typed DoltHub HTTP client with anon-read fallback and p…
jrf0110 May 16, 2026
b5ff793
feat(wl-sdk): schema generator with DML helpers from commons.sql
jrf0110 May 16, 2026
decf598
feat(wl-sdk): protocol ops layer (claim, post, browse, join, publish,…
jrf0110 May 16, 2026
26aefd7
feat(wl-sdk): WlClient class and finalized public surface
jrf0110 May 16, 2026
dae9bf2
feat(wasteland): wire wl-sdk into tRPC behind WL_SDK_ENABLED flag
jrf0110 May 16, 2026
688f34d
feat(wasteland): owner/repo lookup via registry + resolveOwnerRepo pr…
jrf0110 May 16, 2026
ec40dd3
feat(web): owner/repo-keyed wasteland routes with three-place navigation
jrf0110 May 16, 2026
5038528
feat(wasteland+web): fork workshop view and pulls view at owner/repo …
jrf0110 May 16, 2026
90a530a
feat(web): settings page at /wasteland/[owner]/[repo]/settings
jrf0110 May 16, 2026
3286272
feat(web): item drawer with three-tab anchor (Upstream / My branch / …
jrf0110 May 16, 2026
0e3270c
feat(wasteland+web): explicit fork-and-register connect ceremony
jrf0110 May 16, 2026
2db2051
feat(web): redirect legacy UUID wasteland URLs to owner/repo URLs
jrf0110 May 17, 2026
5aa0384
chore(wasteland): delete libwl, container runtime, and WL_SDK_ENABLED…
jrf0110 May 17, 2026
93d1a57
chore(wasteland): v3 migration drops WastelandContainerDO + remove wa…
jrf0110 May 17, 2026
0a8529d
docs(wasteland): final M3.6 sweep \u2014 docs and stale comments
jrf0110 May 17, 2026
25e6077
chore(wasteland): remove final stale 'container' references in source
jrf0110 May 17, 2026
7838704
feat(wasteland): harden DoltHub review workflows
jrf0110 May 18, 2026
7fc58cf
feat(wasteland+web): stale-fork detection, sync CTA, accept idempoten…
jrf0110 May 19, 2026
2c5361a
chore(wasteland): drop unused WASTELAND_JWT_SECRET binding
jrf0110 May 19, 2026
d34953e
fix(web): clear pre-existing lint errors blocking pre-push hook
jrf0110 May 19, 2026
f954593
Merge branch 'main' into wasteland-staging-v3
jrf0110 May 19, 2026
881b5fb
chore: revert .husky/pre-push and drop wasteland-gastown-poc plan
jrf0110 May 19, 2026
0f51eff
Merge branch 'main' into wasteland-staging-v3
jrf0110 May 19, 2026
2928755
chore: dev-server fixes after main merge
jrf0110 May 19, 2026
797366a
fix(web): revert apps/web/tsconfig.json changes that broke CI
jrf0110 May 19, 2026
ce8ef04
chore(web): remove wasteland legacy URL bridge from next.config.mjs
jrf0110 May 19, 2026
0d64665
chore: revert packageManager bump to pnpm@11.1.2
jrf0110 May 19, 2026
696ff09
chore(wasteland): Omit wasteland from auto-deploy
jrf0110 May 19, 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
6 changes: 3 additions & 3 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ LINKEDIN_CLIENT_SECRET=your-linkedin-client-secret
DISCORD_CLIENT_ID=your-discord-client-id
DISCORD_CLIENT_SECRET=your-discord-client-secret

# DoltHub OAuth (dev only — app pending admin approval)
DOLTHUB_APP_DEV_CLIENT_ID=your-dolthub-client-id
DOLTHUB_APP_DEV_CLIENT_SECRET=your-dolthub-client-secret
# DoltHub OAuth
DOLTHUB_APP_CLIENT_ID=your-dolthub-client-id
DOLTHUB_APP_CLIENT_SECRET=your-dolthub-client-secret
# ============================================================================
# Billing & Stripe (Required for billing features)
# ============================================================================
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/deploy-workers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ jobs:
EXCLUDED=(
services/kiloclaw # Docker-based deploy in deploy-production.yml
services/gastown # Deployed separately
services/wasteland # Deployed separately
services/deploy-infra/builder-docker-container/container-files # Build artifact template, not a deployable worker
)

Expand Down
4 changes: 3 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"**/.wrangler/**",
"supabase/functions/**",
"**/types/opencode.gen.ts",
"**/gastown/types/**"
"**/gastown/types/**",
"**/wasteland/types/**"
],
"rules": {
"constructor-super": "off",
Expand Down Expand Up @@ -182,6 +183,7 @@
"services/cloud-agent/src/session/queries/*.ts",
"services/cloud-agent-next/src/session/queries/*.ts",
"services/gastown/**/*.ts",
"services/wasteland/**/*.ts",
"services/webhook-agent-ingest/**/*.ts",
"services/kiloclaw/**/*.ts"
],
Expand Down
3 changes: 3 additions & 0 deletions apps/web/.env.development.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ NEXT_PUBLIC_KILO_CHAT_URL=http://localhost:8808
# @url event-service
NEXT_PUBLIC_EVENT_SERVICE_URL=ws://localhost:8809

# @url wasteland
# NEXT_PUBLIC_WASTELAND_URL=http://localhost:8790

# KiloClaw legacy recognized Stripe price IDs (test values)
# Required so settlement, plan detection, and legacy intro repair continue to recognize pre-rollout subscriptions.
STRIPE_KILOCLAW_2026_03_19_STANDARD_INTRO_PRICE_ID=price_test_kiloclaw_2026_03_19_standard_intro
Expand Down
33 changes: 33 additions & 0 deletions apps/web/src/app/(app)/components/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useUrlOrganizationId } from '@/hooks/useUrlOrganizationId';
import PersonalAppSidebar from './PersonalAppSidebar';
import OrganizationAppSidebar from './OrganizationAppSidebar';
import { GastownTownSidebar } from '@/components/gastown/GastownTownSidebar';
import { WastelandSidebar } from '@/components/wasteland/WastelandSidebar';

const UUID = '[0-9a-f-]{36}';

Expand All @@ -26,6 +27,18 @@ function isKiloClawNewPath(pathname: string): boolean {
return pathname === '/claw/new' || new RegExp(`^/organizations/${UUID}/claw/new$`).test(pathname);
}

/** Extract the wastelandId from a /wasteland/[wastelandId] pathname, or null. */
function extractWastelandId(pathname: string): string | null {
const match = pathname.match(new RegExp(`^/wasteland/(${UUID})`));
return match ? match[1] : null;
}

/** Extract {orgId, wastelandId} from an /organizations/[id]/wasteland/[wastelandId] pathname, or null. */
function extractOrgWastelandId(pathname: string): { orgId: string; wastelandId: string } | null {
const match = pathname.match(new RegExp(`^/organizations/(${UUID})/wasteland/(${UUID})`));
return match ? { orgId: match[1], wastelandId: match[2] } : null;
}

export default function AppSidebar(props: React.ComponentProps<typeof Sidebar>) {
const currentOrgId = useUrlOrganizationId();
const pathname = usePathname();
Expand Down Expand Up @@ -78,6 +91,26 @@ export default function AppSidebar(props: React.ComponentProps<typeof Sidebar>)
);
}

// Personal wasteland — show the wasteland-specific sidebar
const wastelandId = extractWastelandId(pathname);
if (wastelandId) {
return <WastelandSidebar wastelandId={wastelandId} {...props} />;
}

// Org wasteland — show the same sidebar with org-prefixed paths
const orgWasteland = extractOrgWastelandId(pathname);
if (orgWasteland) {
const orgBase = `/organizations/${orgWasteland.orgId}`;
return (
<WastelandSidebar
wastelandId={orgWasteland.wastelandId}
basePath={`${orgBase}/wasteland/${orgWasteland.wastelandId}`}
backHref={`${orgBase}/wasteland`}
{...props}
/>
);
}

// Render organization sidebar if viewing an organization
if (currentOrgId) {
return <OrganizationAppSidebar organizationId={currentOrgId} {...props} />;
Expand Down
81 changes: 80 additions & 1 deletion apps/web/src/app/(app)/gastown/TownListPageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,48 @@ import { useEffect, useRef } from 'react';
import { useRouter } from 'next/navigation';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useGastownTRPC } from '@/lib/gastown/trpc';
import { useWastelandTRPC } from '@/lib/wasteland/trpc';
import { PageContainer } from '@/components/layouts/PageContainer';
import { Button } from '@/components/Button';
import { Badge } from '@/components/ui/badge';
import { SetPageTitle } from '@/components/SetPageTitle';
import { Card, CardContent } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { GastownBackdrop } from '@/components/gastown/GastownBackdrop';
import { Plus, Factory, Trash2 } from 'lucide-react';
import { Plus, Factory, Trash2, Skull } from 'lucide-react';
import { toast } from 'sonner';
import { formatDistanceToNow } from 'date-fns';
import {
WastelandCard,
WastelandListSkeleton,
} from '@/app/(app)/wasteland/_components/WastelandListComponents';
import { parseDolthubUpstream } from '@/lib/wasteland/upstream';

/**
* Wastelands live on the personal gastown overview rather than as a
* top-level nav item. The mental model the product wants to surface is
* "wasteland is a thing your town connects to," so the list of
* wastelands sits underneath the towns list on this page. Org
* gastown overviews deliberately omit this section — wastelands are
* personal-scoped only.
*/
function linkForWasteland(wasteland: { wasteland_id: string; dolthub_upstream: string | null }) {
const upstream = parseDolthubUpstream(wasteland.dolthub_upstream);
if (upstream) return `/wasteland/${upstream.owner}/${upstream.repo}`;
return `/wasteland/${wasteland.wasteland_id}`;
}

export function TownListPageClient() {
const router = useRouter();
const trpc = useGastownTRPC();
const wastelandTrpc = useWastelandTRPC();

const queryClient = useQueryClient();
const townsQuery = useQuery(trpc.gastown.listTowns.queryOptions());
const wastelandsQuery = useQuery({
...wastelandTrpc.wasteland.listWastelands.queryOptions({}),
refetchInterval: 30_000,
});
const didAutoRedirect = useRef(false);

// Auto-redirect new users with no towns to the onboarding wizard (once per page load)
Expand Down Expand Up @@ -163,6 +188,60 @@ export function TownListPageClient() {
))}
</div>
)}

{/* ── Wastelands section ─────────────────────────────────────────
Sits underneath the towns list to reinforce "your town connects
to a wasteland." Identical visual rhythm to the towns list:
header bar with a `New Wasteland` action, skeleton during load,
empty-state copy that mentions the relationship to towns. */}
<section className="space-y-3">
<div className="flex items-end justify-between gap-3">
<div>
<h2 className="text-base font-medium text-white/85">Wastelands</h2>
<p className="mt-0.5 text-xs text-white/45">
Hosted bounty boards your towns can connect to. Backed by DoltHub.
</p>
</div>
<Button
variant="secondary"
size="sm"
onClick={() => router.push('/wasteland/new')}
className="shrink-0 gap-1.5 border-white/10 bg-white/[0.04] text-white/80 hover:bg-white/[0.08]"
>
<Plus className="size-3.5" />
New Wasteland
</Button>
</div>

{wastelandsQuery.isLoading && <WastelandListSkeleton />}

{wastelandsQuery.data && wastelandsQuery.data.length === 0 && (
<div className="rounded-2xl border border-white/[0.06] bg-white/[0.02] px-5 py-6">
<div className="flex flex-col items-center gap-3 text-center">
<Skull className="size-8 text-white/35" />
<div>
<p className="text-sm font-medium text-white/80">No wastelands yet</p>
<p className="mt-1 max-w-md text-xs text-white/50">
Create one to start posting wanted items, then connect a town to it from the
town&apos;s settings.
</p>
</div>
</div>
</div>
)}

{wastelandsQuery.data && wastelandsQuery.data.length > 0 && (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{wastelandsQuery.data.map(wasteland => (
<WastelandCard
key={wasteland.wasteland_id}
wasteland={wasteland}
onClick={() => router.push(linkForWasteland(wasteland))}
/>
))}
</div>
)}
</section>
</PageContainer>
);
}
Loading