Skip to content

Commit 482f396

Browse files
committed
Merge branch 'dev' into feat/no-share-in-header
2 parents 7c1e3e9 + 2105373 commit 482f396

File tree

23 files changed

+421
-79
lines changed

23 files changed

+421
-79
lines changed

bun.lock

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nix/hashes.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"nodeModules": "sha256-OJ3C4RMzfbbG1Fwa/5yru0rlISj+28UPITMNBEU5AeM="
2+
"nodeModules": "sha256-mZGKIkOLmesEhCpEZTLiPbBisZOxdZ1NgqnRnVHJlLU="
33
}

packages/app/src/components/session/session-header.tsx

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,12 @@ export function SessionHeader() {
3535
const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
3636

3737
const sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID))
38-
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
38+
const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
39+
const parentSession = createMemo(() => {
40+
const current = currentSession()
41+
if (!current?.parentID) return undefined
42+
return sync.data.session.find((s) => s.id === current.parentID)
43+
})
3944
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
4045
const worktrees = createMemo(() => layout.projects.list().map((p) => p.worktree), [], { equals: same })
4146

@@ -45,6 +50,8 @@ export function SessionHeader() {
4550

4651
function navigateToSession(session: Session | undefined) {
4752
if (!session) return
53+
// Only navigate if we're actually changing to a different session
54+
if (session.id === params.id) return
4855
navigate(`/${params.dir}/session/${session.id}`)
4956
}
5057

@@ -79,18 +86,56 @@ export function SessionHeader() {
7986
</Select>
8087
<div class="text-text-weaker">/</div>
8188
</div>
82-
<Select
83-
options={sessions()}
84-
current={currentSession()}
85-
placeholder="New session"
86-
label={(x) => x.title}
87-
value={(x) => x.id}
88-
onSelect={navigateToSession}
89-
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
90-
variant="ghost"
91-
/>
89+
<Show
90+
when={parentSession()}
91+
fallback={
92+
<>
93+
<Select
94+
options={sessions()}
95+
current={currentSession()}
96+
placeholder="New session"
97+
label={(x) => x.title}
98+
value={(x) => x.id}
99+
onSelect={navigateToSession}
100+
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
101+
variant="ghost"
102+
/>
103+
</>
104+
}
105+
>
106+
<div class="flex items-center gap-2 min-w-0">
107+
<Select
108+
options={sessions()}
109+
current={parentSession()}
110+
placeholder="Back to parent session"
111+
label={(x) => x.title}
112+
value={(x) => x.id}
113+
onSelect={(session) => {
114+
// Only navigate if selecting a different session than current parent
115+
const currentParent = parentSession()
116+
if (session && currentParent && session.id !== currentParent.id) {
117+
navigateToSession(session)
118+
}
119+
}}
120+
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
121+
variant="ghost"
122+
/>
123+
<div class="text-text-weaker">/</div>
124+
<div class="flex items-center gap-1.5 min-w-0">
125+
<Tooltip value="Back to parent session">
126+
<button
127+
type="button"
128+
class="flex items-center justify-center gap-1 p-1 rounded hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors flex-shrink-0"
129+
onClick={() => navigateToSession(parentSession())}
130+
>
131+
<Icon name="arrow-left" size="small" class="text-icon-base" />
132+
</button>
133+
</Tooltip>
134+
</div>
135+
</div>
136+
</Show>
92137
</div>
93-
<Show when={currentSession()}>
138+
<Show when={currentSession() && !parentSession()}>
94139
<TooltipKeybind class="hidden xl:block" title="New session" keybind={command.keybind("session.new")}>
95140
<IconButton as={A} href={`/${params.dir}/session`} icon="edit-small-2" variant="ghost" />
96141
</TooltipKeybind>

packages/app/src/pages/directory-layout.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createMemo, Show, type ParentProps } from "solid-js"
2-
import { useParams } from "@solidjs/router"
2+
import { useNavigate, useParams } from "@solidjs/router"
33
import { SDKProvider, useSDK } from "@/context/sdk"
44
import { SyncProvider, useSync } from "@/context/sync"
55
import { LocalProvider } from "@/context/local"
@@ -10,6 +10,7 @@ import { iife } from "@opencode-ai/util/iife"
1010

1111
export default function Layout(props: ParentProps) {
1212
const params = useParams()
13+
const navigate = useNavigate()
1314
const directory = createMemo(() => {
1415
return base64Decode(params.dir!)
1516
})
@@ -26,8 +27,17 @@ export default function Layout(props: ParentProps) {
2627
response: "once" | "always" | "reject"
2728
}) => sdk.client.permission.respond(input)
2829

30+
const navigateToSession = (sessionID: string) => {
31+
navigate(`/${params.dir}/session/${sessionID}`)
32+
}
33+
2934
return (
30-
<DataProvider data={sync.data} directory={directory()} onPermissionRespond={respond}>
35+
<DataProvider
36+
data={sync.data}
37+
directory={directory()}
38+
onPermissionRespond={respond}
39+
onNavigateToSession={navigateToSession}
40+
>
3141
<LocalProvider>{props.children}</LocalProvider>
3242
</DataProvider>
3343
)

packages/app/src/pages/session.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ export default function Page() {
502502
// Restore the prompt from the reverted message
503503
const parts = sync.data.part[message.id]
504504
if (parts) {
505-
const restored = extractPromptFromParts(parts)
505+
const restored = extractPromptFromParts(parts, { directory: sdk.directory })
506506
prompt.set(restored)
507507
}
508508
// Navigate to the message before the reverted one (which will be the new last visible message)

packages/app/src/utils/prompt.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,25 @@ function textPartValue(parts: Part[]) {
5353
* Extract prompt content from message parts for restoring into the prompt input.
5454
* This is used by undo to restore the original user prompt.
5555
*/
56-
export function extractPromptFromParts(parts: Part[]): Prompt {
56+
export function extractPromptFromParts(parts: Part[], opts?: { directory?: string }): Prompt {
5757
const textPart = textPartValue(parts)
5858
const text = textPart?.text ?? ""
59+
const directory = opts?.directory
60+
61+
const toRelative = (path: string) => {
62+
if (!directory) return path
63+
64+
const prefix = directory.endsWith("/") ? directory : directory + "/"
65+
if (path.startsWith(prefix)) return path.slice(prefix.length)
66+
67+
if (path.startsWith(directory)) {
68+
const next = path.slice(directory.length)
69+
if (next.startsWith("/")) return next.slice(1)
70+
return next
71+
}
72+
73+
return path
74+
}
5975

6076
const inline: Inline[] = []
6177
const images: ImageAttachmentPart[] = []
@@ -78,7 +94,7 @@ export function extractPromptFromParts(parts: Part[]): Prompt {
7894
start,
7995
end,
8096
value,
81-
path,
97+
path: toRelative(path),
8298
selection: selectionFromFileUrl(filePart.url),
8399
})
84100
continue
@@ -158,20 +174,21 @@ export function extractPromptFromParts(parts: Part[]): Prompt {
158174

159175
for (const item of inline) {
160176
if (item.start < 0 || item.end < item.start) continue
161-
if (item.end > text.length) continue
162-
if (item.start < cursor) continue
163177

164-
pushText(text.slice(cursor, item.start))
178+
const expected = item.value
179+
if (!expected) continue
165180

166-
if (item.type === "file") {
167-
pushFile(item)
168-
}
181+
const mismatch = item.end > text.length || item.start < cursor || text.slice(item.start, item.end) !== expected
182+
const start = mismatch ? text.indexOf(expected, cursor) : item.start
183+
if (start === -1) continue
184+
const end = mismatch ? start + expected.length : item.end
169185

170-
if (item.type === "agent") {
171-
pushAgent(item)
172-
}
186+
pushText(text.slice(cursor, start))
187+
188+
if (item.type === "file") pushFile(item)
189+
if (item.type === "agent") pushAgent(item)
173190

174-
cursor = item.end
191+
cursor = end
175192
}
176193

177194
pushText(text.slice(cursor))

packages/console/app/src/routes/workspace/[id]/billing/payment-section.module.css

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,19 @@
4545
text-decoration: line-through;
4646
}
4747
}
48+
49+
&[data-slot="payment-receipt"] {
50+
span {
51+
display: inline-block;
52+
padding: var(--space-3) var(--space-4);
53+
font-size: var(--font-size-sm);
54+
line-height: 1.5;
55+
}
56+
57+
button {
58+
font-size: var(--font-size-sm);
59+
}
60+
}
4861
}
4962

5063
tbody tr {
@@ -61,13 +74,17 @@
6174
}
6275

6376
th {
64-
&:nth-child(2) /* Payment ID */ {
77+
&:nth-child(2)
78+
79+
/* Payment ID */ {
6580
display: none;
6681
}
6782
}
6883

6984
td {
70-
&:nth-child(2) /* Payment ID */ {
85+
&:nth-child(2)
86+
87+
/* Payment ID */ {
7188
display: none;
7289
}
7390
}

packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export function PaymentSection() {
7777
<For each={payments()!}>
7878
{(payment) => {
7979
const date = new Date(payment.timeCreated)
80+
const isCredit = !payment.paymentID
8081
return (
8182
<tr>
8283
<td data-slot="payment-date" title={formatDateUTC(date)}>
@@ -85,19 +86,24 @@ export function PaymentSection() {
8586
<td data-slot="payment-id">{payment.id}</td>
8687
<td data-slot="payment-amount" data-refunded={!!payment.timeRefunded}>
8788
${((payment.amount ?? 0) / 100000000).toFixed(2)}
89+
{isCredit ? " (credit)" : ""}
8890
</td>
8991
<td data-slot="payment-receipt">
90-
<button
91-
onClick={async () => {
92-
const receiptUrl = await downloadReceiptAction(params.id!, payment.paymentID!)
93-
if (receiptUrl) {
94-
window.open(receiptUrl, "_blank")
95-
}
96-
}}
97-
data-slot="receipt-button"
98-
>
99-
View
100-
</button>
92+
{isCredit ? (
93+
<span>-</span>
94+
) : (
95+
<button
96+
onClick={async () => {
97+
const receiptUrl = await downloadReceiptAction(params.id!, payment.paymentID!)
98+
if (receiptUrl) {
99+
window.open(receiptUrl, "_blank")
100+
}
101+
}}
102+
data-slot="receipt-button"
103+
>
104+
View
105+
</button>
106+
)}
101107
</td>
102108
</tr>
103109
)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Billing } from "../src/billing.js"
2+
3+
// get input from command line
4+
const workspaceID = process.argv[2]
5+
const dollarAmount = process.argv[3]
6+
7+
if (!workspaceID || !dollarAmount) {
8+
console.error("Usage: bun credit-workspace.ts <workspaceID> <dollarAmount>")
9+
process.exit(1)
10+
}
11+
12+
const amountInDollars = parseFloat(dollarAmount)
13+
if (isNaN(amountInDollars) || amountInDollars <= 0) {
14+
console.error("Error: dollarAmount must be a positive number")
15+
process.exit(1)
16+
}
17+
18+
await Billing.grantCredit(workspaceID, amountInDollars)
19+
20+
console.log(`Added payment of $${amountInDollars.toFixed(2)} to workspace ${workspaceID}`)

0 commit comments

Comments
 (0)