Compare commits

...
3 Commits
13 changed files with 105 additions and 12 deletions

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

@@ -859,6 +859,41 @@ for (const delivery of ["steer", "queue"] as const) {
await expect(pending).toContainText(followUp)
await expect(thinking).toHaveCount(0)
const bubble = pending.locator('[data-slot="user-message-text"]')
const colors =
delivery === "steer"
? {
delivered: await userRow(page, userID)
.locator('[data-slot="user-message-text"]')
.evaluate((element) => ({
background: getComputedStyle(element).backgroundColor,
text: getComputedStyle(element).color,
})),
pending: await bubble.evaluate((element) => {
const probe = document.createElement("span")
probe.style.backgroundColor = "var(--v2-background-bg-layer-02)"
probe.style.color = "var(--v2-text-text-base)"
element.appendChild(probe)
const colors = {
background: getComputedStyle(probe).backgroundColor,
text: getComputedStyle(probe).color,
}
probe.remove()
return colors
}),
}
: undefined
if (colors) {
await expect(bubble).toHaveCSS("background-color", colors.pending.background)
await expect(bubble).toHaveCSS("color", colors.pending.text)
await pending.hover()
await expect(pending.locator('[data-slot="user-message-meta"]')).toHaveText(
/^Pending\s*·\s*Build\s*·\s*Queue Model$/,
)
await bubble.evaluate((element) => ((element as HTMLElement).dataset.deliveryMarker = "pending"))
}
// The next assistant step still belongs to U1: U2 has been admitted, not delivered.
mock.emit("session.step.started", {
sessionID,
@@ -921,6 +956,16 @@ for (const delivery of ["steer", "queue"] as const) {
mock.emit("session.inbox.delivered", { sessionID, inboxID })
await expect(thinking).toHaveCount(0)
await expect(pending).toHaveCount(1)
if (colors) {
await expect(bubble).toHaveAttribute("data-delivery-marker", "pending")
await expect(pending.locator('[data-slot="user-message-meta"]')).toHaveText(/^Build\s*·\s*Queue Model$/)
await expect(bubble).toHaveCSS("background-color", colors.delivered.background)
await expect(bubble).toHaveCSS("color", colors.delivered.text)
await expect(bubble).toHaveCSS("transition-property", "background-color, color")
await transcript.screenshot({ path: testInfo.outputPath("delivered-steer.png") })
await page.emulateMedia({ reducedMotion: "reduce" })
await expect(bubble).toHaveCSS("transition-duration", "0s")
}
await expect(transcript.locator('[data-timeline-row="UserMessage"]')).toHaveCount(2)
await expect(transcript.locator('[data-timeline-row="AssistantPart"]').filter({ has: tools })).toHaveAttribute(
"data-message-id",
@@ -181,7 +181,7 @@ function SessionQueueRow(props: { queue: SessionQueueView; id: string; index: nu
type="button"
size="small"
variant="ghost-muted"
icon={<Icon name="arrow-down-to-line" />}
icon={<Icon name="outline-undo" />}
disabled={props.queue.busy()}
aria-label={language.t("session.queue.undo")}
onClick={() => props.queue.undo(props.id)}
+12 -1
View File
@@ -40,6 +40,17 @@ CLI preparation uses these channel rules:
default. `bun dev --download-server <version>` instead downloads that CLI version for local development. Neither path
requires `OPENCODE_CLI_DIST` or runs the production prebuild.
To run a worktree alongside another dev instance, give it a separate profile and unused server/debug ports:
```bash
OPENCODE_DESKTOP_TEST_ROOT="$HOME/.local/share/opencode/desktop-worktrees/my-worktree" \
OPENCODE_DESKTOP_SERVER_PORT=3085 \
OPENCODE_DESKTOP_REMOTE_DEBUGGING_PORT=9223 bun dev
```
The profile isolates desktop state, server data, configuration, and service registration. The renderer dev server
automatically selects an available port. Without the server port override, isolated dev servers use port `3084`.
## Startup benchmark
`bun run bench:startup` measures a **packaged** build from process spawn to the restored tab being ready and the
@@ -74,4 +85,4 @@ written to `dist/bench-startup`.
A packaged beta or prod build registers itself as the `opencode://` handler when it starts, even from the bench; the
installed app takes the registration back on its next launch. Those channels run with `HTTPS_PROXY` pointed at a
closed port (`--offline` forces it for dev) so the updater's first check fails fast instead of reaching GitHub.
closed port (`--offline` forces it for dev) so the updater's first check fails fast instead of reaching GitHub.
@@ -50,7 +50,9 @@ const connect = Effect.fn("BackgroundService.connect")(function* (mode: "initial
...cli.command,
"serve",
"--service",
...(isolated ? ["--hostname", "0.0.0.0", "--port", String(0x0c0c)] : []),
...(isolated
? ["--hostname", "0.0.0.0", "--port", process.env.OPENCODE_DESKTOP_SERVER_PORT ?? String(0x0c0c)]
: []),
],
onStart: (reason, previousVersion) =>
runFork(Effect.logInfo("v2 CLI background service starting", { reason, previousVersion })),
@@ -145,6 +145,13 @@
border: none;
padding: 8px 12px;
border-radius: 10px;
transition:
background-color 200ms ease,
color 200ms ease;
@media (prefers-reduced-motion: reduce) {
transition: none;
}
[data-highlight="file"],
[data-highlight="agent"] {
@@ -156,6 +163,11 @@
[data-slot="user-message-mention-prefix"] {
color: var(--user-message-mention-prefix-color);
transition: color 200ms ease;
@media (prefers-reduced-motion: reduce) {
transition: none;
}
}
max-width: 100%;
@@ -1573,29 +1585,41 @@
background: var(--v2-background-bg-layer-02);
}
:root body [data-workspace-session] [data-component="user-message"] [data-slot="user-message-text"] {
:root body
[data-workspace-session]
[data-component="user-message"]:not([data-pending])
[data-slot="user-message-text"] {
--user-message-mention-prefix-color: var(--v2-blue-300);
background: var(--v2-background-bg-accent);
color: var(--v2-text-text-contrast);
}
:root body [data-local-session] [data-component="user-message"] [data-slot="user-message-text"] {
border-radius: 8px;
}
:root[data-color-scheme="light"]
body
[data-local-session]
[data-component="user-message"]
[data-component="user-message"]:not([data-pending])
[data-slot="user-message-text"] {
background: var(--v2-blue-100);
color: var(--v2-blue-700);
border-radius: 8px;
}
:root[data-color-scheme="dark"]
body
[data-local-session]
[data-component="user-message"]
[data-component="user-message"]:not([data-pending])
[data-slot="user-message-text"] {
background: var(--v2-blue-1200);
color: var(--v2-blue-300);
border-radius: 8px;
}
:root body [data-component="user-message"][data-pending] [data-slot="user-message-text"] {
--user-message-mention-prefix-color: currentColor;
background: var(--v2-background-bg-layer-02);
color: var(--v2-text-text-base);
}
@@ -74,7 +74,7 @@ function modelLabel(
function MessageActionButton(
props: Pick<ComponentProps<"button">, "disabled" | "onMouseDown" | "onClick" | "aria-label"> & {
icon: "check" | "copy" | "reset" | "bullet-list" | "outline-trash"
icon: "check" | "copy" | "reset" | "arrow-down-to-line" | "outline-trash"
label: JSX.Element
},
) {
@@ -271,7 +271,9 @@ export function CurrentUserMessageDisplay(props: {
const metaHead = createMemo(() => {
const agent = props.agent
return [agent ? agent[0]?.toUpperCase() + agent.slice(1) : "", model()].filter(Boolean).join("\u00A0\u00B7\u00A0")
return [pending() ? i18n.t("ui.message.pending") : "", agent ? agent[0]?.toUpperCase() + agent.slice(1) : "", model()]
.filter(Boolean)
.join("\u00A0\u00B7\u00A0")
})
const stamp = createMemo(() => timefmt().format(props.message.time.created))
@@ -349,7 +351,11 @@ export function CurrentUserMessageDisplay(props: {
)
return (
<div data-component="user-message" data-timeline-part-id={props.text ? `${props.message.id}:text:0` : undefined}>
<div
data-component="user-message"
data-pending={pending() ? "true" : undefined}
data-timeline-part-id={props.text ? `${props.message.id}:text:0` : undefined}
>
<Show
when={props.text}
fallback={
@@ -388,7 +394,7 @@ export function CurrentUserMessageDisplay(props: {
</span>
<Show when={pending()}>
<MessageActionButton
icon="bullet-list"
icon="arrow-down-to-line"
label={i18n.t("ui.message.moveToQueue")}
disabled={state.updating}
onMouseDown={(event) => event.preventDefault()}
+1
View File
@@ -244,6 +244,7 @@ const source = {
"ui.message.forkMessage": "Fork to new session",
"ui.message.revertMessage": "Revert message",
"ui.message.moveToQueue": "Move to queue",
"ui.message.pending": "Pending",
"ui.message.deletePending": "Delete",
"ui.message.copyResponse": "Copy response",
"ui.message.copied": "Copied",
+4
View File
@@ -222,6 +222,10 @@ const icons = {
viewBox: "0 0 20 20",
body: `<path d="M5.83333 4.16406L2.5 7.4974L5.83333 10.8307M3.33333 7.4974H17.9167V15.4141H10" stroke="currentColor" stroke-linecap="square"/>`,
},
"outline-undo": {
viewBox: "0 0 16 16",
body: `<path d="M2 10L4.25193 7.76999C7.29213 4.7594 12.4119 5.77372 14.0747 9.71606" stroke="currentColor"/><path d="M2 6V10H6" stroke="currentColor" stroke-linecap="square"/>`,
},
"outline-reset": {
viewBox: "0 0 20 20",
body: `<path d="M5.83333 4.16406L2.5 7.4974L5.83333 10.8307M3.33333 7.4974H17.9167V15.4141H10" stroke="currentColor" stroke-linecap="square"/>`,