summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJay V <[email protected]>2025-09-10 17:59:03 -1000
committerJay V <[email protected]>2025-09-10 17:59:03 -1000
commitc2fa28c1be43b5932707e6cc068e28cc6e8a92e8 (patch)
tree61ad528e56e246bbadcaf252712749ad8e12acaf
parent30aae66320b4f132c6240e5dffef3b439f1b0649 (diff)
downloadopencode-c2fa28c1be43b5932707e6cc068e28cc6e8a92e8.tar.gz
opencode-c2fa28c1be43b5932707e6cc068e28cc6e8a92e8.zip
ignore: zen
-rw-r--r--cloud/app/src/routes/workspace.css14
-rw-r--r--cloud/app/src/routes/workspace/[id].css219
-rw-r--r--cloud/app/src/routes/workspace/[id].tsx117
-rw-r--r--packages/web/src/content/docs/index.mdx2
-rw-r--r--packages/web/src/content/docs/providers.mdx6
-rw-r--r--packages/web/src/content/docs/zen.mdx2
6 files changed, 327 insertions, 33 deletions
diff --git a/cloud/app/src/routes/workspace.css b/cloud/app/src/routes/workspace.css
index 2658ad7e5..ed94365f0 100644
--- a/cloud/app/src/routes/workspace.css
+++ b/cloud/app/src/routes/workspace.css
@@ -15,7 +15,7 @@
cursor: pointer;
transition: all 0.15s ease;
- &:hover {
+ &:hover:not(:disabled) {
background-color: var(--color-surface-hover);
border-color: var(--color-accent);
}
@@ -26,13 +26,7 @@
&:disabled {
opacity: 0.5;
- cursor: not-allowed;
-
- &:hover {
- background-color: var(--color-bg);
- border-color: var(--color-border);
- transform: none;
- }
+ transform: none;
}
&[data-color="primary"] {
@@ -40,7 +34,7 @@
border-color: var(--color-primary);
color: var(--color-primary-text);
- &:hover {
+ &:hover:not(:disabled) {
background-color: var(--color-primary-hover);
border-color: var(--color-primary-hover);
}
@@ -51,7 +45,7 @@
border-color: transparent;
color: var(--color-text-muted);
- &:hover {
+ &:hover:not(:disabled) {
background-color: var(--color-surface-hover);
border-color: var(--color-border);
color: var(--color-text);
diff --git a/cloud/app/src/routes/workspace/[id].css b/cloud/app/src/routes/workspace/[id].css
index 397ceec54..2d9b0e638 100644
--- a/cloud/app/src/routes/workspace/[id].css
+++ b/cloud/app/src/routes/workspace/[id].css
@@ -47,12 +47,6 @@
font-size: var(--font-size-md);
}
}
-
- p {
- line-height: 1.4;
- font-size: var(--font-size-sm);
- color: var(--color-text-muted);
- }
}
}
section:not(:last-child) {
@@ -192,11 +186,35 @@
&[data-slot="key-value"] {
font-family: var(--font-mono);
- div {
- cursor: pointer;
+ button {
display: flex;
align-items: center;
gap: var(--space-2);
+ padding: var(--space-2) var(--space-3);
+ font-size: var(--font-size-sm);
+ font-weight: 400;
+ border: none;
+ background-color: transparent;
+ color: var(--color-text-muted);
+ font-family: var(--font-mono);
+ border-radius: var(--border-radius-sm);
+ cursor: pointer;
+ transition: all 0.15s ease;
+ text-transform: none;
+
+ &:hover:not(:disabled) {
+ background-color: var(--color-bg-surface);
+ color: var(--color-text);
+ }
+
+ &:disabled {
+ cursor: default;
+ color: var(--color-text);
+ }
+
+ span {
+ font-family: inherit;
+ }
}
}
@@ -262,6 +280,9 @@
[data-slot="value"] {
color: var(--color-danger);
}
+ [data-slot="currency"] {
+ color: var(--color-danger);
+ }
}
[data-slot="currency"] {
@@ -428,4 +449,186 @@
}
}
}
+
+ [data-slot="new-user-sections"] {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-16);
+
+ @media (max-width: 30rem) {
+ gap: var(--space-8);
+ }
+
+ [data-component="feature-grid"] {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: var(--space-6);
+
+ @media (max-width: 30rem) {
+ grid-template-columns: 1fr;
+ gap: var(--space-4);
+ }
+
+ [data-slot="feature"] {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
+ padding: var(--space-4);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-sm);
+ background-color: var(--color-bg-surface);
+
+ h3 {
+ font-size: var(--font-size-sm);
+ font-weight: 600;
+ margin: 0;
+ color: var(--color-text);
+ text-transform: uppercase;
+ letter-spacing: -0.025rem;
+ }
+
+ p {
+ font-size: var(--font-size-sm);
+ line-height: 1.5;
+ margin: 0;
+ color: var(--color-text-muted);
+ }
+ }
+ }
+
+ [data-component="api-key-highlight"] {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-6);
+
+ [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-md);
+ }
+ }
+ }
+
+ [data-slot="key-display"] {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-3);
+
+ [data-slot="key-container"] {
+ display: flex;
+ gap: var(--space-3);
+ padding: var(--space-4);
+ border: 2px solid var(--color-accent);
+ border-radius: var(--border-radius-sm);
+ background-color: var(--color-bg-surface);
+ align-items: center;
+
+ @media (max-width: 40rem) {
+ flex-direction: column;
+ gap: var(--space-3);
+ align-items: stretch;
+ }
+
+ [data-slot="key-value"] {
+ flex: 1;
+ font-family: var(--font-mono);
+ font-size: var(--font-size-sm);
+ color: var(--color-text);
+ background-color: var(--color-bg);
+ padding: var(--space-3);
+ border-radius: var(--border-radius-sm);
+ border: 1px solid var(--color-border);
+ word-break: break-all;
+ line-height: 1.4;
+
+ @media (max-width: 40rem) {
+ font-size: var(--font-size-xs);
+ padding: var(--space-2-5);
+ }
+ }
+
+ button {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ padding: var(--space-3) var(--space-4);
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+ white-space: nowrap;
+ min-width: 130px;
+
+ @media (max-width: 40rem) {
+ justify-content: center;
+ padding: var(--space-2-5) var(--space-3);
+ font-size: var(--font-size-xs);
+ min-width: 96px;
+ }
+ }
+ }
+ }
+ }
+
+ [data-component="next-steps"] {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-6);
+
+ [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-md);
+ }
+ }
+ }
+
+ ol {
+ margin: 0;
+ padding-left: 0;
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
+ list-style-position: inside;
+
+ li {
+ font-size: var(--font-size-sm);
+ line-height: 1.5;
+ color: var(--color-text-muted);
+
+ code {
+ font-family: var(--font-mono);
+ font-size: var(--font-size-xs);
+ padding: var(--space-1) var(--space-2);
+ background-color: var(--color-bg-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-sm);
+ color: var(--color-text);
+ }
+ }
+ }
+ }
+ }
}
diff --git a/cloud/app/src/routes/workspace/[id].tsx b/cloud/app/src/routes/workspace/[id].tsx
index 959799c63..19d711e8b 100644
--- a/cloud/app/src/routes/workspace/[id].tsx
+++ b/cloud/app/src/routes/workspace/[id].tsx
@@ -237,7 +237,12 @@ function KeysSection() {
<tr>
<td data-slot="key-name">{key.name}</td>
<td data-slot="key-value">
- <div onClick={() => copyKeyToClipboard(key.key, key.id)} title="Click to copy API key">
+ <button
+ data-color="ghost"
+ disabled={copiedId() === key.id}
+ onClick={() => copyKeyToClipboard(key.key, key.id)}
+ title="Copy API key"
+ >
<span>{formatKey(key.key)}</span>
<Show
when={copiedId() === key.id}
@@ -245,7 +250,7 @@ function KeysSection() {
>
<IconCheck style={{ width: "14px", height: "14px" }} />
</Show>
- </div>
+ </button>
</td>
<td data-slot="key-date" title={formatDateUTC(key.timeCreated)}>
{formatDateForTable(key.timeCreated)}
@@ -464,7 +469,99 @@ function PaymentsSection() {
)
}
-export default function () {
+function NewUserSection() {
+ const params = useParams()
+ const keys = createAsync(() => listKeys(params.id))
+ const [copiedKey, setCopiedKey] = createSignal(false)
+
+ async function copyKeyToClipboard(text: string) {
+ try {
+ await navigator.clipboard.writeText(text)
+ setCopiedKey(true)
+ setTimeout(() => setCopiedKey(false), 2000)
+ } catch (error) {
+ console.error("Failed to copy to clipboard:", error)
+ }
+ }
+
+ return (
+ <div data-slot="new-user-sections">
+ <div data-component="feature-grid">
+ <div data-slot="feature">
+ <h3>Tested & Verified Models</h3>
+ <p>We've benchmarked and tested models specifically for coding agents to ensure the best performance.</p>
+ </div>
+ <div data-slot="feature">
+ <h3>Highest Quality</h3>
+ <p>Access models configured for optimal performance - no downgrades or routing to cheaper providers.</p>
+ </div>
+ <div data-slot="feature">
+ <h3>No Lock-in</h3>
+ <p>Use Zen with any coding agent, and continue using other providers with opencode whenever you want.</p>
+ </div>
+ </div>
+
+ <div data-component="api-key-highlight">
+ <div data-slot="section-title">
+ <h2>Your API Key</h2>
+ </div>
+
+ <Show when={keys()?.length}>
+ <div data-slot="key-display">
+ <div data-slot="key-container">
+ <code data-slot="key-value">{keys()![0].key}</code>
+ <button
+ data-color="primary"
+ disabled={copiedKey()}
+ onClick={() => copyKeyToClipboard(keys()![0].key)}
+ title="Copy API key"
+ >
+ <Show
+ when={copiedKey()}
+ fallback={
+ <>
+ <IconCopy style={{ width: "16px", height: "16px" }} /> Copy Key
+ </>
+ }
+ >
+ <IconCheck style={{ width: "16px", height: "16px" }} /> Copied!
+ </Show>
+ </button>
+ </div>
+ </div>
+ </Show>
+ </div>
+
+ <div data-component="next-steps">
+ <div data-slot="section-title">
+ <h2>Next Steps</h2>
+ </div>
+ <ol>
+ <li>Copy your API key above</li>
+ <li>
+ Run <code>opencode auth login</code> and select opencode
+ </li>
+ <li>Paste your API key when prompted</li>
+ <li>
+ Run <code>/models</code> to see available models
+ </li>
+ </ol>
+ </div>
+ </div>
+ )
+}
+
+export default function() {
+ const params = useParams()
+ const keys = createAsync(() => listKeys(params.id))
+ const usage = createAsync(() => getUsageInfo(params.id))
+
+ const isNewUser = createMemo(() => {
+ const keysList = keys()
+ const usageList = usage()
+ return keysList?.length === 1 && (!usageList || usageList.length === 0)
+ })
+
return (
<div data-page="workspace-[id]">
<section data-component="title-section">
@@ -478,12 +575,14 @@ export default function () {
</p>
</section>
- <div data-slot="sections">
- <KeysSection />
- <BalanceSection />
- <UsageSection />
- <PaymentsSection />
- </div>
+ <Show when={!isNewUser()} fallback={<NewUserSection />}>
+ <div data-slot="sections">
+ <KeysSection />
+ <BalanceSection />
+ <UsageSection />
+ <PaymentsSection />
+ </div>
+ </Show>
</div>
)
}
diff --git a/packages/web/src/content/docs/index.mdx b/packages/web/src/content/docs/index.mdx
index 7513d5bf4..928f89c34 100644
--- a/packages/web/src/content/docs/index.mdx
+++ b/packages/web/src/content/docs/index.mdx
@@ -79,7 +79,7 @@ $ opencode auth login
┌ Add credential
◆ Select provider
-│ ● Anthropic (recommended)
+│ ● Anthropic
│ ○ OpenAI
│ ○ Google
│ ○ Amazon Bedrock
diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx
index 641ed7ced..7b109265d 100644
--- a/packages/web/src/content/docs/providers.mdx
+++ b/packages/web/src/content/docs/providers.mdx
@@ -58,7 +58,7 @@ If you are new, we recommend starting with opencode zen.
:::
1. You sign in to **<a href={console}>opencode zen</a>** and get your API key.
-2. You run `opencode auth login` and select opencode zen and add your API key.
+2. You run `opencode auth login` and select opencode and add your API key.
3. Run `/models` in the TUI to see the list of models we recommend.
It works like any other provider in opencode. And is completely optional to use
@@ -131,9 +131,7 @@ $ opencode auth login
┌ Add credential
◆ Select provider
-│ ● Anthropic (recommended)
-│ ○ OpenAI
-│ ○ Google
+│ ● Anthropic
│ ...
```
diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx
index 3f54d30e2..140c66be8 100644
--- a/packages/web/src/content/docs/zen.mdx
+++ b/packages/web/src/content/docs/zen.mdx
@@ -50,7 +50,7 @@ opencode zen is an AI gateway that gives you access to these models.
opencode zen works like any other provider in opencode.
1. You sign in to **<a href={console}>opencode zen</a>** and get your API key.
-2. You run `opencode auth login` and select opencode zen and add your API key.
+2. You run `opencode auth login` and select opencode and add your API key.
3. Run `/models` in the TUI to see the list of models we recommend.
You are charged per request and you can add credits to your account.