diff --git a/STATS.md b/STATS.md index 62990eb5d6..6b6ad8684c 100644 --- a/STATS.md +++ b/STATS.md @@ -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) | diff --git a/cloud/app/src/routes/workspace.css b/cloud/app/src/routes/workspace.css index e18b410ee1..e378ef461c 100644 --- a/cloud/app/src/routes/workspace.css +++ b/cloud/app/src/routes/workspace.css @@ -54,7 +54,10 @@ a, button { + appearance: none; + background: none; border: none; + cursor: pointer; padding: 0; color: var(--color-text); text-decoration: underline; diff --git a/cloud/app/src/routes/workspace.tsx b/cloud/app/src/routes/workspace.tsx index 6876ae962f..f75af8e74d 100644 --- a/cloud/app/src/routes/workspace.tsx +++ b/cloud/app/src/routes/workspace.tsx @@ -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 (
@@ -26,7 +40,9 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
- name@example.com + {userInfo() && + {userInfo()!.user.email} + }
diff --git a/cloud/app/src/routes/workspace/[id].css b/cloud/app/src/routes/workspace/[id].css index 887469e337..651cae627b 100644 --- a/cloud/app/src/routes/workspace/[id].css +++ b/cloud/app/src/routes/workspace/[id].css @@ -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; + } } } } diff --git a/cloud/app/src/routes/workspace/[id].tsx b/cloud/app/src/routes/workspace/[id].tsx index ccdfe42f52..a03811c178 100644 --- a/cloud/app/src/routes/workspace/[id].tsx +++ b/cloud/app/src/routes/workspace/[id].tsx @@ -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 () {
- {/* Actor Section */} -
-
-

Actor

-

Current authenticated user information and session details.

-
-
{JSON.stringify(actor())}
-
- {/* API Keys Section */}
@@ -321,14 +204,7 @@ export default function () { />
- +
} > @@ -426,51 +308,12 @@ export default function () { })()}
-
- {/* Payments Section */} - 0}> - {/* Real data condition: billingInfo() && billingInfo()!.payments.length > 0 */} -
-
-

Payments History

-

Recent payment transactions.

-
-
- - - - - - - - - - - {/* Real data: billingInfo()?.payments */} - {(payment) => { - const date = new Date(payment.timeCreated) - return ( - - - - - - ) - }} - - -
DatePayment IDAmount
- {formatDateForTable(date)} - {payment.id}${((payment.amount ?? 0) / 100000000).toFixed(2)}
-
-
-
- {/* Usage Section */}
@@ -479,7 +322,7 @@ export default function () {
0} + when={billingInfo() && billingInfo()!.usage.length > 0} fallback={

Make your first API call to get started.

@@ -496,7 +339,7 @@ export default function () { - + {(usage) => { const totalTokens = usage.inputTokens + usage.outputTokens + (usage.reasoningTokens || 0) const date = new Date(usage.timeCreated) @@ -517,6 +360,44 @@ export default function () {
+ + {/* Payments Section */} + 0}> +
+
+

Payments History

+

Recent payment transactions.

+
+
+ + + + + + + + + + + {(payment) => { + const date = new Date(payment.timeCreated) + return ( + + + + + + ) + }} + + +
DatePayment IDAmount
+ {formatDateForTable(date)} + {payment.id}${((payment.amount ?? 0) / 100000000).toFixed(2)}
+
+
+
+ ) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index f62e46479d..135c0e80c3 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -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(), diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index 52658e7522..20c11c9f1b 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -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 } = {} diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 9dc7f1ccaa..690e262df6 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -298,6 +298,23 @@ export namespace LSPServer { args.push(...["run", js]) } args.push("--stdio") + + const initialization: Record = {} + + 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, } }, } diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 099c841c8a..3d6ea16e5b 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -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 diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 0894adbc6f..654607e744 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -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) => { diff --git a/packages/opencode/src/session/prompt/title.txt b/packages/opencode/src/session/prompt/title.txt index 6de65d2b7a..997a960bc6 100644 --- a/packages/opencode/src/session/prompt/title.txt +++ b/packages/opencode/src/session/prompt/title.txt @@ -1,31 +1,24 @@ +You are a title generator. You output ONLY a thread title. Nothing else. + -Generate a conversation thread title from the user message. +Convert the user message into a thread title. +Output: Single line, ≤50 chars, no explanations. - -You are generating titles for a coding assistant conversation. - - -- 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 -"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 - -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. - +Output the title now: diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index 1b321523a1..b4cdd920ac 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -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) { 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 }) diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go index 7defbc2947..0733719c91 100644 --- a/packages/tui/internal/app/app.go +++ b/packages/tui/internal/app/app.go @@ -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)