mirror of
https://github.com/anomalyco/opencode.git
synced 2026-10-04 14:36:18 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b4e52f5df | ||
|
|
713a49bf75 |
No files matched your search
@@ -0,0 +1,47 @@
|
||||
import { expect, story } from "../../storybook/playwright/story"
|
||||
|
||||
story("updates the timeline from the production slider and advanced settings", async ({ mount }) => {
|
||||
const component = await mount("app-real-timeline--preset-explorer")
|
||||
const slider = component.getByRole("slider", { name: "Timeline detail" })
|
||||
|
||||
await expect(slider).toHaveAttribute("aria-valuetext", "Compact")
|
||||
await expect(component.getByText(/^Used 10 /)).toBeVisible()
|
||||
await slider.press("End")
|
||||
await expect(slider).toHaveAttribute("aria-valuetext", "Everything")
|
||||
await expect(component.getByText(/^Used 10 /)).toHaveCount(0)
|
||||
|
||||
await component.getByRole("button", { name: "Advanced", exact: true }).click()
|
||||
const shellGrouped = component.getByRole("switch", { name: "Shell grouped", exact: true })
|
||||
await expect(shellGrouped).not.toBeChecked()
|
||||
await shellGrouped.press("Space")
|
||||
await expect(slider).toHaveAttribute("aria-valuetext", "Custom")
|
||||
await expect(component.getByText(/^Used /)).toHaveCount(2)
|
||||
})
|
||||
|
||||
story("renders a representative session with the desktop compact preset", async ({ mount }) => {
|
||||
const component = await mount("app-real-timeline--representative-compact")
|
||||
|
||||
await expect(
|
||||
component.getByText(/Used 10 Skill, Read, Grep, Glob, Execute, Shell, Web Search, Webfetch, Patch, Agent/),
|
||||
).toBeVisible()
|
||||
await expect(component.getByRole("img", { name: "lorem-layout.png" })).toBeVisible()
|
||||
await expect(component.locator('[data-component="session-timeline"]')).toContainText("Lorem ipsum")
|
||||
})
|
||||
|
||||
story("renders every server message family with the everything preset", async ({ mount }) => {
|
||||
const component = await mount("app-real-timeline--all-server-parts-everything")
|
||||
|
||||
await expect(component.getByText("Build → Review", { exact: true })).toBeVisible()
|
||||
await expect(component.getByText("Switched to ipsum-3", { exact: true })).toBeVisible()
|
||||
await expect(component.getByText("/workspace/lorem/ipsum/dolor/sit/amet", { exact: true })).toBeVisible()
|
||||
await expect(component.getByText("Command timed out", { exact: true })).toBeVisible()
|
||||
await expect(component.getByText("Session compaction started", { exact: true })).toHaveCount(4)
|
||||
})
|
||||
|
||||
story("keeps failures and required notices visible in text-only mode", async ({ mount }) => {
|
||||
const component = await mount("app-real-timeline--failures-text-only")
|
||||
|
||||
await expect(component.getByText("Grep", { exact: true })).toBeVisible()
|
||||
await expect(component.getByText("todo", { exact: true })).toBeVisible()
|
||||
await expect(component.getByText("Shell finished · Background shell failed", { exact: true })).toBeVisible()
|
||||
})
|
||||
@@ -0,0 +1,777 @@
|
||||
import type {
|
||||
JsonValue,
|
||||
SessionMessageAssistant,
|
||||
SessionMessageAssistantTool,
|
||||
SessionMessageInfo,
|
||||
SessionMessageUser,
|
||||
} from "@opencode/client/promise"
|
||||
import type { SessionDocument } from "@opencode/session-ui/document"
|
||||
import type { SessionUserPresentation } from "@opencode/session-ui/timeline"
|
||||
|
||||
const TIME = 1_789_171_200_000
|
||||
const MODEL = { providerID: "lorem", id: "ipsum-2", variant: "balanced" }
|
||||
const LOREM =
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. "
|
||||
|
||||
function lorem(length: number) {
|
||||
return LOREM.repeat(Math.ceil(length / LOREM.length))
|
||||
.slice(0, length)
|
||||
.trimEnd()
|
||||
}
|
||||
|
||||
function output(length: number) {
|
||||
const line = `${lorem(72)}\n`
|
||||
return line
|
||||
.repeat(Math.ceil(length / line.length))
|
||||
.slice(0, length)
|
||||
.trimEnd()
|
||||
}
|
||||
|
||||
function user(id: string, text: string, offset: number): SessionMessageUser {
|
||||
return {
|
||||
id,
|
||||
type: "user",
|
||||
text,
|
||||
files: [],
|
||||
agents: [],
|
||||
skills: [],
|
||||
time: { created: TIME + offset },
|
||||
metadata: { agent: "build", model: MODEL },
|
||||
}
|
||||
}
|
||||
|
||||
function assistant(input: {
|
||||
id: string
|
||||
offset: number
|
||||
content: SessionMessageAssistant["content"]
|
||||
finish?: SessionMessageAssistant["finish"]
|
||||
error?: SessionMessageAssistant["error"]
|
||||
retry?: SessionMessageAssistant["retry"]
|
||||
completed?: boolean
|
||||
}): SessionMessageAssistant {
|
||||
return {
|
||||
id: input.id,
|
||||
type: "assistant",
|
||||
agent: "build",
|
||||
model: MODEL,
|
||||
content: input.content,
|
||||
finish: input.finish,
|
||||
error: input.error,
|
||||
retry: input.retry,
|
||||
time: {
|
||||
created: TIME + input.offset,
|
||||
...(input.completed === false ? {} : { completed: TIME + input.offset + 1_200 }),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function reasoning(text: string, offset: number) {
|
||||
return {
|
||||
type: "reasoning",
|
||||
text,
|
||||
time: { created: TIME + offset, completed: TIME + offset + 500 },
|
||||
} satisfies SessionMessageAssistant["content"][number]
|
||||
}
|
||||
|
||||
function completedTool(input: {
|
||||
id: string
|
||||
name: string
|
||||
offset: number
|
||||
args: Record<string, JsonValue>
|
||||
result: string
|
||||
metadata?: Record<string, JsonValue>
|
||||
files?: { uri: string; mime: string; name?: string }[]
|
||||
}): SessionMessageAssistantTool {
|
||||
return {
|
||||
type: "tool",
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
state: {
|
||||
status: "completed",
|
||||
input: input.args,
|
||||
content: [
|
||||
{ type: "text", text: input.result },
|
||||
...(input.files ?? []).map((file) => ({ type: "file" as const, ...file })),
|
||||
],
|
||||
metadata: input.metadata,
|
||||
},
|
||||
time: { created: TIME + input.offset, ran: TIME + input.offset + 100, completed: TIME + input.offset + 900 },
|
||||
}
|
||||
}
|
||||
|
||||
function errorTool(input: {
|
||||
id: string
|
||||
name: string
|
||||
offset: number
|
||||
args: Record<string, JsonValue>
|
||||
message: string
|
||||
metadata?: Record<string, JsonValue>
|
||||
result?: string
|
||||
}): SessionMessageAssistantTool {
|
||||
return {
|
||||
type: "tool",
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
state: {
|
||||
status: "error",
|
||||
input: input.args,
|
||||
error: { type: "ToolExecutionError", message: input.message },
|
||||
content: input.result ? [{ type: "text", text: input.result }] : undefined,
|
||||
metadata: input.metadata,
|
||||
},
|
||||
time: { created: TIME + input.offset, ran: TIME + input.offset + 100, completed: TIME + input.offset + 700 },
|
||||
}
|
||||
}
|
||||
|
||||
function runningTool(input: {
|
||||
id: string
|
||||
name: string
|
||||
offset: number
|
||||
args: Record<string, JsonValue>
|
||||
metadata?: Record<string, JsonValue>
|
||||
}): SessionMessageAssistantTool {
|
||||
return {
|
||||
type: "tool",
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
state: { status: "running", input: input.args, metadata: input.metadata ?? {} },
|
||||
time: { created: TIME + input.offset, ran: TIME + input.offset + 100 },
|
||||
}
|
||||
}
|
||||
|
||||
function document(messages: SessionMessageInfo[], status: SessionDocument["status"] = { type: "idle" }) {
|
||||
return { sessionID: "session_real_timeline", messages, status, diffs: [] } satisfies SessionDocument
|
||||
}
|
||||
|
||||
const representativeUser = {
|
||||
...user("msg_real_user", lorem(1_440), 1_000),
|
||||
files: [
|
||||
{
|
||||
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
||||
mime: "image/png",
|
||||
name: "lorem-layout.png",
|
||||
source: { type: "inline" },
|
||||
},
|
||||
],
|
||||
agents: [{ name: "review", mention: { text: "@review", start: 0, end: 7 } }],
|
||||
} satisfies SessionMessageInfo
|
||||
|
||||
export const realTimelineDocument = document([
|
||||
representativeUser,
|
||||
assistant({
|
||||
id: "msg_real_skill",
|
||||
offset: 3_000,
|
||||
finish: "tool-calls",
|
||||
content: [
|
||||
reasoning(lorem(210), 3_050),
|
||||
completedTool({
|
||||
id: "tool_real_skill",
|
||||
name: "skill",
|
||||
offset: 3_300,
|
||||
args: { name: "lorem-guidance" },
|
||||
result: output(12_800),
|
||||
metadata: { name: "lorem-guidance" },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
assistant({
|
||||
id: "msg_real_read",
|
||||
offset: 5_000,
|
||||
finish: "tool-calls",
|
||||
content: [
|
||||
reasoning(lorem(190), 5_050),
|
||||
completedTool({
|
||||
id: "tool_real_read",
|
||||
name: "read",
|
||||
offset: 5_300,
|
||||
args: { path: "src/lorem/timeline-controller.tsx", offset: 1, limit: 240 },
|
||||
result: output(21_700),
|
||||
metadata: { loaded: ["src/lorem/timeline-controller.tsx"] },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
assistant({
|
||||
id: "msg_real_search",
|
||||
offset: 7_000,
|
||||
finish: "tool-calls",
|
||||
content: [
|
||||
reasoning(lorem(205), 7_050),
|
||||
completedTool({
|
||||
id: "tool_real_grep",
|
||||
name: "grep",
|
||||
offset: 7_300,
|
||||
args: { pattern: "lorem", path: "src", include: "*.tsx" },
|
||||
result: output(3_500),
|
||||
metadata: { matches: 18 },
|
||||
}),
|
||||
completedTool({
|
||||
id: "tool_real_glob",
|
||||
name: "glob",
|
||||
offset: 7_500,
|
||||
args: { pattern: "src/**/*.tsx" },
|
||||
result: output(1_800),
|
||||
metadata: { count: 42 },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
assistant({
|
||||
id: "msg_real_code",
|
||||
offset: 9_000,
|
||||
finish: "tool-calls",
|
||||
content: [
|
||||
reasoning(lorem(180), 9_050),
|
||||
completedTool({
|
||||
id: "tool_real_execute",
|
||||
name: "execute",
|
||||
offset: 9_300,
|
||||
args: { code: "const result = await tools.lorem.inspect({ limit: 24 })\nreturn result" },
|
||||
result: output(6_300),
|
||||
metadata: {
|
||||
toolCalls: [
|
||||
{ name: "lorem.inspect", status: "completed" },
|
||||
{ name: "lorem.measure", status: "completed" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
assistant({
|
||||
id: "msg_real_shell",
|
||||
offset: 11_000,
|
||||
finish: "tool-calls",
|
||||
content: [
|
||||
reasoning(lorem(195), 11_050),
|
||||
completedTool({
|
||||
id: "tool_real_shell",
|
||||
name: "shell",
|
||||
offset: 11_300,
|
||||
args: { command: "bun test src/lorem/timeline.test.ts" },
|
||||
result: output(4_700),
|
||||
metadata: { exit: 0 },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
assistant({
|
||||
id: "msg_real_web",
|
||||
offset: 13_000,
|
||||
finish: "tool-calls",
|
||||
content: [
|
||||
reasoning(lorem(200), 13_050),
|
||||
completedTool({
|
||||
id: "tool_real_websearch",
|
||||
name: "websearch",
|
||||
offset: 13_300,
|
||||
args: { query: "lorem ipsum timeline guidance" },
|
||||
result: output(16_500),
|
||||
metadata: { provider: "lorem" },
|
||||
}),
|
||||
completedTool({
|
||||
id: "tool_real_webfetch",
|
||||
name: "webfetch",
|
||||
offset: 13_500,
|
||||
args: { url: "https://example.com/lorem" },
|
||||
result: output(15_100),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
{
|
||||
id: "msg_real_system",
|
||||
type: "system",
|
||||
text: lorem(920),
|
||||
description: "Instructions updated: lorem/timeline",
|
||||
time: { created: TIME + 15_000 },
|
||||
},
|
||||
assistant({
|
||||
id: "msg_real_edit",
|
||||
offset: 16_000,
|
||||
finish: "tool-calls",
|
||||
content: [
|
||||
reasoning(lorem(185), 16_050),
|
||||
completedTool({
|
||||
id: "tool_real_patch",
|
||||
name: "patch",
|
||||
offset: 16_300,
|
||||
args: { patchText: lorem(3_200) },
|
||||
result: lorem(190),
|
||||
metadata: {
|
||||
files: [
|
||||
{
|
||||
file: "src/lorem/real-timeline.tsx",
|
||||
status: "modified",
|
||||
patch: "@@ -1 +1 @@\n-export const lorem = false\n+export const lorem = true",
|
||||
additions: 1,
|
||||
deletions: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
assistant({
|
||||
id: "msg_real_subagent",
|
||||
offset: 18_000,
|
||||
finish: "tool-calls",
|
||||
content: [
|
||||
reasoning(lorem(200), 18_050),
|
||||
completedTool({
|
||||
id: "tool_real_subagent",
|
||||
name: "subagent",
|
||||
offset: 18_300,
|
||||
args: { agent: "explore", description: "Lorem ipsum audit", prompt: lorem(1_400) },
|
||||
result: lorem(480),
|
||||
metadata: { sessionID: "session_lorem_child", status: "completed" },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
{
|
||||
id: "msg_real_background_complete",
|
||||
type: "synthetic",
|
||||
text: lorem(8_900),
|
||||
description: "Background task “Lorem ipsum audit” completed",
|
||||
metadata: {
|
||||
source: "subagent",
|
||||
state: "completed",
|
||||
childID: "session_lorem_child",
|
||||
agent: "explore",
|
||||
},
|
||||
time: { created: TIME + 20_000 },
|
||||
},
|
||||
assistant({
|
||||
id: "msg_real_result",
|
||||
offset: 21_000,
|
||||
finish: "stop",
|
||||
content: [reasoning(lorem(160), 21_050), { type: "text", text: lorem(560) }],
|
||||
}),
|
||||
{ id: "msg_real_idle", type: "idle", outcome: "succeeded", time: { created: TIME + 23_000 } },
|
||||
] satisfies SessionMessageInfo[])
|
||||
|
||||
export const realTimelinePresentation = {
|
||||
msg_real_user: {
|
||||
displayText: lorem(420),
|
||||
comments: [
|
||||
{
|
||||
path: "src/lorem/timeline-controller.tsx",
|
||||
comment: lorem(180),
|
||||
selection: { startLine: 118, endLine: 126 },
|
||||
},
|
||||
],
|
||||
},
|
||||
} satisfies Record<string, SessionUserPresentation>
|
||||
|
||||
const coveragePrelude = [
|
||||
user("msg_coverage_user", lorem(620), 30_000),
|
||||
{
|
||||
id: "msg_coverage_agent",
|
||||
type: "agent-switched",
|
||||
agent: "review",
|
||||
previous: "build",
|
||||
time: { created: TIME + 31_000 },
|
||||
},
|
||||
{
|
||||
id: "msg_coverage_model",
|
||||
type: "model-switched",
|
||||
model: { providerID: "lorem", id: "ipsum-3" },
|
||||
previous: MODEL,
|
||||
time: { created: TIME + 31_500 },
|
||||
},
|
||||
{
|
||||
id: "msg_coverage_location",
|
||||
type: "location-switched",
|
||||
location: { directory: "/workspace/lorem/ipsum/dolor/sit/amet" },
|
||||
projectID: "project_lorem",
|
||||
subpath: "packages/timeline",
|
||||
previous: {
|
||||
location: { directory: "/workspace/lorem" },
|
||||
projectID: "project_previous",
|
||||
subpath: "packages/app",
|
||||
},
|
||||
time: { created: TIME + 32_000 },
|
||||
},
|
||||
{
|
||||
id: "msg_coverage_skill",
|
||||
type: "skill",
|
||||
skill: "skill_lorem",
|
||||
name: "lorem-audit",
|
||||
text: lorem(12_800),
|
||||
time: { created: TIME + 32_500 },
|
||||
},
|
||||
{
|
||||
id: "msg_coverage_system",
|
||||
type: "system",
|
||||
text: lorem(2_400),
|
||||
description: "Lorem ipsum system notice",
|
||||
time: { created: TIME + 33_000 },
|
||||
},
|
||||
{
|
||||
id: "msg_coverage_synthetic_blank",
|
||||
type: "synthetic",
|
||||
text: lorem(240),
|
||||
description: " ",
|
||||
time: { created: TIME + 33_500 },
|
||||
},
|
||||
] satisfies SessionMessageInfo[]
|
||||
|
||||
export const realTimelineCoverageDocument = document([
|
||||
...coveragePrelude,
|
||||
{
|
||||
id: "msg_coverage_shell_success",
|
||||
type: "shell",
|
||||
shellID: "shell_lorem_success",
|
||||
command: "bun test src/lorem",
|
||||
status: "exited",
|
||||
exit: 0,
|
||||
output: { output: output(4_700), cursor: 4_700, size: 4_700, truncated: false },
|
||||
time: { created: TIME + 34_000, completed: TIME + 35_000 },
|
||||
},
|
||||
{
|
||||
id: "msg_coverage_shell_timeout",
|
||||
type: "shell",
|
||||
shellID: "shell_lorem_timeout",
|
||||
command: "bun run lorem:slow",
|
||||
status: "timeout",
|
||||
exit: 124,
|
||||
output: { output: output(2_000), cursor: 2_000, size: 2_000, truncated: true },
|
||||
time: { created: TIME + 36_000, completed: TIME + 38_000 },
|
||||
},
|
||||
{
|
||||
id: "msg_coverage_shell_killed",
|
||||
type: "shell",
|
||||
shellID: "shell_lorem_killed",
|
||||
command: "bun run lorem:cancelled",
|
||||
status: "killed",
|
||||
exit: 137,
|
||||
output: { output: output(1_200), cursor: 1_200, size: 1_200, truncated: false },
|
||||
time: { created: TIME + 38_100, completed: TIME + 38_600 },
|
||||
},
|
||||
{
|
||||
id: "msg_coverage_shell_failed",
|
||||
type: "shell",
|
||||
shellID: "shell_lorem_failed",
|
||||
command: "bun test src/lorem/failing.test.ts",
|
||||
status: "exited",
|
||||
exit: 1,
|
||||
output: { output: output(1_800), cursor: 1_800, size: 1_800, truncated: false },
|
||||
time: { created: TIME + 38_700, completed: TIME + 38_900 },
|
||||
},
|
||||
assistant({
|
||||
id: "msg_coverage_streaming",
|
||||
offset: 39_000,
|
||||
completed: false,
|
||||
content: [
|
||||
{ type: "reasoning", text: lorem(200), time: { created: TIME + 39_050 } },
|
||||
{
|
||||
type: "tool",
|
||||
id: "tool_coverage_streaming",
|
||||
name: "webfetch",
|
||||
state: { status: "streaming", input: '{"url":"https://example.com/lorem' },
|
||||
time: { created: TIME + 39_300 },
|
||||
},
|
||||
],
|
||||
}),
|
||||
assistant({
|
||||
id: "msg_coverage_running",
|
||||
offset: 41_000,
|
||||
completed: false,
|
||||
content: [
|
||||
runningTool({
|
||||
id: "tool_coverage_background_shell",
|
||||
name: "shell",
|
||||
offset: 41_300,
|
||||
args: { command: "bun run lorem:background" },
|
||||
metadata: { shellID: "shell_lorem_background", status: "running", output: output(900) },
|
||||
}),
|
||||
runningTool({
|
||||
id: "tool_coverage_background_agent",
|
||||
name: "subagent",
|
||||
offset: 41_500,
|
||||
args: { agent: "general", description: "Lorem background", prompt: lorem(700) },
|
||||
metadata: { sessionID: "session_lorem_background", status: "running" },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
assistant({
|
||||
id: "msg_coverage_question",
|
||||
offset: 43_000,
|
||||
finish: "tool-calls",
|
||||
content: [
|
||||
completedTool({
|
||||
id: "tool_coverage_question",
|
||||
name: "question",
|
||||
offset: 43_300,
|
||||
args: { questions: [{ header: "Lorem", question: lorem(120), options: ["Ipsum", "Dolor"] }] },
|
||||
result: "Ipsum",
|
||||
metadata: { answers: [["Ipsum"]] },
|
||||
}),
|
||||
completedTool({
|
||||
id: "tool_coverage_file_result",
|
||||
name: "browser",
|
||||
offset: 43_500,
|
||||
args: { action: "screenshot" },
|
||||
result: lorem(120),
|
||||
files: [{ uri: "data:image/png;base64,iVBORw0KGgo=", mime: "image/png", name: "lorem.png" }],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
assistant({
|
||||
id: "msg_coverage_native",
|
||||
offset: 45_000,
|
||||
finish: "tool-calls",
|
||||
content: [
|
||||
completedTool({
|
||||
id: "tool_coverage_native_read",
|
||||
name: "read",
|
||||
offset: 45_200,
|
||||
args: { path: "src/lorem/native.ts" },
|
||||
result: output(2_200),
|
||||
}),
|
||||
completedTool({
|
||||
id: "tool_coverage_native_shell",
|
||||
name: "shell",
|
||||
offset: 45_400,
|
||||
args: { command: "bun test src/lorem/native.test.ts" },
|
||||
result: "1 pass\n0 fail",
|
||||
metadata: { exit: 0 },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
assistant({
|
||||
id: "msg_coverage_code_mode",
|
||||
offset: 47_000,
|
||||
finish: "tool-calls",
|
||||
content: [
|
||||
completedTool({
|
||||
id: "tool_coverage_code_mode",
|
||||
name: "execute",
|
||||
offset: 47_300,
|
||||
args: {
|
||||
code: "const data = await tools.read({ path: 'src/lorem/code.ts' })\nreturn tools.shell({ command: 'bun test' })",
|
||||
},
|
||||
result: output(6_300),
|
||||
metadata: {
|
||||
toolCalls: [
|
||||
{ name: "read", status: "completed" },
|
||||
{ name: "shell", status: "error" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
assistant({
|
||||
id: "msg_coverage_tool_error",
|
||||
offset: 49_000,
|
||||
finish: "tool-calls",
|
||||
content: [
|
||||
errorTool({
|
||||
id: "tool_coverage_error",
|
||||
name: "mcp_lorem_lookup",
|
||||
offset: 49_300,
|
||||
args: { query: lorem(160) },
|
||||
message: lorem(320),
|
||||
result: lorem(180),
|
||||
metadata: { provider: "lorem" },
|
||||
}),
|
||||
errorTool({
|
||||
id: "tool_coverage_todo_error",
|
||||
name: "todo",
|
||||
offset: 49_500,
|
||||
args: { todos: [{ content: lorem(80), status: "pending" }] },
|
||||
message: lorem(240),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
{
|
||||
id: "msg_coverage_shell_notice",
|
||||
type: "synthetic",
|
||||
text: output(8_900),
|
||||
description: "Background shell “Lorem ipsum” failed",
|
||||
metadata: { source: "shell", state: "completed", shellID: "shell_lorem_background", exit: 1, truncated: false },
|
||||
time: { created: TIME + 51_000 },
|
||||
},
|
||||
{
|
||||
id: "msg_coverage_subagent_cancelled",
|
||||
type: "synthetic",
|
||||
text: lorem(1_200),
|
||||
description: "Background task “Lorem cancelled” was cancelled",
|
||||
metadata: { source: "subagent", state: "cancelled", childID: "session_lorem_cancelled", agent: "general" },
|
||||
time: { created: TIME + 51_200 },
|
||||
},
|
||||
{
|
||||
id: "msg_coverage_subagent_error",
|
||||
type: "synthetic",
|
||||
text: lorem(1_400),
|
||||
description: "Background task “Lorem failed” failed",
|
||||
metadata: { source: "subagent", state: "error", childID: "session_lorem_failed", agent: "general" },
|
||||
time: { created: TIME + 51_400 },
|
||||
},
|
||||
assistant({
|
||||
id: "msg_coverage_interrupted",
|
||||
offset: 52_000,
|
||||
finish: "error",
|
||||
error: { type: "ExecutionInterrupted", message: "Lorem ipsum interrupted" },
|
||||
content: [{ type: "text", text: lorem(280) }],
|
||||
}),
|
||||
assistant({
|
||||
id: "msg_coverage_error",
|
||||
offset: 54_000,
|
||||
finish: "error",
|
||||
error: { type: "provider.invalid-output", message: lorem(360) },
|
||||
content: [],
|
||||
}),
|
||||
assistant({
|
||||
id: "msg_coverage_retry",
|
||||
offset: 56_000,
|
||||
completed: false,
|
||||
retry: {
|
||||
attempt: 3,
|
||||
at: TIME + 56_500,
|
||||
error: { type: "provider.internal", message: lorem(420) },
|
||||
},
|
||||
content: [],
|
||||
}),
|
||||
{
|
||||
id: "msg_coverage_compaction_running",
|
||||
type: "compaction",
|
||||
status: "running",
|
||||
reason: "auto",
|
||||
summary: lorem(18_000),
|
||||
recent: lorem(107_000),
|
||||
time: { created: TIME + 58_000 },
|
||||
},
|
||||
{
|
||||
id: "msg_coverage_compaction_complete",
|
||||
type: "compaction",
|
||||
status: "completed",
|
||||
reason: "manual",
|
||||
model: MODEL,
|
||||
summary: lorem(18_000),
|
||||
recent: lorem(107_000),
|
||||
providerContext: {
|
||||
version: 1,
|
||||
provenance: {
|
||||
providerID: "lorem",
|
||||
provider: "Lorem",
|
||||
modelID: "ipsum-2",
|
||||
route: "lorem-route",
|
||||
protocol: "lorem-protocol",
|
||||
endpoint: "lorem-endpoint-digest",
|
||||
},
|
||||
messages: [],
|
||||
},
|
||||
cost: 0.012,
|
||||
tokens: { input: 3_200, output: 420, reasoning: 90, cache: { read: 8_700, write: 0 } },
|
||||
time: { created: TIME + 60_000 },
|
||||
},
|
||||
{
|
||||
id: "msg_coverage_compaction_failed",
|
||||
type: "compaction",
|
||||
status: "failed",
|
||||
reason: "auto",
|
||||
error: { type: "compaction.failed", message: lorem(320) },
|
||||
cost: 0.004,
|
||||
tokens: { input: 1_200, output: 90, reasoning: 20, cache: { read: 2_100, write: 0 } },
|
||||
time: { created: TIME + 60_500 },
|
||||
},
|
||||
{
|
||||
id: "msg_coverage_compaction_interrupted",
|
||||
type: "compaction",
|
||||
status: "failed",
|
||||
reason: "manual",
|
||||
error: { type: "compaction.interrupted", message: "Lorem ipsum compaction interrupted" },
|
||||
time: { created: TIME + 61_000 },
|
||||
},
|
||||
assistant({
|
||||
id: "msg_coverage_recovered",
|
||||
offset: 62_000,
|
||||
finish: "stop",
|
||||
content: [{ type: "text", text: lorem(560) }],
|
||||
}),
|
||||
{ id: "msg_coverage_idle", type: "idle", outcome: "interrupted", time: { created: TIME + 64_000 } },
|
||||
] satisfies SessionMessageInfo[])
|
||||
|
||||
export const realTimelineFailureDocument = document([
|
||||
user("msg_failure_user", lorem(420), 70_000),
|
||||
assistant({
|
||||
id: "msg_failure_tools",
|
||||
offset: 71_000,
|
||||
finish: "tool-calls",
|
||||
content: [
|
||||
errorTool({
|
||||
id: "tool_failure_hidden",
|
||||
name: "grep",
|
||||
offset: 71_200,
|
||||
args: { pattern: "lorem", path: "src" },
|
||||
message: lorem(360),
|
||||
}),
|
||||
errorTool({
|
||||
id: "tool_failure_todo",
|
||||
name: "todo",
|
||||
offset: 71_400,
|
||||
args: { todos: [{ content: lorem(120), status: "pending" }] },
|
||||
message: lorem(280),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
{
|
||||
id: "msg_failure_required_notice",
|
||||
type: "synthetic",
|
||||
text: lorem(900),
|
||||
description: "Background shell failed",
|
||||
metadata: { source: "shell", state: "completed", shellID: "shell_failure", exit: 1 },
|
||||
time: { created: TIME + 73_000 },
|
||||
},
|
||||
assistant({
|
||||
id: "msg_failure_assistant",
|
||||
offset: 74_000,
|
||||
finish: "error",
|
||||
error: { type: "provider.invalid-output", message: lorem(420) },
|
||||
content: [],
|
||||
}),
|
||||
] satisfies SessionMessageInfo[])
|
||||
|
||||
export const realTimelineVerboseDocument = document([
|
||||
{
|
||||
...user("msg_verbose_user", lorem(24_500), 80_000),
|
||||
files: [
|
||||
{
|
||||
data: "IyBMb3JlbSBpcHN1bQ==",
|
||||
mime: "text/markdown",
|
||||
name: `${lorem(110).replaceAll(" ", "-")}.md`,
|
||||
source: { type: "inline" },
|
||||
},
|
||||
{
|
||||
data: "JVBERi0xLjQK",
|
||||
mime: "application/pdf",
|
||||
name: "lorem-verbose.pdf",
|
||||
source: { type: "inline" },
|
||||
},
|
||||
],
|
||||
},
|
||||
assistant({
|
||||
id: "msg_verbose_assistant",
|
||||
offset: 82_000,
|
||||
finish: "tool-calls",
|
||||
content: [
|
||||
reasoning(lorem(1_100), 82_050),
|
||||
completedTool({
|
||||
id: "tool_verbose_read",
|
||||
name: "read",
|
||||
offset: 82_300,
|
||||
args: { path: `/workspace/${lorem(220).replaceAll(" ", "/")}/timeline.tsx`, offset: 1, limit: 2_000 },
|
||||
result: output(106_000),
|
||||
}),
|
||||
errorTool({
|
||||
id: "tool_verbose_error",
|
||||
name: "mcp_lorem_extremely_verbose_tool_name_for_overflow_validation",
|
||||
offset: 82_500,
|
||||
args: { query: lorem(500) },
|
||||
message: lorem(670),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
assistant({
|
||||
id: "msg_verbose_result",
|
||||
offset: 84_000,
|
||||
finish: "stop",
|
||||
content: [{ type: "text", text: lorem(28_000) }],
|
||||
}),
|
||||
] satisfies SessionMessageInfo[])
|
||||
@@ -0,0 +1,150 @@
|
||||
import { timelinePresets, type TimelineDetail } from "@opencode/session-ui/timeline/detail"
|
||||
import { Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { SettingsList } from "@/settings/list"
|
||||
import { TimelineDetailControl } from "@/settings/timeline-detail"
|
||||
import { SessionPreview } from "./story-model"
|
||||
import {
|
||||
realTimelineCoverageDocument,
|
||||
realTimelineDocument,
|
||||
realTimelineFailureDocument,
|
||||
realTimelinePresentation,
|
||||
realTimelineVerboseDocument,
|
||||
} from "./real-timeline-fixtures"
|
||||
import "@/settings/settings.css"
|
||||
|
||||
const description = "anonymized seven-day session shape · lorem ipsum content"
|
||||
const presets = Object.fromEntries(timelinePresets.map((preset) => [preset.id, preset.value])) as Record<
|
||||
string,
|
||||
TimelineDetail
|
||||
>
|
||||
|
||||
function Representative(props: { preset: string }) {
|
||||
return (
|
||||
<SessionPreview
|
||||
title="Real Timeline"
|
||||
description={description}
|
||||
document={realTimelineDocument}
|
||||
presentation={realTimelinePresentation}
|
||||
timelineDetail={presets[props.preset] ?? timelinePresets[2].value}
|
||||
draft="Lorem ipsum dolor sit amet"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TimelineSettingsPlayground() {
|
||||
const [state, setState] = createStore({ value: structuredClone(timelinePresets[2].value) as TimelineDetail })
|
||||
|
||||
return (
|
||||
<div class="flex h-screen min-h-[640px] w-full flex-col bg-background-base lg:flex-row">
|
||||
<aside class="max-h-[46vh] w-full shrink-0 overflow-y-auto border-b border-border-weak-base bg-background-stronger p-4 lg:max-h-none lg:w-[420px] lg:border-b-0 lg:border-r">
|
||||
<SettingsList>
|
||||
<div class="py-5">
|
||||
<TimelineDetailControl value={state.value} onChange={(value) => setState("value", value)} />
|
||||
</div>
|
||||
</SettingsList>
|
||||
</aside>
|
||||
<section class="min-h-0 min-w-0 flex-1">
|
||||
<Show when={state.value} keyed>
|
||||
{(timelineDetail) => (
|
||||
<SessionPreview
|
||||
title="Real Timeline · settings playground"
|
||||
description={description}
|
||||
document={realTimelineDocument}
|
||||
presentation={realTimelinePresentation}
|
||||
timelineDetail={timelineDetail}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: "OpenCode/Session/Real Timeline",
|
||||
id: "app-real-timeline",
|
||||
component: SessionPreview,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Production Session timeline rendering with synthetic Lorem ipsum payloads shaped from a seven-day aggregate of local Session data. No original prompts, outputs, paths, URLs, or identifiers are retained.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const PresetExplorer = {
|
||||
render: () => <TimelineSettingsPlayground />,
|
||||
}
|
||||
|
||||
export const RepresentativeCompact = {
|
||||
render: () => <Representative preset="compact" />,
|
||||
}
|
||||
|
||||
export const RepresentativeEverything = {
|
||||
render: () => <Representative preset="everything" />,
|
||||
}
|
||||
|
||||
export const AllServerPartsEverything = {
|
||||
render: () => (
|
||||
<SessionPreview
|
||||
title="Real Timeline · all server parts"
|
||||
description={description}
|
||||
document={realTimelineCoverageDocument}
|
||||
timelineDetail={timelinePresets[0].value}
|
||||
/>
|
||||
),
|
||||
}
|
||||
|
||||
export const AllServerPartsCompact = {
|
||||
render: () => (
|
||||
<SessionPreview
|
||||
title="Real Timeline · compact grouping"
|
||||
description={description}
|
||||
document={realTimelineCoverageDocument}
|
||||
timelineDetail={timelinePresets[2].value}
|
||||
/>
|
||||
),
|
||||
}
|
||||
|
||||
export const FailuresQuiet = {
|
||||
render: () => (
|
||||
<SessionPreview
|
||||
title="Real Timeline · failures stay visible"
|
||||
description={description}
|
||||
document={realTimelineFailureDocument}
|
||||
timelineDetail={timelinePresets[3].value}
|
||||
/>
|
||||
),
|
||||
}
|
||||
|
||||
export const FailuresTextOnly = {
|
||||
render: () => (
|
||||
<SessionPreview
|
||||
title="Real Timeline · text only"
|
||||
description={description}
|
||||
document={realTimelineFailureDocument}
|
||||
timelineDetail={timelinePresets[4].value}
|
||||
/>
|
||||
),
|
||||
}
|
||||
|
||||
export const ExtremelyVerboseCompact = {
|
||||
render: () => (
|
||||
<SessionPreview
|
||||
title="Real Timeline · verbose values"
|
||||
description={description}
|
||||
document={realTimelineVerboseDocument}
|
||||
timelineDetail={timelinePresets[2].value}
|
||||
/>
|
||||
),
|
||||
}
|
||||
|
||||
export const MobileCompact = {
|
||||
globals: { viewport: { value: "mobile1", isRotated: false } },
|
||||
parameters: { viewport: { defaultViewport: "mobile1" } },
|
||||
render: () => <Representative preset="compact" />,
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
import { SessionPanelFrame, SessionRouteFrame } from "@/session/session-frame"
|
||||
import type { FormInfo, PermissionRequest, SessionStatus } from "@opencode/client/promise"
|
||||
import type { SessionDocument } from "@opencode/session-ui/document"
|
||||
import type { TimelineDetail } from "@opencode/session-ui/timeline/detail"
|
||||
import type { SessionUserPresentation } from "@opencode/session-ui/timeline"
|
||||
import { CurrentSessionProviders, STORY_MODEL } from "@opencode/session-ui/storybook"
|
||||
import { SessionTimeline } from "@opencode/session-ui/timeline"
|
||||
import { SessionReviewEmptyChangesV2 } from "@opencode/session-ui/v2/session-review-empty-changes-v2"
|
||||
@@ -87,6 +89,8 @@ export type SessionPreviewProps = {
|
||||
reviewOpened?: boolean
|
||||
child?: { parentID: string }
|
||||
terminal?: { title: string; lines: string[] }
|
||||
timelineDetail?: TimelineDetail
|
||||
presentation?: Record<string, SessionUserPresentation | undefined>
|
||||
}
|
||||
|
||||
export function SessionPreview(props: SessionPreviewProps) {
|
||||
@@ -259,6 +263,15 @@ function SessionSurfaceState(props: SessionPreviewProps & { onReset: () => void
|
||||
<div class="min-h-0 flex-1 overflow-y-auto py-6">
|
||||
<SessionTimeline
|
||||
document={props.document}
|
||||
presentation={props.presentation}
|
||||
timelineDetail={props.timelineDetail}
|
||||
reasoningMode={
|
||||
props.timelineDetail?.thinking.placement === "hidden"
|
||||
? "hidden"
|
||||
: props.timelineDetail?.thinking.details === "expanded"
|
||||
? "full"
|
||||
: "compact"
|
||||
}
|
||||
editToolDefaultOpen
|
||||
shellToolDefaultOpen
|
||||
class="mx-auto w-full max-w-[840px]"
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { SessionDocument } from "../document"
|
||||
import type { SessionUserActions } from "../actions"
|
||||
import { createReactiveTimelineProjection, TimelineRow, type ReasoningMode } from "./projection"
|
||||
import { createSessionTimelineRowRenderer, type SessionUserPresentation } from "./session-timeline-row"
|
||||
import type { TimelineDetail } from "./detail"
|
||||
|
||||
export type { SessionUserPresentation } from "./session-timeline-row"
|
||||
|
||||
@@ -12,18 +13,21 @@ export type SessionTimelineProps = {
|
||||
presentation?: Record<string, SessionUserPresentation | undefined>
|
||||
actions?: SessionUserActions
|
||||
reasoningMode?: ReasoningMode
|
||||
timelineDetail?: TimelineDetail
|
||||
shellToolDefaultOpen?: boolean
|
||||
editToolDefaultOpen?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
export function SessionTimeline(props: SessionTimelineProps) {
|
||||
const timelineDetail = props.timelineDetail
|
||||
const projection = createReactiveTimelineProjection({
|
||||
sessionMessages: () => props.document.messages,
|
||||
status: () => props.document.status,
|
||||
reasoningMode: () => props.reasoningMode ?? "compact",
|
||||
shellToolDefaultOpen: () => props.shellToolDefaultOpen ?? false,
|
||||
editToolDefaultOpen: () => props.editToolDefaultOpen ?? false,
|
||||
timelineDetail: timelineDetail ? () => timelineDetail : undefined,
|
||||
})
|
||||
const [toolOpen, setToolOpen] = createStore<Record<string, boolean | undefined>>({})
|
||||
const renderer = createSessionTimelineRowRenderer({
|
||||
@@ -35,6 +39,7 @@ export function SessionTimeline(props: SessionTimelineProps) {
|
||||
reasoningMode: () => props.reasoningMode ?? "compact",
|
||||
shellToolDefaultOpen: () => props.shellToolDefaultOpen ?? false,
|
||||
editToolDefaultOpen: () => props.editToolDefaultOpen ?? false,
|
||||
timelineDetail: timelineDetail ? () => timelineDetail : undefined,
|
||||
disclosure: {
|
||||
value: (key) => toolOpen[key],
|
||||
set: (key, open) => setToolOpen(key, open),
|
||||
|
||||
Reference in new issue
Block a user