mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 03:56:55 +03:00
fix(session-ui): space errors and grouped updates in timeline (#52876)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
48b295aef4
commit
3ca0ac718b
@@ -1,5 +1,25 @@
|
||||
import { expect, story } from "../../storybook/playwright/story"
|
||||
|
||||
story("spaces an error between a shell result and expanded updates", async ({ mount }) => {
|
||||
const timeline = await mount("current-session-error-spacing--error-and-updates")
|
||||
const shell = timeline.locator('[data-component="bash-output"]')
|
||||
const error = timeline.locator('[data-kind="session-error-card"]')
|
||||
const updates = timeline.locator('[data-component="collapsed-tool-group"] [data-slot="collapsible-trigger"]')
|
||||
const shellBox = (await shell.boundingBox())!
|
||||
const errorBox = (await error.boundingBox())!
|
||||
const updatesBox = (await updates.boundingBox())!
|
||||
expect(errorBox.y - (shellBox.y + shellBox.height)).toBe(24)
|
||||
expect(updatesBox.y - (errorBox.y + errorBox.height)).toBe(8)
|
||||
expect(updatesBox.x).toBe(errorBox.x)
|
||||
await updates.click()
|
||||
const notice = timeline.locator('[data-slot="session-timeline-notice"]')
|
||||
await expect(notice).toContainText("Instructions updated")
|
||||
const expandedBox = (await updates.boundingBox())!
|
||||
expect((await notice.boundingBox())!.y - (expandedBox.y + expandedBox.height)).toBe(0)
|
||||
expect((await notice.boundingBox())!.x - expandedBox.x).toBe(12)
|
||||
expect(expandedBox.height).toBe(28)
|
||||
})
|
||||
|
||||
for (const streaming of [false, true]) {
|
||||
story(`renders Mermaid in the ${streaming ? "streaming" : "completed"} timeline`, async ({ mount, page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 900 })
|
||||
|
||||
@@ -688,6 +688,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-notices-only] > [data-component="collapsed-tool-group"] > [data-component="collapsible"].tool-collapsible {
|
||||
--tool-content-gap: 0px;
|
||||
}
|
||||
|
||||
[data-component="context-tool-group-list"] {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
@@ -1585,7 +1589,8 @@
|
||||
background: var(--v2-background-bg-layer-02);
|
||||
}
|
||||
|
||||
:root body
|
||||
:root
|
||||
body
|
||||
[data-workspace-session]
|
||||
[data-component="user-message"]:not([data-pending])
|
||||
[data-slot="user-message-text"] {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { SessionDocument } from "../document"
|
||||
import { CURRENT_SESSION_ID, STORY_MODEL, STORY_TIME } from "../storybook/current-session-fixtures"
|
||||
import { CurrentSessionProviders } from "../storybook/current-session-story"
|
||||
import { timelinePresets } from "./detail"
|
||||
import { SessionTimeline } from "./session-timeline"
|
||||
|
||||
export default {
|
||||
title: "OpenCode/Conversation/Error spacing",
|
||||
id: "current-session-error-spacing",
|
||||
component: SessionTimeline,
|
||||
parameters: { layout: "fullscreen" },
|
||||
}
|
||||
|
||||
const document = {
|
||||
sessionID: CURRENT_SESSION_ID,
|
||||
status: { type: "idle" },
|
||||
diffs: [],
|
||||
messages: [
|
||||
{
|
||||
id: "msg_user_shell_error",
|
||||
type: "user",
|
||||
text: "Check the shell command and its result.",
|
||||
metadata: { agent: "build", model: STORY_MODEL },
|
||||
time: { created: STORY_TIME },
|
||||
},
|
||||
{
|
||||
id: "msg_shell_error",
|
||||
type: "assistant",
|
||||
agent: "build",
|
||||
model: STORY_MODEL,
|
||||
content: [
|
||||
{
|
||||
type: "tool",
|
||||
id: "tool_shell_error",
|
||||
name: "shell",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { command: "cd ~/Documents/Local/opencode && ls packages/tui" },
|
||||
content: [{ type: "text", text: "AGENTS.md\nbunfig.toml\nnode_modules\npackage.json\nsrc" }],
|
||||
metadata: {},
|
||||
},
|
||||
time: { created: STORY_TIME + 100, ran: STORY_TIME + 150, completed: STORY_TIME + 300 },
|
||||
},
|
||||
],
|
||||
error: { type: "ProviderError", message: "getaddrinfo ENOTFOUND opencode.ai" },
|
||||
time: { created: STORY_TIME + 50, completed: STORY_TIME + 400 },
|
||||
},
|
||||
{
|
||||
id: "msg_after_error_updates",
|
||||
type: "system",
|
||||
description: "Instructions updated: core/codemode, core/mcp-guidance",
|
||||
text: "Instructions updated: core/codemode, core/mcp-guidance",
|
||||
time: { created: STORY_TIME + 500 },
|
||||
},
|
||||
],
|
||||
} satisfies SessionDocument
|
||||
|
||||
export const ErrorAndUpdates = {
|
||||
render: () => (
|
||||
<section class="mx-auto min-h-screen w-full max-w-[720px] bg-v2-background-bg-base p-6">
|
||||
<CurrentSessionProviders document={document}>
|
||||
<SessionTimeline
|
||||
document={document}
|
||||
timelineDetail={timelinePresets.find((preset) => preset.id === "detailed")!.value}
|
||||
/>
|
||||
</CurrentSessionProviders>
|
||||
</section>
|
||||
),
|
||||
}
|
||||
@@ -550,7 +550,10 @@ function groupMessages(rows: TimelineRow.TimelineRow[], detail: TimelineDetail,
|
||||
? new TimelineRow.AssistantPart({
|
||||
userMessageID: row.userMessageID,
|
||||
previousAssistantPart: Predicate.isTagged(previous, "AssistantPart"),
|
||||
spacing: Predicate.isTagged(previous, "AssistantPart") ? "tool" : undefined,
|
||||
spacing:
|
||||
Predicate.isTagged(previous, "AssistantPart") || Predicate.isTagged(previous, "Error")
|
||||
? "tool"
|
||||
: undefined,
|
||||
group: {
|
||||
type: "context",
|
||||
key: `message:${row.messageID}`,
|
||||
|
||||
@@ -213,43 +213,45 @@ export function createSessionTimelineRowRenderer(input: {
|
||||
const key = () => `context:${row().group.key}`
|
||||
|
||||
return (
|
||||
<SessionContextToolGroup
|
||||
parts={parts()}
|
||||
patchGroupKey={(tools) => {
|
||||
const parts = tools.map((tool) => patchPartKeys.get(tool)!)
|
||||
<div class="contents" data-notices-only={parts().every((part) => part.type === "notice") ? "" : undefined}>
|
||||
<SessionContextToolGroup
|
||||
parts={parts()}
|
||||
patchGroupKey={(tools) => {
|
||||
const parts = tools.map((tool) => patchPartKeys.get(tool)!)
|
||||
|
||||
// After a split, only the subgroup with the earliest surviving member keeps the old anchor.
|
||||
const key =
|
||||
parts
|
||||
.map((part) => patchGroupKeys.get(part))
|
||||
.find((key) => key !== undefined && parts.includes(patchOwners().get(key)!)) ?? parts[0]!
|
||||
// After a split, only the subgroup with the earliest surviving member keeps the old anchor.
|
||||
const key =
|
||||
parts
|
||||
.map((part) => patchGroupKeys.get(part))
|
||||
.find((key) => key !== undefined && parts.includes(patchOwners().get(key)!)) ?? parts[0]!
|
||||
|
||||
parts.forEach((part) => patchGroupKeys.set(part, key))
|
||||
parts.forEach((part) => patchGroupKeys.set(part, key))
|
||||
|
||||
return key
|
||||
}}
|
||||
reasoningDefaultOpen={
|
||||
input.timelineDetail
|
||||
? input.timelineDetail().thinking.details === "expanded"
|
||||
: input.reasoningMode() === "full"
|
||||
}
|
||||
reasoningOpen={(id) => input.disclosure.value(id)}
|
||||
onReasoningOpenChange={(id, open) => input.disclosure.set(id, open)}
|
||||
toolDefaultOpen={(tool) => (input.timelineDetail ? contentDefaultOpen(tool) : false)}
|
||||
toolOpen={(id) => input.disclosure.value(`${row().group.key}:tool:${id}`)}
|
||||
onToolOpenChange={(id, open) => input.disclosure.set(`${row().group.key}:tool:${id}`, open)}
|
||||
fileOpen={(path) =>
|
||||
input.disclosure.value(`patch:${path}`) ?? input.timelineDetail?.().edit.details === "expanded"
|
||||
}
|
||||
onFileOpenChange={(path, open) => input.disclosure.set(`patch:${path}`, open)}
|
||||
open={input.disclosure.value(key()) === true}
|
||||
busy={
|
||||
workingTurn(row().userMessageID) &&
|
||||
input.projection.lastAssistantGroupKey().get(row().userMessageID) === row().group.key
|
||||
}
|
||||
onOpenChange={(open) => input.disclosure.set(key(), open)}
|
||||
onSizeChange={onSizeChange}
|
||||
/>
|
||||
return key
|
||||
}}
|
||||
reasoningDefaultOpen={
|
||||
input.timelineDetail
|
||||
? input.timelineDetail().thinking.details === "expanded"
|
||||
: input.reasoningMode() === "full"
|
||||
}
|
||||
reasoningOpen={(id) => input.disclosure.value(id)}
|
||||
onReasoningOpenChange={(id, open) => input.disclosure.set(id, open)}
|
||||
toolDefaultOpen={(tool) => (input.timelineDetail ? contentDefaultOpen(tool) : false)}
|
||||
toolOpen={(id) => input.disclosure.value(`${row().group.key}:tool:${id}`)}
|
||||
onToolOpenChange={(id, open) => input.disclosure.set(`${row().group.key}:tool:${id}`, open)}
|
||||
fileOpen={(path) =>
|
||||
input.disclosure.value(`patch:${path}`) ?? input.timelineDetail?.().edit.details === "expanded"
|
||||
}
|
||||
onFileOpenChange={(path, open) => input.disclosure.set(`patch:${path}`, open)}
|
||||
open={input.disclosure.value(key()) === true}
|
||||
busy={
|
||||
workingTurn(row().userMessageID) &&
|
||||
input.projection.lastAssistantGroupKey().get(row().userMessageID) === row().group.key
|
||||
}
|
||||
onOpenChange={(open) => input.disclosure.set(key(), open)}
|
||||
onSizeChange={onSizeChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -471,6 +473,7 @@ export function createSessionTimelineRowRenderer(input: {
|
||||
"md:max-w-[1000px] md:mx-auto": input.centered?.(),
|
||||
"pt-2": Predicate.isTagged(props.row, "AssistantPart") && props.row.spacing === "tool",
|
||||
"pt-4": Predicate.isTagged(props.row, "AssistantPart") && props.row.spacing === "content",
|
||||
"pt-6": Predicate.isTagged(props.row, "Error"),
|
||||
}}
|
||||
>
|
||||
<div data-component="session-turn" class="min-w-0 w-full relative" style={{ height: "auto" }}>
|
||||
|
||||
Reference in New Issue
Block a user