Skip to content

Commit c6e6a59

Browse files
authored
Merge branch 'dev' into dev
2 parents 3407713 + 425a4f5 commit c6e6a59

33 files changed

Lines changed: 7328 additions & 752 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"dev:desktop": "bun --cwd packages/desktop dev",
1111
"dev:web": "bun --cwd packages/app dev",
1212
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
13-
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
13+
"dev:stats": "bun sst shell --stage=dev -- bun run --cwd packages/stats/app dev",
1414
"dev:storybook": "bun --cwd packages/storybook storybook",
1515
"lint": "oxlint",
1616
"typecheck": "bun turbo typecheck",

packages/console/app/src/component/go-referral.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import { action, json, query, useAction, useSubmission } from "@solidjs/router"
22
import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js"
33
import { getRequestEvent } from "solid-js/web"
44
import { Referral } from "@opencode-ai/console-core/referral.js"
5+
import { Actor } from "@opencode-ai/console-core/actor.js"
56
import { withActor } from "~/context/auth.withActor"
67
import { Modal } from "~/component/modal"
78
import { IconCheck, IconCopy } from "~/component/icon"
89
import { useI18n } from "~/context/i18n"
910
import { useLanguage } from "~/context/language"
1011
import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time"
1112
import { queryLiteSubscription } from "~/routes/workspace/[id]/go/lite-section"
13+
import { clearReferralCookie, referralCodeFromCookieHeader } from "~/lib/referral-invite"
1214
import "./go-referral.css"
1315

