@neurocode-ai/enterprise 1.18.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/package.json +45 -0
- package/public/apple-touch-icon-v3.png +1 -0
- package/public/apple-touch-icon.png +1 -0
- package/public/favicon-96x96-v3.png +1 -0
- package/public/favicon-96x96.png +1 -0
- package/public/favicon-v3.ico +1 -0
- package/public/favicon-v3.svg +1 -0
- package/public/favicon.ico +1 -0
- package/public/favicon.svg +1 -0
- package/public/site.webmanifest +1 -0
- package/public/social-share-zen.png +1 -0
- package/public/social-share.png +1 -0
- package/public/web-app-manifest-192x192.png +1 -0
- package/public/web-app-manifest-512x512.png +1 -0
- package/script/scrap.ts +15 -0
- package/src/app.css +2 -0
- package/src/app.tsx +94 -0
- package/src/core/share.ts +232 -0
- package/src/core/storage.ts +129 -0
- package/src/custom-elements.d.ts +1 -0
- package/src/entry-client.tsx +4 -0
- package/src/entry-server.tsx +38 -0
- package/src/global.d.ts +5 -0
- package/src/routes/[...404].tsx +25 -0
- package/src/routes/api/[...path].ts +171 -0
- package/src/routes/index.tsx +3 -0
- package/src/routes/share/[shareID].tsx +417 -0
- package/src/routes/share.tsx +5 -0
- package/sst-env.d.ts +10 -0
- package/test/core/share.test.ts +292 -0
- package/test/core/storage.test.ts +64 -0
- package/test-debug.ts +40 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +37 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { APIEvent } from "@solidjs/start/server"
|
|
2
|
+
import { Hono } from "hono"
|
|
3
|
+
import { describeRoute, openAPIRouteHandler, resolver } from "hono-openapi"
|
|
4
|
+
import { validator } from "hono-openapi"
|
|
5
|
+
import z from "zod"
|
|
6
|
+
import { cors } from "hono/cors"
|
|
7
|
+
import { Share } from "~/core/share"
|
|
8
|
+
import { Resource } from "sst"
|
|
9
|
+
import { timingSafeEqual } from "node:crypto"
|
|
10
|
+
|
|
11
|
+
const app = new Hono()
|
|
12
|
+
|
|
13
|
+
app
|
|
14
|
+
.basePath("/api")
|
|
15
|
+
.use(cors())
|
|
16
|
+
.get(
|
|
17
|
+
"/doc",
|
|
18
|
+
openAPIRouteHandler(app, {
|
|
19
|
+
documentation: {
|
|
20
|
+
info: {
|
|
21
|
+
title: "Opencode Enterprise API",
|
|
22
|
+
version: "1.0.0",
|
|
23
|
+
description: "Opencode Enterprise API endpoints",
|
|
24
|
+
},
|
|
25
|
+
openapi: "3.1.1",
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
)
|
|
29
|
+
.post(
|
|
30
|
+
"/share",
|
|
31
|
+
describeRoute({
|
|
32
|
+
description: "Create a share",
|
|
33
|
+
operationId: "share.create",
|
|
34
|
+
responses: {
|
|
35
|
+
200: {
|
|
36
|
+
description: "Success",
|
|
37
|
+
content: {
|
|
38
|
+
"application/json": {
|
|
39
|
+
schema: resolver(
|
|
40
|
+
z
|
|
41
|
+
.object({
|
|
42
|
+
id: z.string(),
|
|
43
|
+
url: z.string(),
|
|
44
|
+
secret: z.string(),
|
|
45
|
+
})
|
|
46
|
+
.meta({ ref: "Share" }),
|
|
47
|
+
),
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
validator("json", z.object({ sessionID: z.string() })),
|
|
54
|
+
async (c) => {
|
|
55
|
+
const body = c.req.valid("json")
|
|
56
|
+
const share = await Share.create({ sessionID: body.sessionID })
|
|
57
|
+
const protocol = c.req.header("x-forwarded-proto") ?? c.req.header("x-forwarded-protocol") ?? "https"
|
|
58
|
+
const host = c.req.header("x-forwarded-host") ?? c.req.header("host")
|
|
59
|
+
return c.json({
|
|
60
|
+
id: share.id,
|
|
61
|
+
secret: share.secret,
|
|
62
|
+
url: `${protocol}://${host}/share/${share.id}`,
|
|
63
|
+
})
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
.post(
|
|
67
|
+
"/share/:shareID/sync",
|
|
68
|
+
describeRoute({
|
|
69
|
+
description: "Sync share data",
|
|
70
|
+
operationId: "share.sync",
|
|
71
|
+
responses: {
|
|
72
|
+
200: {
|
|
73
|
+
description: "Success",
|
|
74
|
+
content: {
|
|
75
|
+
"application/json": {
|
|
76
|
+
schema: resolver(z.object({})),
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
validator("param", z.object({ shareID: z.string() })),
|
|
83
|
+
validator("json", z.object({ secret: z.string(), data: Share.Data.array() })),
|
|
84
|
+
async (c) => {
|
|
85
|
+
const { shareID } = c.req.valid("param")
|
|
86
|
+
const body = c.req.valid("json")
|
|
87
|
+
await Share.sync({
|
|
88
|
+
share: { id: shareID, secret: body.secret },
|
|
89
|
+
data: body.data,
|
|
90
|
+
})
|
|
91
|
+
return c.json({})
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
.get(
|
|
95
|
+
"/share/:shareID/data",
|
|
96
|
+
describeRoute({
|
|
97
|
+
description: "Get share data",
|
|
98
|
+
operationId: "share.data",
|
|
99
|
+
responses: {
|
|
100
|
+
200: {
|
|
101
|
+
description: "Success",
|
|
102
|
+
content: {
|
|
103
|
+
"application/json": {
|
|
104
|
+
schema: resolver(z.array(Share.Data)),
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
validator("param", z.object({ shareID: z.string() })),
|
|
111
|
+
async (c) => {
|
|
112
|
+
const { shareID } = c.req.valid("param")
|
|
113
|
+
c.header("Cache-Control", "public, max-age=30, s-maxage=300, stale-while-revalidate=86400")
|
|
114
|
+
return c.json(await Share.data(shareID))
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
.delete(
|
|
118
|
+
"/share/:shareID",
|
|
119
|
+
describeRoute({
|
|
120
|
+
description: "Remove a share",
|
|
121
|
+
operationId: "share.remove",
|
|
122
|
+
responses: {
|
|
123
|
+
200: {
|
|
124
|
+
description: "Success",
|
|
125
|
+
content: {
|
|
126
|
+
"application/json": {
|
|
127
|
+
schema: resolver(z.object({})),
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
validator("param", z.object({ shareID: z.string() })),
|
|
134
|
+
validator("json", z.object({ secret: z.string() })),
|
|
135
|
+
async (c) => {
|
|
136
|
+
const { shareID } = c.req.valid("param")
|
|
137
|
+
const body = c.req.valid("json")
|
|
138
|
+
await Share.remove({ id: shareID, secret: body.secret })
|
|
139
|
+
return c.json({})
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
.delete("/support/actions/remove-share", async (c) => {
|
|
143
|
+
const authorization = c.req.header("authorization")
|
|
144
|
+
const expected = `Bearer ${(Resource as unknown as Record<string, { value: string }>).SUPPORT_API_KEY.value}`
|
|
145
|
+
const actual = Buffer.from(authorization ?? "")
|
|
146
|
+
const secret = Buffer.from(expected)
|
|
147
|
+
if (actual.length !== secret.length || !timingSafeEqual(actual, secret))
|
|
148
|
+
return c.json({ error: "Unauthorized" }, 401)
|
|
149
|
+
|
|
150
|
+
const body = z.object({ shareID: z.string().min(1) }).safeParse(await c.req.json().catch(() => undefined))
|
|
151
|
+
if (!body.success) return c.json({ error: "Invalid request", issues: body.error.issues }, 400)
|
|
152
|
+
return Share.removeAdmin({ id: body.data.shareID })
|
|
153
|
+
.then(() => c.json({ success: true, message: "Share removed" }))
|
|
154
|
+
.catch((error) => c.json({ error: error instanceof Error ? error.message : String(error) }, 400))
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
export function GET(event: APIEvent) {
|
|
158
|
+
return app.fetch(event.request)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function POST(event: APIEvent) {
|
|
162
|
+
return app.fetch(event.request)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function PUT(event: APIEvent) {
|
|
166
|
+
return app.fetch(event.request)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function DELETE(event: APIEvent) {
|
|
170
|
+
return app.fetch(event.request)
|
|
171
|
+
}
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { Message, Model, Part, Session, SessionStatus, SnapshotFileDiff, UserMessage } from "@neurocode-ai/sdk/v2"
|
|
2
|
+
import { SessionTurn } from "@neurocode-ai/session-ui/session-turn"
|
|
3
|
+
import { SessionReview } from "@neurocode-ai/session-ui/session-review"
|
|
4
|
+
import { DataProvider } from "@neurocode-ai/session-ui/context"
|
|
5
|
+
import { FileComponentProvider } from "@neurocode-ai/ui/context/file"
|
|
6
|
+
import { WorkerPoolProvider } from "@neurocode-ai/ui/context/worker-pool"
|
|
7
|
+
import { createAsync, query, useParams } from "@solidjs/router"
|
|
8
|
+
import { createMemo, createSignal, ErrorBoundary, For, Match, Show, Switch } from "solid-js"
|
|
9
|
+
import { Share } from "~/core/share"
|
|
10
|
+
import { Logo, Mark } from "@neurocode-ai/ui/logo"
|
|
11
|
+
import { IconButton } from "@neurocode-ai/ui/icon-button"
|
|
12
|
+
import { ProviderIcon } from "@neurocode-ai/ui/provider-icon"
|
|
13
|
+
import { iife } from "@neurocode-ai/core/util/iife"
|
|
14
|
+
import { Binary } from "@neurocode-ai/core/util/binary"
|
|
15
|
+
import { NamedError } from "@neurocode-ai/core/util/error"
|
|
16
|
+
import { DateTime } from "luxon"
|
|
17
|
+
import { createStore } from "solid-js/store"
|
|
18
|
+
import NotFound from "../[...404]"
|
|
19
|
+
import { Tabs } from "@neurocode-ai/ui/tabs"
|
|
20
|
+
import { MessageNav } from "@neurocode-ai/session-ui/message-nav"
|
|
21
|
+
import { FileSSR } from "@neurocode-ai/session-ui/file-ssr"
|
|
22
|
+
import { clientOnly } from "@solidjs/start"
|
|
23
|
+
import { Meta, Title } from "@solidjs/meta"
|
|
24
|
+
import { Base64 } from "js-base64"
|
|
25
|
+
import { getRequestEvent } from "solid-js/web"
|
|
26
|
+
|
|
27
|
+
const ClientOnlyWorkerPoolProvider = clientOnly(() =>
|
|
28
|
+
import("@neurocode-ai/session-ui/pierre/worker").then((m) => ({
|
|
29
|
+
default: (props: { children: any }) => (
|
|
30
|
+
<WorkerPoolProvider pools={m.getWorkerPools()}>{props.children}</WorkerPoolProvider>
|
|
31
|
+
),
|
|
32
|
+
})),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
class SessionDataMissingError extends NamedError {
|
|
36
|
+
public override readonly name = "SessionDataMissingError"
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
public readonly data: { sessionID: string; message?: string },
|
|
40
|
+
options?: ErrorOptions,
|
|
41
|
+
) {
|
|
42
|
+
super("SessionDataMissingError", options)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static isInstance(input: unknown): input is SessionDataMissingError {
|
|
46
|
+
return NamedError.hasName(input, "SessionDataMissingError")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
schema(): never {
|
|
50
|
+
throw new Error("SessionDataMissingError does not expose a schema")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
toObject() {
|
|
54
|
+
return { name: this.name, data: this.data }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const getData = query(async (shareID) => {
|
|
59
|
+
"use server"
|
|
60
|
+
const share = await Share.get(shareID)
|
|
61
|
+
if (!share) throw new SessionDataMissingError({ sessionID: shareID })
|
|
62
|
+
const data = await Share.data(shareID)
|
|
63
|
+
const result: {
|
|
64
|
+
sessionID: string
|
|
65
|
+
shareID: string
|
|
66
|
+
session: Session[]
|
|
67
|
+
session_diff: {
|
|
68
|
+
[sessionID: string]: SnapshotFileDiff[]
|
|
69
|
+
}
|
|
70
|
+
session_status: {
|
|
71
|
+
[sessionID: string]: SessionStatus
|
|
72
|
+
}
|
|
73
|
+
message: {
|
|
74
|
+
[sessionID: string]: Message[]
|
|
75
|
+
}
|
|
76
|
+
part: {
|
|
77
|
+
[messageID: string]: Part[]
|
|
78
|
+
}
|
|
79
|
+
model: {
|
|
80
|
+
[sessionID: string]: Model[]
|
|
81
|
+
}
|
|
82
|
+
} = {
|
|
83
|
+
sessionID: share.sessionID,
|
|
84
|
+
shareID,
|
|
85
|
+
session: [],
|
|
86
|
+
session_diff: {
|
|
87
|
+
[share.sessionID]: [],
|
|
88
|
+
},
|
|
89
|
+
session_status: {
|
|
90
|
+
[share.sessionID]: {
|
|
91
|
+
type: "idle",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
message: {},
|
|
95
|
+
part: {},
|
|
96
|
+
model: {},
|
|
97
|
+
}
|
|
98
|
+
for (const item of data) {
|
|
99
|
+
switch (item.type) {
|
|
100
|
+
case "session":
|
|
101
|
+
result.session.push(item.data)
|
|
102
|
+
break
|
|
103
|
+
case "session_diff":
|
|
104
|
+
result.session_diff[share.sessionID] = item.data
|
|
105
|
+
break
|
|
106
|
+
case "message":
|
|
107
|
+
result.message[item.data.sessionID] = result.message[item.data.sessionID] ?? []
|
|
108
|
+
result.message[item.data.sessionID].push(item.data)
|
|
109
|
+
break
|
|
110
|
+
case "part":
|
|
111
|
+
result.part[item.data.messageID] = result.part[item.data.messageID] ?? []
|
|
112
|
+
result.part[item.data.messageID].push(item.data)
|
|
113
|
+
break
|
|
114
|
+
case "model":
|
|
115
|
+
result.model[share.sessionID] = item.data
|
|
116
|
+
break
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const match = Binary.search(result.session, share.sessionID, (s) => s.id)
|
|
120
|
+
if (!match.found) throw new SessionDataMissingError({ sessionID: share.sessionID })
|
|
121
|
+
return result
|
|
122
|
+
}, "getShareData")
|
|
123
|
+
|
|
124
|
+
export default function () {
|
|
125
|
+
getRequestEvent()?.response.headers.set(
|
|
126
|
+
"Cache-Control",
|
|
127
|
+
"public, max-age=30, s-maxage=300, stale-while-revalidate=86400",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const params = useParams()
|
|
131
|
+
const data = createAsync(async () => {
|
|
132
|
+
if (!params.shareID) throw new Error("Missing shareID")
|
|
133
|
+
return getData(params.shareID)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<ErrorBoundary
|
|
138
|
+
fallback={(error) => {
|
|
139
|
+
if (SessionDataMissingError.isInstance(error)) {
|
|
140
|
+
return <NotFound />
|
|
141
|
+
}
|
|
142
|
+
console.error(error)
|
|
143
|
+
const details = error instanceof Error ? (error.stack ?? error.message) : String(error)
|
|
144
|
+
return (
|
|
145
|
+
<div class="min-h-screen w-full bg-background-base text-text-base flex flex-col items-center justify-center gap-4 p-6 text-center">
|
|
146
|
+
<p class="text-16-medium">Unable to render this share.</p>
|
|
147
|
+
<p class="text-14-regular text-text-weaker">Check the console for more details.</p>
|
|
148
|
+
<pre class="text-12-mono text-left whitespace-pre-wrap break-words w-full max-w-200 bg-background-stronger rounded-md p-4">
|
|
149
|
+
{details}
|
|
150
|
+
</pre>
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
<Meta name="robots" content="noindex, nofollow" />
|
|
156
|
+
<Show when={data()}>
|
|
157
|
+
{(data) => {
|
|
158
|
+
const match = createMemo(() => Binary.search(data().session, data().sessionID, (s) => s.id))
|
|
159
|
+
if (!match().found) throw new Error(`Session ${data().sessionID} not found`)
|
|
160
|
+
const info = createMemo(() => data().session[match().index])
|
|
161
|
+
const ogImage = createMemo(() => {
|
|
162
|
+
const models = new Set<string>()
|
|
163
|
+
const messages = data().message[data().sessionID] ?? []
|
|
164
|
+
for (const msg of messages) {
|
|
165
|
+
if (msg.role === "assistant" && msg.modelID) {
|
|
166
|
+
models.add(msg.modelID)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const modelIDs = Array.from(models)
|
|
170
|
+
const encodedTitle = encodeURIComponent(Base64.encode(encodeURIComponent(info().title.substring(0, 700))))
|
|
171
|
+
let modelParam: string
|
|
172
|
+
if (modelIDs.length === 1) {
|
|
173
|
+
modelParam = modelIDs[0]
|
|
174
|
+
} else if (modelIDs.length === 2) {
|
|
175
|
+
modelParam = encodeURIComponent(`${modelIDs[0]} & ${modelIDs[1]}`)
|
|
176
|
+
} else if (modelIDs.length > 2) {
|
|
177
|
+
modelParam = encodeURIComponent(`${modelIDs[0]} & ${modelIDs.length - 1} others`)
|
|
178
|
+
} else {
|
|
179
|
+
modelParam = "unknown"
|
|
180
|
+
}
|
|
181
|
+
const version = `v${info().version}`
|
|
182
|
+
return `https://social-cards.sst.dev/neurocode-share/${encodedTitle}.png?model=${modelParam}&version=${version}&id=${data().shareID}`
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<>
|
|
187
|
+
<Show when={info().title}>
|
|
188
|
+
<Title>{info().title} | OpenCode</Title>
|
|
189
|
+
</Show>
|
|
190
|
+
<Meta name="description" content="neurocode - The AI coding agent built for the terminal." />
|
|
191
|
+
<Meta property="og:image" content={ogImage()} />
|
|
192
|
+
<Meta name="twitter:image" content={ogImage()} />
|
|
193
|
+
<ClientOnlyWorkerPoolProvider>
|
|
194
|
+
<FileComponentProvider component={FileSSR}>
|
|
195
|
+
<DataProvider data={data()} directory={info().directory}>
|
|
196
|
+
{iife(() => {
|
|
197
|
+
const [store, setStore] = createStore({
|
|
198
|
+
messageId: undefined as string | undefined,
|
|
199
|
+
})
|
|
200
|
+
const messages = createMemo(() =>
|
|
201
|
+
data().sessionID
|
|
202
|
+
? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
|
|
203
|
+
(a, b) => a.time.created - b.time.created,
|
|
204
|
+
)
|
|
205
|
+
: [],
|
|
206
|
+
)
|
|
207
|
+
const firstUserMessage = createMemo(() => messages().at(0))
|
|
208
|
+
const activeMessage = createMemo(
|
|
209
|
+
() => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
|
|
210
|
+
)
|
|
211
|
+
function setActiveMessage(message: UserMessage | undefined) {
|
|
212
|
+
if (message) {
|
|
213
|
+
setStore("messageId", message.id)
|
|
214
|
+
} else {
|
|
215
|
+
setStore("messageId", undefined)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const provider = createMemo(() => activeMessage()?.model?.providerID)
|
|
219
|
+
const modelID = createMemo(() => activeMessage()?.model?.modelID)
|
|
220
|
+
const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
|
|
221
|
+
const diffs = createMemo(() => data().session_diff[data().sessionID] ?? [])
|
|
222
|
+
const [diffStyle, setDiffStyle] = createSignal<"unified" | "split">("unified")
|
|
223
|
+
|
|
224
|
+
const title = () => (
|
|
225
|
+
<div class="flex flex-col gap-4">
|
|
226
|
+
<div class="flex flex-col gap-2 sm:flex-row sm:gap-4 sm:items-center sm:h-8 justify-start self-stretch">
|
|
227
|
+
<div class="pl-[2.5px] pr-2 flex items-center gap-1.75 bg-surface-strong shadow-xs-border-base w-fit">
|
|
228
|
+
<Mark class="shrink-0 w-3 my-0.5" />
|
|
229
|
+
<div class="text-12-mono text-text-base">v{info().version}</div>
|
|
230
|
+
</div>
|
|
231
|
+
<div class="flex gap-4 items-center">
|
|
232
|
+
<div class="flex gap-2 items-center">
|
|
233
|
+
<Show when={provider()}>
|
|
234
|
+
<ProviderIcon id={provider()!} class="size-3.5 shrink-0 text-icon-strong-base" />
|
|
235
|
+
</Show>
|
|
236
|
+
<div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
|
|
237
|
+
</div>
|
|
238
|
+
<div class="text-12-regular text-text-weaker">
|
|
239
|
+
{DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
<div class="text-left text-16-medium text-text-strong">{info().title}</div>
|
|
244
|
+
</div>
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
const turns = () => (
|
|
248
|
+
<div class="relative mt-2 pb-8 min-w-0 w-full h-full overflow-y-auto no-scrollbar">
|
|
249
|
+
<div class="px-4 py-6">{title()}</div>
|
|
250
|
+
<div class="flex flex-col gap-15 items-start justify-start mt-4">
|
|
251
|
+
<For each={messages()}>
|
|
252
|
+
{(message) => (
|
|
253
|
+
<SessionTurn
|
|
254
|
+
sessionID={data().sessionID}
|
|
255
|
+
messageID={message.id}
|
|
256
|
+
classes={{
|
|
257
|
+
root: "min-w-0 w-full relative",
|
|
258
|
+
content: "flex flex-col justify-between !overflow-visible",
|
|
259
|
+
container: "px-4",
|
|
260
|
+
}}
|
|
261
|
+
/>
|
|
262
|
+
)}
|
|
263
|
+
</For>
|
|
264
|
+
</div>
|
|
265
|
+
<div class="px-4 flex items-center justify-center pt-20 pb-8 shrink-0">
|
|
266
|
+
<Logo class="w-58.5 opacity-12" />
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
const wide = createMemo(() => diffs().length === 0)
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<div class="relative bg-background-stronger w-screen h-screen overflow-hidden flex flex-col">
|
|
275
|
+
<header class="h-12 px-6 py-2 flex items-center justify-between self-stretch bg-background-base border-b border-border-weak-base">
|
|
276
|
+
<div class="">
|
|
277
|
+
<a href="https://opencode.ai">
|
|
278
|
+
<Mark />
|
|
279
|
+
</a>
|
|
280
|
+
</div>
|
|
281
|
+
<div class="flex gap-3 items-center">
|
|
282
|
+
<IconButton
|
|
283
|
+
as={"a"}
|
|
284
|
+
href="https://github.com/anomalyco/opencode"
|
|
285
|
+
target="_blank"
|
|
286
|
+
icon="github"
|
|
287
|
+
variant="ghost"
|
|
288
|
+
/>
|
|
289
|
+
<IconButton
|
|
290
|
+
as={"a"}
|
|
291
|
+
href="https://opencode.ai/discord"
|
|
292
|
+
target="_blank"
|
|
293
|
+
icon="discord"
|
|
294
|
+
variant="ghost"
|
|
295
|
+
/>
|
|
296
|
+
</div>
|
|
297
|
+
</header>
|
|
298
|
+
<div class="select-text flex flex-col flex-1 min-h-0">
|
|
299
|
+
<div
|
|
300
|
+
classList={{
|
|
301
|
+
"hidden w-full flex-1 min-h-0": true,
|
|
302
|
+
"md:flex": wide(),
|
|
303
|
+
"lg:flex": !wide(),
|
|
304
|
+
}}
|
|
305
|
+
>
|
|
306
|
+
<div
|
|
307
|
+
classList={{
|
|
308
|
+
"@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full": true,
|
|
309
|
+
}}
|
|
310
|
+
>
|
|
311
|
+
<div
|
|
312
|
+
classList={{
|
|
313
|
+
"w-full flex justify-start items-start min-w-0 px-6": true,
|
|
314
|
+
}}
|
|
315
|
+
>
|
|
316
|
+
{title()}
|
|
317
|
+
</div>
|
|
318
|
+
<div class="flex items-start justify-start h-full min-h-0">
|
|
319
|
+
<Show when={messages().length > 1}>
|
|
320
|
+
<MessageNav
|
|
321
|
+
class="sticky top-0 shrink-0 py-2 pl-4"
|
|
322
|
+
messages={messages()}
|
|
323
|
+
current={activeMessage()}
|
|
324
|
+
size="compact"
|
|
325
|
+
onMessageSelect={setActiveMessage}
|
|
326
|
+
getLabel={(message) =>
|
|
327
|
+
data()
|
|
328
|
+
.part[message.id]?.find((part) => part.type === "text")
|
|
329
|
+
?.text.trim()
|
|
330
|
+
.split("\n")[0]
|
|
331
|
+
}
|
|
332
|
+
/>
|
|
333
|
+
</Show>
|
|
334
|
+
<SessionTurn
|
|
335
|
+
sessionID={data().sessionID}
|
|
336
|
+
messageID={store.messageId ?? firstUserMessage()!.id!}
|
|
337
|
+
classes={{
|
|
338
|
+
root: "grow",
|
|
339
|
+
content: "flex flex-col justify-between",
|
|
340
|
+
container: "w-full pb-20 px-6",
|
|
341
|
+
}}
|
|
342
|
+
>
|
|
343
|
+
<div classList={{ "w-full flex items-center justify-center pb-8 shrink-0": true }}>
|
|
344
|
+
<Logo class="w-58.5 opacity-12" />
|
|
345
|
+
</div>
|
|
346
|
+
</SessionTurn>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
<Show when={diffs().length > 0}>
|
|
350
|
+
<div class="@container relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
|
|
351
|
+
<SessionReview
|
|
352
|
+
diffs={diffs()}
|
|
353
|
+
diffStyle={diffStyle()}
|
|
354
|
+
onDiffStyleChange={setDiffStyle}
|
|
355
|
+
classes={{
|
|
356
|
+
root: "pb-20",
|
|
357
|
+
header: "px-6",
|
|
358
|
+
container: "px-6",
|
|
359
|
+
}}
|
|
360
|
+
/>
|
|
361
|
+
</div>
|
|
362
|
+
</Show>
|
|
363
|
+
</div>
|
|
364
|
+
<Switch>
|
|
365
|
+
<Match when={diffs().length > 0}>
|
|
366
|
+
<Tabs classList={{ "md:hidden": wide(), "lg:hidden": !wide() }}>
|
|
367
|
+
<Tabs.List>
|
|
368
|
+
<Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
|
|
369
|
+
Session
|
|
370
|
+
</Tabs.Trigger>
|
|
371
|
+
<Tabs.Trigger
|
|
372
|
+
value="review"
|
|
373
|
+
class="w-1/2 !border-r-0"
|
|
374
|
+
classes={{ button: "w-full" }}
|
|
375
|
+
>
|
|
376
|
+
{diffs().length} Files Changed
|
|
377
|
+
</Tabs.Trigger>
|
|
378
|
+
</Tabs.List>
|
|
379
|
+
<Tabs.Content value="session" class="!overflow-hidden">
|
|
380
|
+
{turns()}
|
|
381
|
+
</Tabs.Content>
|
|
382
|
+
<Tabs.Content value="review" class="!overflow-hidden hidden data-[selected]:block">
|
|
383
|
+
<div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
|
|
384
|
+
<SessionReview
|
|
385
|
+
diffs={diffs()}
|
|
386
|
+
classes={{
|
|
387
|
+
root: "pb-20",
|
|
388
|
+
header: "px-4",
|
|
389
|
+
container: "px-4",
|
|
390
|
+
}}
|
|
391
|
+
/>
|
|
392
|
+
</div>
|
|
393
|
+
</Tabs.Content>
|
|
394
|
+
</Tabs>
|
|
395
|
+
</Match>
|
|
396
|
+
<Match when={true}>
|
|
397
|
+
<div
|
|
398
|
+
classList={{ "!overflow-hidden": true, "md:hidden": wide(), "lg:hidden": !wide() }}
|
|
399
|
+
>
|
|
400
|
+
{turns()}
|
|
401
|
+
</div>
|
|
402
|
+
</Match>
|
|
403
|
+
</Switch>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
)
|
|
407
|
+
})}
|
|
408
|
+
</DataProvider>
|
|
409
|
+
</FileComponentProvider>
|
|
410
|
+
</ClientOnlyWorkerPoolProvider>
|
|
411
|
+
</>
|
|
412
|
+
)
|
|
413
|
+
}}
|
|
414
|
+
</Show>
|
|
415
|
+
</ErrorBoundary>
|
|
416
|
+
)
|
|
417
|
+
}
|
package/sst-env.d.ts
ADDED