mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-20 06:53:27 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b567db3495 | |||
| 06dffe3671 | |||
| 56e66656b8 |
@@ -82,6 +82,7 @@ jobs:
|
||||
build-cli:
|
||||
needs: version
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
@@ -852,8 +852,26 @@ function provider() {
|
||||
name: "OpenCode",
|
||||
models: { "claude-opus-4-6": { id: "claude-opus-4-6", name: "Claude Opus 4.6", limit: { context: 200_000 } } },
|
||||
},
|
||||
{
|
||||
id: "company-gateway",
|
||||
name: "Company Gateway",
|
||||
models: {
|
||||
"fast-nano": {
|
||||
id: "fast-nano",
|
||||
api: { id: "openai/gpt-5.4-nano" },
|
||||
name: "GPT-5.4 nano",
|
||||
limit: { context: 128_000 },
|
||||
},
|
||||
"long-context": {
|
||||
id: "long-context",
|
||||
api: { id: "company/long-context" },
|
||||
name: "Company Gateway Extra Long Context Model for Narrow Timeline Layouts",
|
||||
limit: { context: 128_000 },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
connected: ["opencode"],
|
||||
connected: ["opencode", "company-gateway"],
|
||||
default: { providerID: "opencode", modelID: "claude-opus-4-6" },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,43 @@ test.describe("session timeline projection", () => {
|
||||
expect(rows).toEqual(["AssistantPart", "TurnDivider", "AssistantPart"])
|
||||
})
|
||||
|
||||
test("renders aliased and long custom model notices", async ({ page }) => {
|
||||
const shortName = "GPT-5.4 nano"
|
||||
const longName = "Company Gateway Extra Long Context Model for Narrow Timeline Layouts"
|
||||
await setupTimeline(page, {
|
||||
viewport: { width: 420, height: 700 },
|
||||
sessionMessages: [
|
||||
{
|
||||
id: "msg_model_fast_nano",
|
||||
type: "model-switched",
|
||||
time: { created: 1700000000000 },
|
||||
model: { providerID: "company-gateway", id: "fast-nano", variant: "xhigh" },
|
||||
},
|
||||
{
|
||||
id: "msg_model_long_context",
|
||||
type: "model-switched",
|
||||
time: { created: 1700000001000 },
|
||||
model: { providerID: "company-gateway", id: "long-context" },
|
||||
},
|
||||
userMessage(),
|
||||
assistantMessage(),
|
||||
],
|
||||
})
|
||||
|
||||
const shortNotice = page.locator('[data-slot="session-timeline-notice"]').filter({ hasText: shortName })
|
||||
const longNotice = page.locator('[data-slot="session-timeline-notice"]').filter({ hasText: longName })
|
||||
await expect(shortNotice).toBeVisible()
|
||||
await expect(shortNotice.getByText(`Switched to ${shortName}`, { exact: true })).toBeVisible()
|
||||
await expect(shortNotice.locator('[data-slot="session-timeline-notice-variant"]')).toHaveText("xhigh")
|
||||
await expect(page.getByText("fast-nano", { exact: true })).toHaveCount(0)
|
||||
await expect(shortNotice.locator('[data-component="provider-icon"]')).toBeVisible()
|
||||
await expect(longNotice).toBeVisible()
|
||||
await expect(longNotice.locator('[data-component="provider-icon"]')).toBeVisible()
|
||||
await expect(longNotice.locator('[data-slot="session-timeline-notice-variant"]')).toHaveCount(0)
|
||||
await expect(longNotice.locator("[title]")).toHaveAttribute("title", `Switched to ${longName}`)
|
||||
await expect.poll(() => longNotice.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true)
|
||||
})
|
||||
|
||||
test("renders user image, file attachment, file reference, and agent reference", async ({ page }) => {
|
||||
const text = "Use @explore with @src/a.ts and inspect the attachments"
|
||||
const parts: PartSeed<"user">[] = [
|
||||
|
||||
@@ -473,7 +473,7 @@ function currentModels(value: unknown) {
|
||||
return [
|
||||
{
|
||||
id: model.id,
|
||||
modelID: model.id,
|
||||
modelID: record(model.api) && typeof model.api.id === "string" ? model.api.id : model.id,
|
||||
providerID: provider.id,
|
||||
name: model.name,
|
||||
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
||||
|
||||
@@ -670,6 +670,7 @@ export const dict = {
|
||||
"session.timeline.notice.finished": "{{actor}} finished",
|
||||
"session.timeline.notice.failed": "{{actor}} failed",
|
||||
"session.timeline.notice.cancelled": "{{actor}} cancelled",
|
||||
"session.timeline.notice.modelSwitched": "Switched to {{model}}",
|
||||
"session.error.serverConnection": "Can't connect to this server",
|
||||
"session.review.filesChanged": "Files Changed {{count}}",
|
||||
"session.review.change.one": "Change",
|
||||
|
||||
@@ -13,9 +13,10 @@ import {
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createVirtualizer, defaultRangeExtractor, elementScroll, type VirtualItem } from "@tanstack/solid-virtual"
|
||||
import { Card } from "@opencode-ai/ui/card"
|
||||
import { MessageDivider, SessionShellMessage, type UserActions } from "@opencode-ai/session-ui/message-part"
|
||||
import { SessionShellMessage, type UserActions } from "@opencode-ai/session-ui/message-part"
|
||||
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||
@@ -103,6 +104,37 @@ function TimelineThinkingRow(props: { reasoningHeading?: string; showReasoningSu
|
||||
)
|
||||
}
|
||||
|
||||
function TimelineSeparator(props: { label: string; providerID?: string; variant?: string }) {
|
||||
return (
|
||||
<div class="flex h-8 w-full items-center gap-3 text-v2-text-text-faint">
|
||||
<span class="h-px min-w-0 flex-1 bg-v2-border-border-strong" />
|
||||
<span class="flex min-w-0 items-center gap-1 text-[13px] font-[440] leading-4 tracking-[-0.04px]">
|
||||
<Show when={props.providerID}>
|
||||
{(providerID) => <ProviderIcon id={providerID()} class="text-v2-icon-icon-faint" aria-hidden="true" />}
|
||||
</Show>
|
||||
<span class="flex min-w-0 items-center gap-1.5">
|
||||
<span class="truncate" title={props.label}>
|
||||
{props.label}
|
||||
</span>
|
||||
<Show when={props.variant && props.variant !== "default" ? props.variant : undefined}>
|
||||
{(variant) => (
|
||||
<>
|
||||
<span class="flex size-1.5 shrink-0 items-center justify-center" aria-hidden="true">
|
||||
<span class="size-[2.25px] rounded-full bg-current" />
|
||||
</span>
|
||||
<span data-slot="session-timeline-notice-variant" class="shrink-0 capitalize">
|
||||
{variant()}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
</span>
|
||||
</span>
|
||||
<span class="h-px min-w-0 flex-1 bg-v2-border-border-strong" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function WorkspaceMoveAction(props: {
|
||||
variant: "inline" | "panel"
|
||||
eligible: boolean
|
||||
@@ -369,11 +401,7 @@ function MessageTimelineView(
|
||||
label: language.t("ui.tool.agent.default"),
|
||||
data: message.previous ? `${message.previous} → ${message.agent}` : message.agent,
|
||||
}
|
||||
if (message.type === "model-switched")
|
||||
return {
|
||||
label: language.t("command.category.model"),
|
||||
data: `${message.model.providerID}/${message.model.id}`,
|
||||
}
|
||||
if (message.type === "model-switched") return undefined
|
||||
if (message.type === "location-switched")
|
||||
return { label: language.t("ui.patch.action.moved"), data: message.location.directory }
|
||||
if (message.type === "skill") return { label: language.t("ui.tool.skill"), data: message.name }
|
||||
@@ -952,20 +980,50 @@ function MessageTimelineView(
|
||||
}
|
||||
case "Notice": {
|
||||
const noticeRow = row as Accessor<TimelineRowByTag<"Notice">>
|
||||
const model = createMemo(() => {
|
||||
const message = sessionMessageByID().get(noticeRow().messageID)
|
||||
if (message?.type !== "model-switched") return undefined
|
||||
const info = data.location.model
|
||||
.list({ directory: sessionDirectory() })
|
||||
?.find((item) => item.providerID === message.model.providerID && item.id === message.model.id)
|
||||
return {
|
||||
providerID: message.model.providerID,
|
||||
variant: message.model.variant,
|
||||
label: language.t("session.timeline.notice.modelSwitched", { model: info?.name ?? message.model.id }),
|
||||
}
|
||||
})
|
||||
const content = createMemo(() => {
|
||||
const message = sessionMessageByID().get(noticeRow().messageID)
|
||||
return message ? noticeContent(message) : undefined
|
||||
})
|
||||
return (
|
||||
<TimelineRowFrame row={noticeRow()}>
|
||||
<Show when={content()}>
|
||||
{(content) => (
|
||||
<Show
|
||||
when={model()}
|
||||
fallback={
|
||||
<Show when={content()}>
|
||||
{(content) => (
|
||||
<div
|
||||
data-slot="session-timeline-notice"
|
||||
class={`w-full pt-3 pb-1 text-13-regular text-text-weak ${turnPadding()}`}
|
||||
>
|
||||
<span class="text-13-medium">{content().label}</span>
|
||||
<Show when={content().data}>{(data) => <span> · {data()}</span>}</Show>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
{(model) => (
|
||||
<div
|
||||
data-slot="session-timeline-notice"
|
||||
class={`w-full pt-3 pb-1 text-13-regular text-text-weak ${turnPadding()}`}
|
||||
class={`w-full py-2 ${turnPadding()}`}
|
||||
>
|
||||
<span class="text-13-medium">{content().label}</span>
|
||||
<Show when={content().data}>{(data) => <span> · {data()}</span>}</Show>
|
||||
<TimelineSeparator
|
||||
label={model().label}
|
||||
providerID={model().providerID}
|
||||
variant={model().variant}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
@@ -978,7 +1036,9 @@ function MessageTimelineView(
|
||||
<TimelineRowFrame row={turnDividerRow()}>
|
||||
<div data-slot="session-turn-message-container" class={`w-full ${turnPadding()}`}>
|
||||
<div data-slot="session-turn-compaction">
|
||||
<MessageDivider label={language.t("ui.message.interrupted")} />
|
||||
<div class="py-2">
|
||||
<TimelineSeparator label={language.t("ui.message.interrupted")} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TimelineRowFrame>
|
||||
|
||||
@@ -9,7 +9,7 @@ export type ProviderIconProps = JSX.SVGElementTags["svg"] & {
|
||||
|
||||
export const ProviderIcon: Component<ProviderIconProps> = (props) => {
|
||||
const [local, rest] = splitProps(props, ["id", "class", "classList"])
|
||||
const resolved = createMemo(() => (iconNames.includes(local.id as IconName) ? local.id : "session.synthetic"))
|
||||
const resolved = createMemo(() => (iconNames.includes(local.id as IconName) ? local.id : "synthetic"))
|
||||
return (
|
||||
<svg
|
||||
data-component="provider-icon"
|
||||
|
||||
@@ -252,6 +252,7 @@
|
||||
--color-v2-text-text-code-accent: var(--v2-text-text-code-accent);
|
||||
--color-v2-icon-icon-base: var(--v2-icon-icon-base);
|
||||
--color-v2-icon-icon-muted: var(--v2-icon-icon-muted);
|
||||
--color-v2-icon-icon-faint: var(--v2-icon-icon-faint);
|
||||
--color-v2-icon-icon-inverse: var(--v2-icon-icon-inverse);
|
||||
--color-v2-icon-icon-contrast: var(--v2-icon-icon-contrast);
|
||||
--color-v2-icon-icon-accent: var(--v2-icon-icon-accent);
|
||||
|
||||
@@ -176,6 +176,7 @@
|
||||
"v2-text-text-code-accent": "var(--v2-blue-900)",
|
||||
"v2-icon-icon-base": "var(--v2-grey-800)",
|
||||
"v2-icon-icon-muted": "var(--v2-grey-600)",
|
||||
"v2-icon-icon-faint": "var(--v2-grey-500)",
|
||||
"v2-icon-icon-inverse": "var(--v2-grey-50)",
|
||||
"v2-icon-icon-contrast": "var(--v2-grey-100)",
|
||||
"v2-icon-icon-accent": "var(--v2-blue-600)",
|
||||
@@ -411,6 +412,7 @@
|
||||
"v2-text-text-code-accent": "var(--v2-blue-400)",
|
||||
"v2-icon-icon-base": "var(--v2-grey-400)",
|
||||
"v2-icon-icon-muted": "var(--v2-grey-600)",
|
||||
"v2-icon-icon-faint": "var(--v2-grey-700)",
|
||||
"v2-icon-icon-inverse": "var(--v2-grey-1100)",
|
||||
"v2-icon-icon-contrast": "var(--v2-grey-100)",
|
||||
"v2-icon-icon-accent": "var(--v2-blue-400)",
|
||||
|
||||
@@ -52,6 +52,7 @@ export function mapV2Foreground(
|
||||
"v2-text-text-faint": shift(body, { l: isDark ? -0.2 : 0.21, c: isDark ? 0.78 : 0.72 }),
|
||||
"v2-icon-icon-base": greyRef(pickGrey(primitives, bgBase, 7, isDark ? 400 : 800)),
|
||||
"v2-icon-icon-muted": greyRef(pickGrey(primitives, bgBase, 3, 600)),
|
||||
"v2-icon-icon-faint": greyRef(pickGrey(primitives, bgBase, 2, isDark ? 700 : 500)),
|
||||
"v2-icon-icon-inverse": greyRef(pickGrey(primitives, bgInverse, 7, inverseTarget)),
|
||||
"v2-icon-icon-contrast": greyRef(pickGrey(primitives, bgContrast, 7, 100)),
|
||||
"v2-icon-icon-accent": isDark ? "var(--v2-blue-400)" : "var(--v2-blue-600)",
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
/* ── Icon ── */
|
||||
--v2-icon-icon-base: var(--v2-grey-800);
|
||||
--v2-icon-icon-muted: var(--v2-grey-600);
|
||||
--v2-icon-icon-faint: var(--v2-grey-500);
|
||||
--v2-icon-icon-inverse: var(--v2-grey-50);
|
||||
--v2-icon-icon-contrast: var(--v2-grey-100);
|
||||
--v2-icon-icon-accent: var(--v2-blue-600);
|
||||
@@ -158,6 +159,7 @@
|
||||
|
||||
--v2-icon-icon-base: var(--v2-grey-400);
|
||||
--v2-icon-icon-muted: var(--v2-grey-600);
|
||||
--v2-icon-icon-faint: var(--v2-grey-700);
|
||||
--v2-icon-icon-inverse: var(--v2-grey-1000);
|
||||
--v2-icon-icon-contrast: var(--v2-grey-200);
|
||||
--v2-icon-icon-accent: var(--v2-blue-400);
|
||||
@@ -271,6 +273,7 @@
|
||||
|
||||
--v2-icon-icon-base: var(--v2-grey-800);
|
||||
--v2-icon-icon-muted: var(--v2-grey-600);
|
||||
--v2-icon-icon-faint: var(--v2-grey-500);
|
||||
--v2-icon-icon-inverse: var(--v2-grey-50);
|
||||
--v2-icon-icon-contrast: var(--v2-grey-100);
|
||||
--v2-icon-icon-accent: var(--v2-blue-600);
|
||||
@@ -393,6 +396,7 @@
|
||||
|
||||
--v2-icon-icon-base: var(--v2-grey-400);
|
||||
--v2-icon-icon-muted: var(--v2-grey-600);
|
||||
--v2-icon-icon-faint: var(--v2-grey-700);
|
||||
--v2-icon-icon-inverse: var(--v2-grey-1100);
|
||||
--v2-icon-icon-contrast: var(--v2-grey-100);
|
||||
--v2-icon-icon-accent: var(--v2-blue-400);
|
||||
|
||||
Reference in New Issue
Block a user