1416
type GoReferralSummary = Awaited<ReturnType<typeof Referral.summary>>
@@ -25,7 +27,21 @@ const emptyUsagePreview = {
2527

2628
export const queryGoReferral = query(async (workspaceID: string) => {
2729
"use server"
28-
return withActor(() => Referral.summary(), workspaceID)
30+
return withActor(async () => {
31+
const event = getRequestEvent()
32+
const referralCode = referralCodeFromCookieHeader(event?.request.headers.get("cookie") ?? null)
33+
if (referralCode) {
34+
await Referral.createFromAccount({
35+
accountID: Actor.account(),
36+
referralCode,
37+
}).catch((error) => {
38+
console.error("Referral create failed", error)
39+
})
40+
event?.response.headers.append("set-cookie", clearReferralCookie())
41+
}
42+
43+
return Referral.summary()
44+
}, workspaceID)
2945
}, "go.referral.get")
3046

3147
export const queryGoReferralUsagePreview = query(async (workspaceID: string, referralID?: string) => {
@@ -65,6 +81,8 @@ function rewardDescriptionKey(source: GoReferralReward["source"]) {
6581

6682
function rewardActionKey(reward: GoReferralReward, hasActiveGo: boolean) {
6783
if (reward.status === "applied") return "workspace.referral.reward.action.applied" as const
84+
if (reward.status === "pending" && reward.source === "inviter")
85+
return "workspace.referral.reward.source.pendingInviter" as const
6886
if (reward.status === "pending" || !hasActiveGo) return "workspace.referral.reward.action.subscribeUnlock" as const
6987
return "workspace.referral.reward.action.view" as const
7088
}

packages/console/app/src/routes/auth/[...callback].ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { redirect } from "@solidjs/router"
22
import type { APIEvent } from "@solidjs/start/server"
3-
import { Referral } from "@opencode-ai/console-core/referral.js"
43
import { AuthClient } from "~/context/auth"
54
import { useAuthSession } from "~/context/auth"
65
import { i18n } from "~/i18n"
76
import { localeFromRequest, route } from "~/lib/language"
8-
import { clearReferralCookie, referralCodeFromCookieHeader } from "~/lib/referral-invite"
97

108
export async function GET(input: APIEvent) {
119
const url = new URL(input.request.url)
@@ -19,7 +17,6 @@ export async function GET(input: APIEvent) {
1917
if (result.err) throw new Error(result.err.message)
2018
const decoded = AuthClient.decode(result.tokens.access, {} as any)
2119
if (decoded.err) throw new Error(decoded.err.message)
22-
const referralCode = referralCodeFromCookieHeader(input.request.headers.get("cookie"))
2320
const session = await useAuthSession()
2421
const id = decoded.subject.properties.accountID
2522
await session.update((value) => {
@@ -35,15 +32,8 @@ export async function GET(input: APIEvent) {
3532
current: id,
3633
}
3734
})
38-
if (decoded.subject.properties.newAccount && referralCode) {
39-
await Referral.createFromAccount({ accountID: id, referralCode }).catch((error) => {
40-
console.error("Referral create failed", error)
41-
})
42-
}
4335
const next = url.pathname === "/auth/callback" ? "/auth" : url.pathname.replace("/auth/callback", "")
44-
const response = redirect(route(locale, next))
45-
if (referralCode) response.headers.append("set-cookie", clearReferralCookie())
46-
return response
36+
return redirect(route(locale, next))
4737
} catch (e: any) {
4838
return new Response(
4939
JSON.stringify({

packages/console/app/vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default defineConfig({
1717
],
1818
server: {
1919
allowedHosts: true,
20+
port: 3001,
2021
},
2122
build: {
2223
rollupOptions: {

packages/console/core/src/referral.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { z } from "zod"
2-
import { and, asc, eq, isNull, sql, Database } from "./drizzle"
2+
import { and, asc, eq, inArray, isNull, sql, Database } from "./drizzle"
33
import { Actor } from "./actor"
44
import { Identifier } from "./identifier"
5-
import { LiteTable } from "./schema/billing.sql"
5+
import { LiteTable, PaymentTable } from "./schema/billing.sql"
66
import { ReferralCodeTable, ReferralRewardTable, ReferralTable } from "./schema/referral.sql"
77
import { AuthTable } from "./schema/auth.sql"
88
import { UserTable } from "./schema/user.sql"
@@ -318,6 +318,26 @@ export namespace Referral {
318318
.then((rows) => rows[0])
319319
if (selfReferral) throw new Error("Self-referral is not allowed")
320320

321+
const workspaceIDs = await tx
322+
.select({ workspaceID: UserTable.workspaceID })
323+
.from(UserTable)
324+
.where(and(eq(UserTable.accountID, input.accountID), isNull(UserTable.timeDeleted)))
325+
.then((rows) => rows.map((row) => row.workspaceID))
326+
if (workspaceIDs.length === 0) return
327+
328+
const litePayment = await tx
329+
.select({ id: PaymentTable.id })
330+
.from(PaymentTable)
331+
.where(
332+
and(
333+
inArray(PaymentTable.workspaceID, workspaceIDs),
334+
isNull(PaymentTable.timeDeleted),
335+
sql`JSON_UNQUOTE(JSON_EXTRACT(${PaymentTable.enrichment}, '$.type')) = 'lite'`,
336+
),
337+
)
338+
.then((rows) => rows[0])
339+
if (litePayment) return
340+
321341
const referralID = Identifier.create("referral")
322342
await tx.insert(ReferralTable).ignore().values({
323343
workspaceID: code.workspaceID,
@@ -355,7 +375,7 @@ export namespace Referral {
355375
.from(ReferralTable)
356376
.where(and(eq(ReferralTable.inviteeAccountID, invitee.accountID), isNull(ReferralTable.timeDeleted)))
357377
.then((rows) => rows[0])
358-
if (!referral) throw new Error("Referral not found")
378+
if (!referral) return
359379

360380
const result = await tx
361381
.insert(ReferralRewardTable)
@@ -373,7 +393,7 @@ export namespace Referral {
373393
},
374394
])
375395

376-
if (result.rowsAffected === 0) throw new Error("Referral already completed")
396+
if (result.rowsAffected === 0) return
377397
})
378398
}
379399

packages/enterprise/vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export default defineConfig({
2929
server: {
3030
host: "0.0.0.0",
3131
allowedHosts: true,
32+
port: 3002,
3233
},
3334
worker: {
3435
format: "es",

packages/opencode/src/acp-next/service.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -259,21 +259,35 @@ export function make(input: {
259259
),
260260
"session",
261261
)
262-
const sorted = sessions.toSorted((a, b) => b.time.updated - a.time.updated)
262+
const serverEntries = sessions.map(
263+
(item): SessionInfo => ({
264+
sessionId: item.id,
265+
cwd: item.directory,
266+
title: item.title,
267+
updatedAt: new Date(item.time.updated).toISOString(),
268+
}),
269+
)
270+
const liveEntries = (yield* session.list(params.cwd ?? undefined))
271+
.filter((item) => !serverEntries.some((entry) => entry.sessionId === item.id))
272+
.map(
273+
(item): SessionInfo => ({
274+
sessionId: item.id,
275+
cwd: item.cwd,
276+
updatedAt: item.createdAt.toISOString(),
277+
}),
278+
)
279+
const sorted = [...liveEntries, ...serverEntries].toSorted(
280+
(a, b) => new Date(b.updatedAt ?? 0).getTime() - new Date(a.updatedAt ?? 0).getTime(),
281+
)
263282
const filtered =
264-
cursor === undefined || !Number.isFinite(cursor) ? sorted : sorted.filter((item) => item.time.updated < cursor)
283+
cursor === undefined || !Number.isFinite(cursor)
284+
? sorted
285+
: sorted.filter((item) => new Date(item.updatedAt ?? 0).getTime() < cursor)
265286
const page = filtered.slice(0, limit)
266287
const last = page.at(-1)
267288
return {
268-
sessions: page.map(
269-
(item): SessionInfo => ({
270-
sessionId: item.id,
271-
cwd: item.directory,
272-
title: item.title,
273-
updatedAt: new Date(item.time.updated).toISOString(),
274-
}),
275-
),
276-
...(filtered.length > limit && last ? { nextCursor: String(last.time.updated) } : {}),
289+
sessions: page,
290+
...(filtered.length > limit && last ? { nextCursor: String(new Date(last.updatedAt ?? 0).getTime()) } : {}),
277291
}
278292
})
279293

packages/opencode/src/acp-next/session.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export type PartMetadataLookupInput = {
6060
export type Interface = {
6161
readonly create: (input: StoreInput) => Effect.Effect<Info>
6262
readonly load: (input: StoreInput) => Effect.Effect<Info>
63+
readonly list: (cwd?: string) => Effect.Effect<readonly Info[]>
6364
readonly get: (sessionId: string) => Effect.Effect<Info, ACPNextError.SessionNotFoundError>
6465
readonly tryGet: (sessionId: string) => Effect.Effect<Info | undefined>
6566
readonly remove: (sessionId: string) => Effect.Effect<Info | undefined>
@@ -168,6 +169,12 @@ export const layer = Layer.effect(
168169
return Service.of({
169170
create: store,
170171
load: store,
172+
list: Effect.fn("ACPNext.Session.list")(function* (cwd?: string) {
173+
return [...(yield* Ref.get(sessions)).values()]
174+
.filter((session) => !cwd || session.cwd === cwd)
175+
.map(snapshot)
176+
.toSorted((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
177+
}),
171178
get,
172179
tryGet,
173180
remove,

packages/opencode/test/acp-next/service-session.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,15 @@ describe("ACP next service sessions", () => {
323323
expect(second.sessions).toEqual(first.sessions)
324324
})
325325

326+
it("includes live ACP sessions before they appear in server-backed session list", async () => {
327+
const { service } = makeService()
328+
const created = await Effect.runPromise(service.newSession({ cwd: "/workspace", mcpServers: [] }))
329+
const listed = await Effect.runPromise(service.listSessions({ cwd: "/workspace" }))
330+
331+
expect(listed.sessions[0]?.sessionId).toBe(created.sessionId)
332+
expect(listed.sessions[0]?.cwd).toBe("/workspace")
333+
})
334+
326335
it("lists all sessions with next cursor when the first page is full", async () => {
327336
const { service } = makeService()
328337
const first = await Effect.runPromise(service.listSessions({}))

packages/opencode/test/cli/acp-next/helpers.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,6 @@ import {
1111
type AcpClient,
1212
} from "../acp/acp-test-client"
1313

14-
export const diagnosticFirstSessionThresholdMs = 5_000
15-
export const diagnosticFastPathThresholdMs = 15_000
16-
17-
// TODO: tighten to the public verifier target of 500ms once acp-next startup is optimized.
18-
export const finalFirstSessionThresholdMs = 500
19-
// TODO: tighten warm session/config/skill fast paths to the public verifier target of 100ms.
20-
export const finalFastPathThresholdMs = 100
21-
2214
export function createAcpNextClient(input: Pick<CliFixture, "opencode">, env?: Record<string, string>) {
2315
return Effect.gen(function* () {
2416
return createAcpClient(

0 commit comments

Comments
 (0)