Merge branch 'dev' into project

This commit is contained in:
Dax Raad 2025-08-31 20:32:44 -04:00
commit 9ec65c5e4b
13 changed files with 553 additions and 599 deletions

View file

@ -64,3 +64,4 @@
| 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-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) |

View file

@ -54,7 +54,10 @@
a,
button {
appearance: none;
background: none;
border: none;
cursor: pointer;
padding: 0;
color: var(--color-text);
text-decoration: underline;

View file

@ -1,7 +1,20 @@
import "./workspace.css"
import { useAuthSession } from "~/context/auth.session"
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 () => {
"use server"
@ -17,6 +30,7 @@ const logout = action(async () => {
})
export default function WorkspaceLayout(props: RouteSectionProps) {
const userInfo = createAsync(() => getUserInfo())
return (
<main data-page="workspace">
<header data-component="workspace-header">
@ -26,7 +40,9 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
</a>
</div>
<div data-slot="header-actions">
<span>name@example.com</span>
{userInfo() &&
<span>{userInfo()!.user.email}</span>
}
<form action={logout} method="post">
<button type="submit" formaction={logout}>Logout</button>
</form>

View file

@ -23,453 +23,450 @@
padding-bottom: var(--space-16);
}
}
}
/* Common elements */
button {
padding: var(--space-3) var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
background-color: var(--color-bg);
color: var(--color-text);
font-size: var(--font-size-sm);
font-family: var(--font-sans);
font-weight: 500;
text-transform: uppercase;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background-color: var(--color-surface-hover);
border-color: var(--color-accent);
}
&:active {
transform: translateY(1px);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
&:hover {
background-color: var(--color-bg);
border-color: var(--color-border);
transform: none;
}
}
&[color="primary"] {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: var(--color-primary-text);
&:hover {
background-color: var(--color-primary-hover);
border-color: var(--color-primary-hover);
}
}
&[color="ghost"] {
background-color: transparent;
border-color: transparent;
color: var(--color-text-muted);
/* Common elements */
button {
padding: var(--space-3) var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
background-color: var(--color-bg);
color: var(--color-text);
font-size: var(--font-size-sm);
font-family: var(--font-sans);
font-weight: 500;
text-transform: uppercase;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background-color: var(--color-surface-hover);
border-color: var(--color-border);
color: var(--color-text);
border-color: var(--color-accent);
}
}
}
a {
color: var(--color-text);
text-decoration: underline;
text-underline-offset: var(--space-0-75);
text-decoration-thickness: 1px;
}
&:active {
transform: translateY(1px);
}
[data-slot="empty-state"] {
padding: var(--space-20) var(--space-6);
text-align: center;
border: 1px dashed var(--color-border);
border-radius: var(--border-radius-sm);
display: flex;
flex-direction: column;
gap: var(--space-2);
&:disabled {
opacity: 0.5;
cursor: not-allowed;
p {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
margin: 0;
}
}
&:hover {
background-color: var(--color-bg);
border-color: var(--color-border);
transform: none;
}
}
/* Title section */
[data-slot="title-section"] {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding-bottom: var(--space-8);
border-bottom: 1px solid var(--color-border);
&[data-color="primary"] {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: var(--color-primary-text);
h1 {
font-size: var(--font-size-2xl);
font-weight: 500;
line-height: 1.2;
letter-spacing: -0.03125rem;
margin: 0;
text-transform: uppercase;
&:hover {
background-color: var(--color-primary-hover);
border-color: var(--color-primary-hover);
}
}
@media (max-width: 30rem) {
font-size: var(--font-size-xl);
line-height: 1.25;
&[data-color="ghost"] {
background-color: transparent;
border-color: transparent;
color: var(--color-text-muted);
&:hover {
background-color: var(--color-surface-hover);
border-color: var(--color-border);
color: var(--color-text);
}
}
}
p {
font-size: var(--font-size-md);
color: var(--color-text-muted);
a {
color: var(--color-text);
text-decoration: underline;
text-underline-offset: var(--space-0-75);
text-decoration-thickness: 1px;
}
a {
[data-slot="empty-state"] {
padding: var(--space-20) var(--space-6);
text-align: center;
border: 1px dashed var(--color-border);
border-radius: var(--border-radius-sm);
display: flex;
flex-direction: column;
gap: var(--space-2);
p {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
margin: 0;
}
}
/* Title section */
[data-slot="title-section"] {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding-bottom: var(--space-8);
border-bottom: 1px solid var(--color-border);
h1 {
font-size: var(--font-size-2xl);
font-weight: 500;
line-height: 1.2;
letter-spacing: -0.03125rem;
margin: 0;
text-transform: uppercase;
@media (max-width: 30rem) {
font-size: var(--font-size-xl);
line-height: 1.25;
}
}
p {
font-size: var(--font-size-md);
color: var(--color-text-muted);
a {
color: var(--color-text-muted);
}
}
}
/* Section titles */
[data-slot="section-title"] {
display: flex;
flex-direction: column;
gap: var(--space-1);
h2 {
font-size: var(--font-size-md);
font-weight: 600;
line-height: 1.2;
letter-spacing: -0.03125rem;
margin: 0;
color: var(--color-text-secondary);
text-transform: uppercase;
@media (max-width: 30rem) {
font-size: var(--font-size-lg);
line-height: 1.25;
}
}
p {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
}
}
/* Section titles */
[data-slot="section-title"] {
display: flex;
flex-direction: column;
gap: var(--space-1);
h2 {
font-size: var(--font-size-md);
font-weight: 600;
line-height: 1.2;
letter-spacing: -0.03125rem;
margin: 0;
color: var(--color-text-secondary);
text-transform: uppercase;
@media (max-width: 30rem) {
font-size: var(--font-size-lg);
line-height: 1.25;
}
}
p {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
}
/* API Keys Section */
[data-slot="api-keys-section"] {
[data-slot="create-form"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-4);
background-color: var(--color-bg-surface);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
max-width: 32rem;
input {
padding: var(--space-2) var(--space-3);
/* API Keys Section */
[data-slot="api-keys-section"] {
[data-slot="create-form"] {
display: flex;
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
background-color: var(--color-bg);
color: var(--color-text);
input {
flex: 1;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
background-color: var(--color-bg);
color: var(--color-text);
font-size: var(--font-size-sm);
font-family: var(--font-mono);
&:focus {
outline: none;
border-color: var(--color-accent);
}
&::placeholder {
color: var(--color-text-disabled);
}
}
[data-slot="form-actions"] {
display: flex;
gap: var(--space-2);
}
}
[data-slot="api-keys-table"] {
overflow-x: auto;
}
[data-slot="api-keys-table-element"] {
width: 100%;
border-collapse: collapse;
font-size: var(--font-size-sm);
font-family: var(--font-mono);
&:focus {
outline: none;
border-color: var(--color-accent);
}
&::placeholder {
color: var(--color-text-disabled);
}
}
[data-slot="form-actions"] {
display: flex;
gap: var(--space-2);
justify-content: flex-end;
}
}
[data-slot="api-keys-table"] {
overflow-x: auto;
}
[data-slot="api-keys-table-element"] {
width: 100%;
border-collapse: collapse;
font-size: var(--font-size-sm);
thead {
border-bottom: 1px solid var(--color-border);
}
th {
padding: var(--space-3) var(--space-4);
text-align: left;
font-weight: normal;
color: var(--color-text-muted);
text-transform: uppercase;
}
td {
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
color: var(--color-text-muted);
font-family: var(--font-mono);
&[data-slot="key-name"] {
color: var(--color-text);
font-family: var(--font-sans);
font-weight: 500;
}
&[data-slot="key-value"] {
font-family: var(--font-mono);
div {
cursor: pointer;
display: flex;
align-items: center;
gap: var(--space-2);
}
}
&[data-slot="key-date"] {
color: var(--color-text);
}
&[data-slot="key-actions"] {
font-family: var(--font-sans);
}
}
tbody tr {
&:last-child td {
border-bottom: none;
}
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
thead {
border-bottom: 1px solid var(--color-border);
}
th {
&:nth-child(3) /* Date */ {
display: none;
}
padding: var(--space-3) var(--space-4);
text-align: left;
font-weight: normal;
color: var(--color-text-muted);
text-transform: uppercase;
}
td {
&:nth-child(3) /* Date */ {
display: none;
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
color: var(--color-text-muted);
font-family: var(--font-mono);
&[data-slot="key-name"] {
color: var(--color-text);
font-family: var(--font-sans);
font-weight: 500;
}
&[data-slot="key-value"] {
font-family: var(--font-mono);
div {
cursor: pointer;
display: flex;
align-items: center;
gap: var(--space-2);
}
}
&[data-slot="key-date"] {
color: var(--color-text);
}
&[data-slot="key-actions"] {
font-family: var(--font-sans);
}
}
tbody tr {
&:last-child td {
border-bottom: none;
}
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
}
th {
&:nth-child(3) /* Date */ {
display: none;
}
}
td {
&:nth-child(3) /* Date */ {
display: none;
}
}
}
}
}
}
/* Balance Section */
[data-slot="balance-section"] {
[data-slot="balance"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
min-width: 14.5rem;
width: fit-content;
[data-slot="amount"] {
padding: var(--space-3-5) var(--space-4);
background-color: var(--color-bg-surface);
/* Balance Section */
[data-slot="balance-section"] {
[data-slot="balance"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
display: flex;
align-items: baseline;
gap: var(--space-1);
justify-content: flex-end;
min-width: 14.5rem;
width: fit-content;
[data-slot="amount"] {
padding: var(--space-3-5) var(--space-4);
background-color: var(--color-bg-surface);
border-radius: var(--border-radius-sm);
display: flex;
align-items: baseline;
gap: var(--space-1);
justify-content: flex-end;
&.danger {
[data-slot="value"] {
color: var(--color-danger);
}
}
[data-slot="currency"] {
position: relative;
bottom: 2px;
font-size: var(--font-size-lg);
color: var(--color-text-muted);
font-weight: 400;
}
&.danger {
[data-slot="value"] {
color: var(--color-danger);
font-size: var(--font-size-3xl);
font-weight: 500;
color: var(--color-text);
}
}
}
}
[data-slot="currency"] {
position: relative;
bottom: 2px;
font-size: var(--font-size-lg);
/* Payments Section */
[data-slot="payments-section"] {
[data-slot="payments-table"] {
overflow-x: auto;
}
[data-slot="payments-table-element"] {
width: 100%;
border-collapse: collapse;
font-size: var(--font-size-sm);
thead {
border-bottom: 1px solid var(--color-border);
}
th {
padding: var(--space-3) var(--space-4);
text-align: left;
font-weight: normal;
color: var(--color-text-muted);
font-weight: 400;
text-transform: uppercase;
}
[data-slot="value"] {
font-size: var(--font-size-3xl);
font-weight: 500;
color: var(--color-text);
}
}
}
}
/* Payments Section */
[data-slot="payments-section"] {
[data-slot="payments-table"] {
overflow-x: auto;
}
[data-slot="payments-table-element"] {
width: 100%;
border-collapse: collapse;
font-size: var(--font-size-sm);
thead {
border-bottom: 1px solid var(--color-border);
}
th {
padding: var(--space-3) var(--space-4);
text-align: left;
font-weight: normal;
color: var(--color-text-muted);
text-transform: uppercase;
}
td {
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
color: var(--color-text-muted);
font-family: var(--font-mono);
&[data-slot="payment-date"] {
color: var(--color-text);
}
&[data-slot="payment-id"] {
td {
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
color: var(--color-text-muted);
font-family: var(--font-mono);
font-weight: 400;
&[data-slot="payment-date"] {
color: var(--color-text);
}
&[data-slot="payment-id"] {
font-family: var(--font-mono);
font-weight: 400;
color: var(--color-text-muted);
max-width: 200px;
word-break: break-word;
}
&[data-slot="payment-amount"] {
color: var(--color-text);
}
}
tbody tr {
&:last-child td {
border-bottom: none;
}
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
}
th {
&:nth-child(2) /* Payment ID */ {
display: none;
}
}
td {
&:nth-child(2) /* Payment ID */ {
display: none;
}
}
}
}
}
/* Usage Section */
[data-slot="usage-section"] {
[data-slot="usage-table"] {
overflow-x: auto;
}
[data-slot="usage-table-element"] {
width: 100%;
border-collapse: collapse;
font-size: var(--font-size-sm);
thead {
border-bottom: 1px solid var(--color-border);
}
th {
padding: var(--space-3) var(--space-4);
text-align: left;
font-weight: normal;
color: var(--color-text-muted);
max-width: 200px;
word-break: break-word;
text-transform: uppercase;
}
&[data-slot="payment-amount"] {
color: var(--color-text);
}
}
tbody tr {
&:last-child td {
border-bottom: none;
}
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
}
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
color: var(--color-text-muted);
font-family: var(--font-mono);
th {
&:nth-child(2) /* Payment ID */ {
display: none;
&[data-slot="usage-date"] {
color: var(--color-text);
}
&[data-slot="usage-model"] {
font-family: var(--font-sans);
font-weight: 400;
color: var(--color-text-secondary);
max-width: 200px;
word-break: break-word;
}
&[data-slot="usage-cost"] {
color: var(--color-text);
}
}
td {
&:nth-child(2) /* Payment ID */ {
display: none;
}
}
}
}
}
/* Usage Section */
[data-slot="usage-section"] {
[data-slot="usage-table"] {
overflow-x: auto;
}
[data-slot="usage-table-element"] {
width: 100%;
border-collapse: collapse;
font-size: var(--font-size-sm);
thead {
border-bottom: 1px solid var(--color-border);
}
th {
padding: var(--space-3) var(--space-4);
text-align: left;
font-weight: normal;
color: var(--color-text-muted);
text-transform: uppercase;
}
td {
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
color: var(--color-text-muted);
font-family: var(--font-mono);
&[data-slot="usage-date"] {
color: var(--color-text);
}
&[data-slot="usage-model"] {
font-family: var(--font-sans);
font-weight: 400;
color: var(--color-text-secondary);
max-width: 200px;
word-break: break-word;
}
&[data-slot="usage-cost"] {
color: var(--color-text);
}
}
tbody tr {
&:last-child td {
border-bottom: none;
}
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
}
th {
&:nth-child(2) /* Model */ {
display: none;
}
}
td {
&:nth-child(2) /* Model */ {
display: none;
tbody tr {
&:last-child td {
border-bottom: none;
}
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
}
th {
&:nth-child(2) /* Model */ {
display: none;
}
}
td {
&:nth-child(2) /* Model */ {
display: none;
}
}
}
}

View file

@ -63,116 +63,8 @@ const createPortalUrl = action(async (returnUrl: string) => {
return withActor(() => Billing.generatePortalUrl({ returnUrl }))
}, "portalUrl")
const dummyUsageData = [
{
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 () {
export default function() {
const actor = createAsync(() => getActor())
onMount(() => {
console.log("MOUNTED", actor())
})
/////////////////
// Keys section
@ -292,15 +184,6 @@ export default function () {
</section>
<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 */}
<section data-slot="api-keys-section">
<div data-slot="section-title">
@ -321,14 +204,7 @@ export default function () {
/>
<div data-slot="form-actions">
<button
color="primary"
disabled={createKeySubmission.pending || !keyName().trim()}
onClick={handleCreateKey}
>
{createKeySubmission.pending ? "Creating..." : "Create"}
</button>
<button
color="ghost"
data-color="ghost"
onClick={() => {
setShowCreateForm(false)
setKeyName("")
@ -336,12 +212,19 @@ export default function () {
>
Cancel
</button>
<button
data-color="primary"
disabled={createKeySubmission.pending || !keyName().trim()}
onClick={handleCreateKey}
>
{createKeySubmission.pending ? "Creating..." : "Create"}
</button>
</div>
</div>
}
>
<button
color="primary"
data-color="primary"
onClick={() => {
console.log("clicked")
setShowCreateForm(true)
@ -370,7 +253,6 @@ export default function () {
</thead>
<tbody>
<For each={keys()!}>
{/* Real data: keys() */}
{(key) => (
<tr>
<td data-slot="key-name">{key.name}</td>
@ -389,7 +271,7 @@ export default function () {
{formatDateForTable(key.timeCreated)}
</td>
<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
</button>
</td>
@ -426,51 +308,12 @@ export default function () {
})()}
</span>
</div>
<button color="primary" disabled={createCheckoutUrlSubmission.pending} onClick={handleBuyCredits}>
<button data-color="primary" disabled={createCheckoutUrlSubmission.pending} onClick={handleBuyCredits}>
{createCheckoutUrlSubmission.pending ? "Loading..." : "Buy Credits"}
</button>
</div>
</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 */}
<section data-slot="usage-section">
<div data-slot="section-title">
@ -479,7 +322,7 @@ export default function () {
</div>
<div data-slot="usage-table">
<Show
when={dummyUsageData.length > 0}
when={billingInfo() && billingInfo()!.usage.length > 0}
fallback={
<div data-slot="empty-state">
<p>Make your first API call to get started.</p>
@ -496,7 +339,7 @@ export default function () {
</tr>
</thead>
<tbody>
<For each={dummyUsageData}>
<For each={billingInfo()!.usage}>
{(usage) => {
const totalTokens = usage.inputTokens + usage.outputTokens + (usage.reasoningTokens || 0)
const date = new Date(usage.timeCreated)
@ -517,6 +360,44 @@ export default function () {
</Show>
</div>
</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>
)

View file

@ -397,6 +397,21 @@ export namespace Config {
.object({
apiKey: 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())
.optional(),

View file

@ -59,7 +59,8 @@ export namespace LSPClient {
return null
})
connection.onRequest("workspace/configuration", async () => {
return [{}]
// Return server initialization options
return [input.server.initialization ?? {}]
})
connection.listen()
@ -108,6 +109,12 @@ export namespace LSPClient {
await connection.sendNotification("initialized", {})
if (input.server.initialization) {
await connection.sendNotification("workspace/didChangeConfiguration", {
settings: input.server.initialization,
})
}
const files: {
[path: string]: number
} = {}

View file

@ -298,6 +298,23 @@ export namespace LSPServer {
args.push(...["run", js])
}
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, {
cwd: root,
env: {
@ -307,6 +324,7 @@ export namespace LSPServer {
})
return {
process: proc,
initialization,
}
},
}

View file

@ -320,9 +320,16 @@ export namespace Provider {
const pkg = provider.npm ?? provider.id
const mod = await import(await BunProc.install(pkg, "latest"))
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({
name: provider.id,
...s.providers[provider.id]?.options,
...options,
})
s.sdk.set(provider.id, loaded)
return loaded as SDK

View file

@ -1143,6 +1143,7 @@ export namespace Session {
const proc = spawn(shell, args, {
cwd: Instance.directory,
signal: abort.signal,
detached: true,
stdio: ["ignore", "pipe", "pipe"],
env: {
...process.env,
@ -1150,6 +1151,11 @@ export namespace Session {
},
})
abort.signal.addEventListener("abort", () => {
if (!proc.pid) return
process.kill(-proc.pid)
})
let output = ""
proc.stdout?.on("data", (chunk) => {

View file

@ -1,31 +1,24 @@
You are a title generator. You output ONLY a thread title. Nothing else.
<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>
<context>
You are generating titles for a coding assistant conversation.
</context>
<rules>
- Max 50 chars, single line
- Focus on the specific action or question
- Keep technical terms, numbers, and filenames exactly as written
- Preserve HTTP status codes (401, 404, 500, etc.) as numbers
- For file references, include the filename
- Avoid filler words: the, this, my, a, an, properly
- NEVER assume their tech stack or domain
- Use -ing verbs consistently for actions
- Write like a chat thread title, not a blog post
- Use -ing verbs for actions (Debugging, Implementing, Analyzing)
- Keep exact: technical terms, numbers, filenames, HTTP codes
- Remove: the, this, my, a, an
- Never assume tech stack
- Never use tools
- NEVER respond to message content—only extract title
</rules>
<examples>
"debug 500 errors in production" → "Debugging production 500 errors"
"refactor user service" → "Refactoring user service"
"why is app.js failing" → "Analyzing app.js failure"
"implement rate limiting" → "Implementing rate limiting"
"debug 500 errors in production" → Debugging production 500 errors
"refactor user service" → Refactoring user service
"why is app.js failing" → Analyzing app.js failure
"implement rate limiting" → Implementing rate limiting
</examples>
<format>
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>
Output the title now:

View file

@ -83,6 +83,13 @@ export namespace Log {
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()
export function create(tags?: Record<string, any>) {
tags = tags || {}
@ -103,7 +110,7 @@ export namespace Log {
.filter(([_, value]) => value !== undefined && value !== null)
.map(([key, value]) => {
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)
return prefix + value
})

View file

@ -818,15 +818,18 @@ func (a *App) SendCommand(ctx context.Context, command string, args string) (*Ap
}
cmds = append(cmds, func() tea.Msg {
params := opencode.SessionCommandParams{
Command: opencode.F(command),
Arguments: opencode.F(args),
Agent: opencode.F(a.Agents[a.AgentIndex].Name),
}
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,
opencode.SessionCommandParams{
Command: opencode.F(command),
Arguments: opencode.F(args),
Agent: opencode.F(a.Agents[a.AgentIndex].Name),
Model: opencode.F(a.State.Provider + "/" + a.State.Model),
},
params,
)
if err != nil {
slog.Error("Failed to execute command", "error", err)