Merge branch 'dev' into project
This commit is contained in:
@@ -64,3 +64,4 @@
|
|||||||
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
|
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
|
||||||
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
|
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
|
||||||
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
|
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
|
||||||
|
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
|
||||||
|
|||||||
@@ -54,7 +54,10 @@
|
|||||||
|
|
||||||
a,
|
a,
|
||||||
button {
|
button {
|
||||||
|
appearance: none;
|
||||||
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
import "./workspace.css"
|
import "./workspace.css"
|
||||||
import { useAuthSession } from "~/context/auth.session"
|
import { useAuthSession } from "~/context/auth.session"
|
||||||
import { IconLogo } from "../component/icon"
|
import { IconLogo } from "../component/icon"
|
||||||
import { action, redirect, RouteSectionProps } from "@solidjs/router"
|
import { withActor } from "~/context/auth.withActor"
|
||||||
|
import "./workspace.css"
|
||||||
|
import { query, action, redirect, createAsync, RouteSectionProps } from "@solidjs/router"
|
||||||
|
import { User } from "@opencode/cloud-core/user.js"
|
||||||
|
import { Actor } from "@opencode/cloud-core/actor.js"
|
||||||
|
|
||||||
|
const getUserInfo = query(async () => {
|
||||||
|
"use server"
|
||||||
|
return withActor(async () => {
|
||||||
|
const actor = Actor.assert("user")
|
||||||
|
const user = await User.fromID(actor.properties.userID)
|
||||||
|
return { user }
|
||||||
|
})
|
||||||
|
}, "userInfo")
|
||||||
|
|
||||||
const logout = action(async () => {
|
const logout = action(async () => {
|
||||||
"use server"
|
"use server"
|
||||||
@@ -17,6 +30,7 @@ const logout = action(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export default function WorkspaceLayout(props: RouteSectionProps) {
|
export default function WorkspaceLayout(props: RouteSectionProps) {
|
||||||
|
const userInfo = createAsync(() => getUserInfo())
|
||||||
return (
|
return (
|
||||||
<main data-page="workspace">
|
<main data-page="workspace">
|
||||||
<header data-component="workspace-header">
|
<header data-component="workspace-header">
|
||||||
@@ -26,7 +40,9 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div data-slot="header-actions">
|
<div data-slot="header-actions">
|
||||||
<span>name@example.com</span>
|
{userInfo() &&
|
||||||
|
<span>{userInfo()!.user.email}</span>
|
||||||
|
}
|
||||||
<form action={logout} method="post">
|
<form action={logout} method="post">
|
||||||
<button type="submit" formaction={logout}>Logout</button>
|
<button type="submit" formaction={logout}>Logout</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -23,10 +23,9 @@
|
|||||||
padding-bottom: var(--space-16);
|
padding-bottom: var(--space-16);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/* Common elements */
|
/* Common elements */
|
||||||
button {
|
button {
|
||||||
padding: var(--space-3) var(--space-4);
|
padding: var(--space-3) var(--space-4);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
@@ -59,7 +58,7 @@ button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[color="primary"] {
|
&[data-color="primary"] {
|
||||||
background-color: var(--color-primary);
|
background-color: var(--color-primary);
|
||||||
border-color: var(--color-primary);
|
border-color: var(--color-primary);
|
||||||
color: var(--color-primary-text);
|
color: var(--color-primary-text);
|
||||||
@@ -70,7 +69,7 @@ button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[color="ghost"] {
|
&[data-color="ghost"] {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
@@ -81,16 +80,16 @@ button {
|
|||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-underline-offset: var(--space-0-75);
|
text-underline-offset: var(--space-0-75);
|
||||||
text-decoration-thickness: 1px;
|
text-decoration-thickness: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="empty-state"] {
|
[data-slot="empty-state"] {
|
||||||
padding: var(--space-20) var(--space-6);
|
padding: var(--space-20) var(--space-6);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 1px dashed var(--color-border);
|
border: 1px dashed var(--color-border);
|
||||||
@@ -104,10 +103,10 @@ a {
|
|||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Title section */
|
/* Title section */
|
||||||
[data-slot="title-section"] {
|
[data-slot="title-section"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
@@ -136,10 +135,10 @@ a {
|
|||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Section titles */
|
/* Section titles */
|
||||||
[data-slot="section-title"] {
|
[data-slot="section-title"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-1);
|
gap: var(--space-1);
|
||||||
@@ -163,21 +162,19 @@ a {
|
|||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* API Keys Section */
|
/* API Keys Section */
|
||||||
[data-slot="api-keys-section"] {
|
[data-slot="api-keys-section"] {
|
||||||
[data-slot="create-form"] {
|
[data-slot="create-form"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
background-color: var(--color-bg-surface);
|
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
max-width: 32rem;
|
|
||||||
|
|
||||||
input {
|
input {
|
||||||
|
flex: 1;
|
||||||
padding: var(--space-2) var(--space-3);
|
padding: var(--space-2) var(--space-3);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
@@ -199,7 +196,6 @@ a {
|
|||||||
[data-slot="form-actions"] {
|
[data-slot="form-actions"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,10 +278,10 @@ a {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Balance Section */
|
/* Balance Section */
|
||||||
[data-slot="balance-section"] {
|
[data-slot="balance-section"] {
|
||||||
[data-slot="balance"] {
|
[data-slot="balance"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -326,10 +322,10 @@ a {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Payments Section */
|
/* Payments Section */
|
||||||
[data-slot="payments-section"] {
|
[data-slot="payments-section"] {
|
||||||
[data-slot="payments-table"] {
|
[data-slot="payments-table"] {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
@@ -400,10 +396,10 @@ a {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Usage Section */
|
/* Usage Section */
|
||||||
[data-slot="usage-section"] {
|
[data-slot="usage-section"] {
|
||||||
[data-slot="usage-table"] {
|
[data-slot="usage-table"] {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
@@ -474,4 +470,5 @@ a {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,116 +63,8 @@ const createPortalUrl = action(async (returnUrl: string) => {
|
|||||||
return withActor(() => Billing.generatePortalUrl({ returnUrl }))
|
return withActor(() => Billing.generatePortalUrl({ returnUrl }))
|
||||||
}, "portalUrl")
|
}, "portalUrl")
|
||||||
|
|
||||||
const dummyUsageData = [
|
export default function() {
|
||||||
{
|
|
||||||
model: "claude-3-5-sonnet-20241022",
|
|
||||||
inputTokens: 1250,
|
|
||||||
outputTokens: 890,
|
|
||||||
reasoningTokens: 150,
|
|
||||||
cacheReadTokens: 0,
|
|
||||||
cacheWriteTokens: 45,
|
|
||||||
cost: 12340000,
|
|
||||||
timeCreated: new Date("2025-01-28T10:30:00Z"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: "claude-3-haiku-20240307",
|
|
||||||
inputTokens: 2100,
|
|
||||||
outputTokens: 450,
|
|
||||||
reasoningTokens: null,
|
|
||||||
cacheReadTokens: 120,
|
|
||||||
cacheWriteTokens: 0,
|
|
||||||
cost: 5670000,
|
|
||||||
timeCreated: new Date("2025-01-27T15:22:00Z"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: "claude-3-5-sonnet-20241022",
|
|
||||||
inputTokens: 850,
|
|
||||||
outputTokens: 1200,
|
|
||||||
reasoningTokens: 220,
|
|
||||||
cacheReadTokens: 30,
|
|
||||||
cacheWriteTokens: 15,
|
|
||||||
cost: 18990000,
|
|
||||||
timeCreated: new Date("2025-01-27T09:15:00Z"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: "claude-3-opus-20240229",
|
|
||||||
inputTokens: 3200,
|
|
||||||
outputTokens: 1800,
|
|
||||||
reasoningTokens: 400,
|
|
||||||
cacheReadTokens: 0,
|
|
||||||
cacheWriteTokens: 100,
|
|
||||||
cost: 45670000,
|
|
||||||
timeCreated: new Date("2025-01-26T14:45:00Z"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: "claude-3-haiku-20240307",
|
|
||||||
inputTokens: 650,
|
|
||||||
outputTokens: 280,
|
|
||||||
reasoningTokens: null,
|
|
||||||
cacheReadTokens: 200,
|
|
||||||
cacheWriteTokens: 0,
|
|
||||||
cost: 2340000,
|
|
||||||
timeCreated: new Date("2025-01-25T16:18:00Z"),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const dummyPaymentData = [
|
|
||||||
{
|
|
||||||
id: "pay_1Ab2Cd3Ef4Gh5678",
|
|
||||||
amount: 2000000000,
|
|
||||||
timeCreated: new Date("2025-01-28T14:32:00Z"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "pay_9Ij8Kl7Mn6Op5432",
|
|
||||||
amount: 1000000000,
|
|
||||||
timeCreated: new Date("2025-01-25T09:18:00Z"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "pay_5Qr4St3Uv2Wx1098",
|
|
||||||
amount: 5000000000,
|
|
||||||
timeCreated: new Date("2025-01-20T16:45:00Z"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "pay_7Yz6Ab5Cd4Ef3210",
|
|
||||||
amount: 1500000000,
|
|
||||||
timeCreated: new Date("2025-01-15T11:22:00Z"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "pay_3Gh2Ij1Kl0Mn9876",
|
|
||||||
amount: 3000000000,
|
|
||||||
timeCreated: new Date("2025-01-10T13:55:00Z"),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const dummyApiKeyData = [
|
|
||||||
{
|
|
||||||
id: "key_1Ab2Cd3Ef4Gh5678",
|
|
||||||
name: "Production API",
|
|
||||||
key: "oc_live_sk_1Ab2Cd3Ef4Gh567890123456789012345678901234567890",
|
|
||||||
timeCreated: new Date("2025-01-28T14:32:00Z"),
|
|
||||||
timeUsed: new Date("2025-01-29T09:15:00Z"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "key_9Ij8Kl7Mn6Op5432",
|
|
||||||
name: "Development Key",
|
|
||||||
key: "oc_test_sk_9Ij8Kl7Mn6Op543210987654321098765432109876543210",
|
|
||||||
timeCreated: new Date("2025-01-25T09:18:00Z"),
|
|
||||||
timeUsed: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "key_5Qr4St3Uv2Wx1098",
|
|
||||||
name: "CI/CD Pipeline",
|
|
||||||
key: "oc_live_sk_5Qr4St3Uv2Wx109876543210987654321098765432109876",
|
|
||||||
timeCreated: new Date("2025-01-20T16:45:00Z"),
|
|
||||||
timeUsed: new Date("2025-01-28T12:30:00Z"),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function () {
|
|
||||||
const actor = createAsync(() => getActor())
|
const actor = createAsync(() => getActor())
|
||||||
onMount(() => {
|
|
||||||
console.log("MOUNTED", actor())
|
|
||||||
})
|
|
||||||
|
|
||||||
/////////////////
|
/////////////////
|
||||||
// Keys section
|
// Keys section
|
||||||
@@ -292,15 +184,6 @@ export default function () {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div data-slot="sections">
|
<div data-slot="sections">
|
||||||
{/* Actor Section */}
|
|
||||||
<section data-slot="actor-section">
|
|
||||||
<div data-slot="section-title">
|
|
||||||
<h2>Actor</h2>
|
|
||||||
<p>Current authenticated user information and session details.</p>
|
|
||||||
</div>
|
|
||||||
<div>{JSON.stringify(actor())}</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* API Keys Section */}
|
{/* API Keys Section */}
|
||||||
<section data-slot="api-keys-section">
|
<section data-slot="api-keys-section">
|
||||||
<div data-slot="section-title">
|
<div data-slot="section-title">
|
||||||
@@ -321,14 +204,7 @@ export default function () {
|
|||||||
/>
|
/>
|
||||||
<div data-slot="form-actions">
|
<div data-slot="form-actions">
|
||||||
<button
|
<button
|
||||||
color="primary"
|
data-color="ghost"
|
||||||
disabled={createKeySubmission.pending || !keyName().trim()}
|
|
||||||
onClick={handleCreateKey}
|
|
||||||
>
|
|
||||||
{createKeySubmission.pending ? "Creating..." : "Create"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
color="ghost"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowCreateForm(false)
|
setShowCreateForm(false)
|
||||||
setKeyName("")
|
setKeyName("")
|
||||||
@@ -336,12 +212,19 @@ export default function () {
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
data-color="primary"
|
||||||
|
disabled={createKeySubmission.pending || !keyName().trim()}
|
||||||
|
onClick={handleCreateKey}
|
||||||
|
>
|
||||||
|
{createKeySubmission.pending ? "Creating..." : "Create"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
color="primary"
|
data-color="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log("clicked")
|
console.log("clicked")
|
||||||
setShowCreateForm(true)
|
setShowCreateForm(true)
|
||||||
@@ -370,7 +253,6 @@ export default function () {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<For each={keys()!}>
|
<For each={keys()!}>
|
||||||
{/* Real data: keys() */}
|
|
||||||
{(key) => (
|
{(key) => (
|
||||||
<tr>
|
<tr>
|
||||||
<td data-slot="key-name">{key.name}</td>
|
<td data-slot="key-name">{key.name}</td>
|
||||||
@@ -389,7 +271,7 @@ export default function () {
|
|||||||
{formatDateForTable(key.timeCreated)}
|
{formatDateForTable(key.timeCreated)}
|
||||||
</td>
|
</td>
|
||||||
<td data-slot="key-actions">
|
<td data-slot="key-actions">
|
||||||
<button color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key">
|
<button data-color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key">
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -426,51 +308,12 @@ export default function () {
|
|||||||
})()}
|
})()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button color="primary" disabled={createCheckoutUrlSubmission.pending} onClick={handleBuyCredits}>
|
<button data-color="primary" disabled={createCheckoutUrlSubmission.pending} onClick={handleBuyCredits}>
|
||||||
{createCheckoutUrlSubmission.pending ? "Loading..." : "Buy Credits"}
|
{createCheckoutUrlSubmission.pending ? "Loading..." : "Buy Credits"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Payments Section */}
|
|
||||||
<Show when={dummyPaymentData.length > 0}>
|
|
||||||
{/* Real data condition: billingInfo() && billingInfo()!.payments.length > 0 */}
|
|
||||||
<section data-slot="payments-section">
|
|
||||||
<div data-slot="section-title">
|
|
||||||
<h2>Payments History</h2>
|
|
||||||
<p>Recent payment transactions.</p>
|
|
||||||
</div>
|
|
||||||
<div data-slot="payments-table">
|
|
||||||
<table data-slot="payments-table-element">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>Payment ID</th>
|
|
||||||
<th>Amount</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<For each={dummyPaymentData}>
|
|
||||||
{/* Real data: billingInfo()?.payments */}
|
|
||||||
{(payment) => {
|
|
||||||
const date = new Date(payment.timeCreated)
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
<td data-slot="payment-date" title={formatDateUTC(date)}>
|
|
||||||
{formatDateForTable(date)}
|
|
||||||
</td>
|
|
||||||
<td data-slot="payment-id">{payment.id}</td>
|
|
||||||
<td data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* Usage Section */}
|
{/* Usage Section */}
|
||||||
<section data-slot="usage-section">
|
<section data-slot="usage-section">
|
||||||
<div data-slot="section-title">
|
<div data-slot="section-title">
|
||||||
@@ -479,7 +322,7 @@ export default function () {
|
|||||||
</div>
|
</div>
|
||||||
<div data-slot="usage-table">
|
<div data-slot="usage-table">
|
||||||
<Show
|
<Show
|
||||||
when={dummyUsageData.length > 0}
|
when={billingInfo() && billingInfo()!.usage.length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<div data-slot="empty-state">
|
<div data-slot="empty-state">
|
||||||
<p>Make your first API call to get started.</p>
|
<p>Make your first API call to get started.</p>
|
||||||
@@ -496,7 +339,7 @@ export default function () {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<For each={dummyUsageData}>
|
<For each={billingInfo()!.usage}>
|
||||||
{(usage) => {
|
{(usage) => {
|
||||||
const totalTokens = usage.inputTokens + usage.outputTokens + (usage.reasoningTokens || 0)
|
const totalTokens = usage.inputTokens + usage.outputTokens + (usage.reasoningTokens || 0)
|
||||||
const date = new Date(usage.timeCreated)
|
const date = new Date(usage.timeCreated)
|
||||||
@@ -517,6 +360,44 @@ export default function () {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Payments Section */}
|
||||||
|
<Show when={billingInfo() && billingInfo()!.payments.length > 0}>
|
||||||
|
<section data-slot="payments-section">
|
||||||
|
<div data-slot="section-title">
|
||||||
|
<h2>Payments History</h2>
|
||||||
|
<p>Recent payment transactions.</p>
|
||||||
|
</div>
|
||||||
|
<div data-slot="payments-table">
|
||||||
|
<table data-slot="payments-table-element">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Payment ID</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<For each={billingInfo()!.payments}>
|
||||||
|
{(payment) => {
|
||||||
|
const date = new Date(payment.timeCreated)
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td data-slot="payment-date" title={formatDateUTC(date)}>
|
||||||
|
{formatDateForTable(date)}
|
||||||
|
</td>
|
||||||
|
<td data-slot="payment-id">{payment.id}</td>
|
||||||
|
<td data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Show>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -397,6 +397,21 @@ export namespace Config {
|
|||||||
.object({
|
.object({
|
||||||
apiKey: z.string().optional(),
|
apiKey: z.string().optional(),
|
||||||
baseURL: z.string().optional(),
|
baseURL: z.string().optional(),
|
||||||
|
timeout: z
|
||||||
|
.union([
|
||||||
|
z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.describe(
|
||||||
|
"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
|
||||||
|
),
|
||||||
|
z.literal(false).describe("Disable timeout for this provider entirely."),
|
||||||
|
])
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.catchall(z.any())
|
.catchall(z.any())
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ export namespace LSPClient {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
connection.onRequest("workspace/configuration", async () => {
|
connection.onRequest("workspace/configuration", async () => {
|
||||||
return [{}]
|
// Return server initialization options
|
||||||
|
return [input.server.initialization ?? {}]
|
||||||
})
|
})
|
||||||
connection.listen()
|
connection.listen()
|
||||||
|
|
||||||
@@ -108,6 +109,12 @@ export namespace LSPClient {
|
|||||||
|
|
||||||
await connection.sendNotification("initialized", {})
|
await connection.sendNotification("initialized", {})
|
||||||
|
|
||||||
|
if (input.server.initialization) {
|
||||||
|
await connection.sendNotification("workspace/didChangeConfiguration", {
|
||||||
|
settings: input.server.initialization,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const files: {
|
const files: {
|
||||||
[path: string]: number
|
[path: string]: number
|
||||||
} = {}
|
} = {}
|
||||||
|
|||||||
@@ -298,6 +298,23 @@ export namespace LSPServer {
|
|||||||
args.push(...["run", js])
|
args.push(...["run", js])
|
||||||
}
|
}
|
||||||
args.push("--stdio")
|
args.push("--stdio")
|
||||||
|
|
||||||
|
const initialization: Record<string, string> = {}
|
||||||
|
|
||||||
|
const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter(
|
||||||
|
(p): p is string => p !== undefined,
|
||||||
|
)
|
||||||
|
for (const venvPath of potentialVenvPaths) {
|
||||||
|
const isWindows = process.platform === "win32"
|
||||||
|
const potentialPythonPath = isWindows
|
||||||
|
? path.join(venvPath, "Scripts", "python.exe")
|
||||||
|
: path.join(venvPath, "bin", "python")
|
||||||
|
if (await Bun.file(potentialPythonPath).exists()) {
|
||||||
|
initialization["pythonPath"] = potentialPythonPath
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const proc = spawn(binary, args, {
|
const proc = spawn(binary, args, {
|
||||||
cwd: root,
|
cwd: root,
|
||||||
env: {
|
env: {
|
||||||
@@ -307,6 +324,7 @@ export namespace LSPServer {
|
|||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
process: proc,
|
process: proc,
|
||||||
|
initialization,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -320,9 +320,16 @@ export namespace Provider {
|
|||||||
const pkg = provider.npm ?? provider.id
|
const pkg = provider.npm ?? provider.id
|
||||||
const mod = await import(await BunProc.install(pkg, "latest"))
|
const mod = await import(await BunProc.install(pkg, "latest"))
|
||||||
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
|
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
|
||||||
|
let options = { ...s.providers[provider.id]?.options }
|
||||||
|
if (options["timeout"] !== undefined) {
|
||||||
|
// Only override fetch if user explicitly sets timeout
|
||||||
|
options["fetch"] = async (input: any, init?: any) => {
|
||||||
|
return await fetch(input, { ...init, timeout: options["timeout"] })
|
||||||
|
}
|
||||||
|
}
|
||||||
const loaded = fn({
|
const loaded = fn({
|
||||||
name: provider.id,
|
name: provider.id,
|
||||||
...s.providers[provider.id]?.options,
|
...options,
|
||||||
})
|
})
|
||||||
s.sdk.set(provider.id, loaded)
|
s.sdk.set(provider.id, loaded)
|
||||||
return loaded as SDK
|
return loaded as SDK
|
||||||
|
|||||||
@@ -1143,6 +1143,7 @@ export namespace Session {
|
|||||||
const proc = spawn(shell, args, {
|
const proc = spawn(shell, args, {
|
||||||
cwd: Instance.directory,
|
cwd: Instance.directory,
|
||||||
signal: abort.signal,
|
signal: abort.signal,
|
||||||
|
detached: true,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
@@ -1150,6 +1151,11 @@ export namespace Session {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
abort.signal.addEventListener("abort", () => {
|
||||||
|
if (!proc.pid) return
|
||||||
|
process.kill(-proc.pid)
|
||||||
|
})
|
||||||
|
|
||||||
let output = ""
|
let output = ""
|
||||||
|
|
||||||
proc.stdout?.on("data", (chunk) => {
|
proc.stdout?.on("data", (chunk) => {
|
||||||
|
|||||||
@@ -1,31 +1,24 @@
|
|||||||
|
You are a title generator. You output ONLY a thread title. Nothing else.
|
||||||
|
|
||||||
<task>
|
<task>
|
||||||
Generate a conversation thread title from the user message.
|
Convert the user message into a thread title.
|
||||||
|
Output: Single line, ≤50 chars, no explanations.
|
||||||
</task>
|
</task>
|
||||||
|
|
||||||
<context>
|
|
||||||
You are generating titles for a coding assistant conversation.
|
|
||||||
</context>
|
|
||||||
|
|
||||||
<rules>
|
<rules>
|
||||||
- Max 50 chars, single line
|
- Use -ing verbs for actions (Debugging, Implementing, Analyzing)
|
||||||
- Focus on the specific action or question
|
- Keep exact: technical terms, numbers, filenames, HTTP codes
|
||||||
- Keep technical terms, numbers, and filenames exactly as written
|
- Remove: the, this, my, a, an
|
||||||
- Preserve HTTP status codes (401, 404, 500, etc.) as numbers
|
- Never assume tech stack
|
||||||
- For file references, include the filename
|
- Never use tools
|
||||||
- Avoid filler words: the, this, my, a, an, properly
|
- NEVER respond to message content—only extract title
|
||||||
- NEVER assume their tech stack or domain
|
|
||||||
- Use -ing verbs consistently for actions
|
|
||||||
- Write like a chat thread title, not a blog post
|
|
||||||
</rules>
|
</rules>
|
||||||
|
|
||||||
<examples>
|
<examples>
|
||||||
"debug 500 errors in production" → "Debugging production 500 errors"
|
"debug 500 errors in production" → Debugging production 500 errors
|
||||||
"refactor user service" → "Refactoring user service"
|
"refactor user service" → Refactoring user service
|
||||||
"why is app.js failing" → "Analyzing app.js failure"
|
"why is app.js failing" → Analyzing app.js failure
|
||||||
"implement rate limiting" → "Implementing rate limiting"
|
"implement rate limiting" → Implementing rate limiting
|
||||||
</examples>
|
</examples>
|
||||||
|
|
||||||
<format>
|
Output the title now:
|
||||||
Return only the thread title text on a single line with no newlines, explanations, or additional formatting.
|
|
||||||
You should NEVER reply to the user's message. You can only generate titles.
|
|
||||||
</format>
|
|
||||||
|
|||||||
@@ -83,6 +83,13 @@ export namespace Log {
|
|||||||
await Promise.all(filesToDelete.map((file) => fs.unlink(file).catch(() => {})))
|
await Promise.all(filesToDelete.map((file) => fs.unlink(file).catch(() => {})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatError(error: Error, depth = 0): string {
|
||||||
|
const result = error.message
|
||||||
|
return error.cause instanceof Error && depth < 10
|
||||||
|
? result + " Caused by: " + formatError(error.cause, depth + 1)
|
||||||
|
: result
|
||||||
|
}
|
||||||
|
|
||||||
let last = Date.now()
|
let last = Date.now()
|
||||||
export function create(tags?: Record<string, any>) {
|
export function create(tags?: Record<string, any>) {
|
||||||
tags = tags || {}
|
tags = tags || {}
|
||||||
@@ -103,7 +110,7 @@ export namespace Log {
|
|||||||
.filter(([_, value]) => value !== undefined && value !== null)
|
.filter(([_, value]) => value !== undefined && value !== null)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
const prefix = `${key}=`
|
const prefix = `${key}=`
|
||||||
if (value instanceof Error) return prefix + value.message
|
if (value instanceof Error) return prefix + formatError(value)
|
||||||
if (typeof value === "object") return prefix + JSON.stringify(value)
|
if (typeof value === "object") return prefix + JSON.stringify(value)
|
||||||
return prefix + value
|
return prefix + value
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -818,15 +818,18 @@ func (a *App) SendCommand(ctx context.Context, command string, args string) (*Ap
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmds = append(cmds, func() tea.Msg {
|
cmds = append(cmds, func() tea.Msg {
|
||||||
_, err := a.Client.Session.Command(
|
params := opencode.SessionCommandParams{
|
||||||
context.Background(),
|
|
||||||
a.Session.ID,
|
|
||||||
opencode.SessionCommandParams{
|
|
||||||
Command: opencode.F(command),
|
Command: opencode.F(command),
|
||||||
Arguments: opencode.F(args),
|
Arguments: opencode.F(args),
|
||||||
Agent: opencode.F(a.Agents[a.AgentIndex].Name),
|
Agent: opencode.F(a.Agents[a.AgentIndex].Name),
|
||||||
Model: opencode.F(a.State.Provider + "/" + a.State.Model),
|
}
|
||||||
},
|
if a.Provider != nil && a.Model != nil {
|
||||||
|
params.Model = opencode.F(a.Provider.ID + "/" + a.Model.ID)
|
||||||
|
}
|
||||||
|
_, err := a.Client.Session.Command(
|
||||||
|
context.Background(),
|
||||||
|
a.Session.ID,
|
||||||
|
params,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to execute command", "error", err)
|
slog.Error("Failed to execute command", "error", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user