From 80ef4f454fa98cb1368dff50dbaf9c08d92b698a Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Sat, 22 Aug 2026 15:17:11 +1000 Subject: [PATCH] feat(desktop): align loaded resource calls with figma (#44060) --- .../src/components/message-part.css | 34 +++++--- .../src/storybook/current-session-fixtures.ts | 30 +++---- packages/session-ui/src/storybook/index.ts | 2 +- .../session-ui/src/timeline/projection.ts | 8 +- .../timeline/research-and-agents.stories.tsx | 10 +-- .../src/timeline/rows-current.test.ts | 52 +++++++++++- .../session-ui/src/tools/tool-renderer.tsx | 80 +++++++++++-------- packages/ui/src/i18n/en.ts | 2 + 8 files changed, 147 insertions(+), 71 deletions(-) diff --git a/packages/session-ui/src/components/message-part.css b/packages/session-ui/src/components/message-part.css index 38c5a5f7aa..6515d2fa83 100644 --- a/packages/session-ui/src/components/message-part.css +++ b/packages/session-ui/src/components/message-part.css @@ -1338,20 +1338,36 @@ } } -[data-component="tool-loaded-file"] { +[data-component="tool-loaded-item"] { display: flex; align-items: center; - gap: 8px; - padding: 4px 0 4px 28px; + gap: 6px; + min-width: 0; + max-width: 100%; font-family: var(--font-family-sans); - font-size: var(--font-size-small); - font-weight: var(--font-weight-regular); - line-height: var(--line-height-large); - color: var(--v2-text-text-muted); + font-size: 13px; + line-height: 13px; + letter-spacing: -0.04px; + color: var(--v2-text-text-base); - [data-slot="icon-svg"] { + [data-slot="tool-loaded-label"] { flex-shrink: 0; - color: var(--icon-weak); + font-weight: 530; + } + + [data-slot="tool-loaded-value"] { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 440; + } + + [data-slot="tool-loaded-kind"] { + flex-shrink: 0; + margin-inline-start: -2px; + font-weight: 440; + color: var(--v2-text-text-muted); } } diff --git a/packages/session-ui/src/storybook/current-session-fixtures.ts b/packages/session-ui/src/storybook/current-session-fixtures.ts index bb8c75ca7f..fa45f38e6a 100644 --- a/packages/session-ui/src/storybook/current-session-fixtures.ts +++ b/packages/session-ui/src/storybook/current-session-fixtures.ts @@ -745,33 +745,25 @@ export const webResearchDocument = document([ }), ] satisfies SessionMessageInfo[]) -export const skillWorkflowDocument = document([ - { - id: "msg_agent_switched_review", - type: "agent-switched", - agent: "review", - previous: "build", - time: { created: STORY_TIME + 78_000 }, - }, - { - id: "msg_skill_loaded_rtl", - type: "skill", - skill: "rtl-aware-development", - name: "RTL-aware development", - text: "Verify direction independently from language.", - time: { created: STORY_TIME + 78_500 }, - }, - user("msg_user_skill", "Review the mixed-direction file row before I merge it.", 79_000), +export const loadedResourcesDocument = document([ + user("msg_user_skill", "Read the project instructions, load the RTL-aware skill, and review the file row.", 79_000), assistant({ id: "msg_assistant_skill", offset: 80_000, completed: 82_000, - agent: "review", content: [ + completedTool({ + id: "tool_loaded_file", + name: "read", + offset: 80_100, + args: { path: "C:/workspaces/opencode/packages/cli/AGENTS.md" }, + output: "Project instructions loaded.", + metadata: { loaded: ["C:/workspaces/opencode/packages/cli/AGENTS.md"] }, + }), completedTool({ id: "tool_skill_rtl", name: "skill", - offset: 80_100, + offset: 80_200, args: { name: "rtl-aware-development" }, output: "Loaded RTL-aware development guidance", metadata: { name: "rtl-aware-development" }, diff --git a/packages/session-ui/src/storybook/index.ts b/packages/session-ui/src/storybook/index.ts index 7ddd8ef58b..93ea26fa94 100644 --- a/packages/session-ui/src/storybook/index.ts +++ b/packages/session-ui/src/storybook/index.ts @@ -23,7 +23,7 @@ export { retryDocument, revertDocument, reviewDiffs, - skillWorkflowDocument, + loadedResourcesDocument, standaloneShellCompletedDocument, standaloneShellRunningDocument, streamingDocument, diff --git a/packages/session-ui/src/timeline/projection.ts b/packages/session-ui/src/timeline/projection.ts index 0cce0876e7..7d70d324ea 100644 --- a/packages/session-ui/src/timeline/projection.ts +++ b/packages/session-ui/src/timeline/projection.ts @@ -449,7 +449,7 @@ function groupContent(items: { messageID: string; partID: string; content: Conte } items.forEach((item) => { - if (item.content.type === "tool" && contextTools.has(item.content.name)) { + if (item.content.type === "tool" && contextTools.has(item.content.name) && !hasLoadedFiles(item.content)) { context.push({ messageID: item.messageID, partID: item.partID }) return } @@ -464,6 +464,12 @@ function groupContent(items: { messageID: string; partID: string; content: Conte return groups } +function hasLoadedFiles(content: Extract) { + if (content.name !== "read" || content.state.status !== "completed") return false + const loaded = content.state.metadata?.loaded + return Array.isArray(loaded) && loaded.some((path) => typeof path === "string") +} + function reasoningHeading(text: string): string | undefined { const markdown = text.replace(/\r\n?/g, "\n") const html = markdown.match(/]*>([\s\S]*?)<\/h[1-6]>/i) diff --git a/packages/session-ui/src/timeline/research-and-agents.stories.tsx b/packages/session-ui/src/timeline/research-and-agents.stories.tsx index 7115103064..ec968aa9e3 100644 --- a/packages/session-ui/src/timeline/research-and-agents.stories.tsx +++ b/packages/session-ui/src/timeline/research-and-agents.stories.tsx @@ -1,7 +1,7 @@ import { CurrentSessionTimelineStory } from "../storybook/current-session-story" import { inspectAndExplainDocument, - skillWorkflowDocument, + loadedResourcesDocument, subagentDocument, webResearchDocument, } from "../storybook/current-session-fixtures" @@ -44,12 +44,12 @@ export const ResearchTheWeb = { ), } -export const UseASpecializedSkill = { +export const LoadedResources = { render: () => ( ), diff --git a/packages/session-ui/src/timeline/rows-current.test.ts b/packages/session-ui/src/timeline/rows-current.test.ts index 604624da56..3c50364806 100644 --- a/packages/session-ui/src/timeline/rows-current.test.ts +++ b/packages/session-ui/src/timeline/rows-current.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import type { SessionMessageInfo } from "@opencode-ai/client/promise" +import type { SessionMessageAssistantTool, SessionMessageInfo } from "@opencode-ai/client/promise" import { Timeline, TimelineRow } from "./projection" describe("current session timeline rows", () => { @@ -276,6 +276,56 @@ describe("current session timeline rows", () => { ]) }) + test("keeps reads that load files outside context groups", () => { + const read = { + type: "tool", + id: "tool_read", + name: "read", + state: { + status: "completed", + input: { path: "packages/cli/AGENTS.md" }, + content: [{ type: "text", text: "instructions" }], + metadata: { loaded: ["packages/cli/AGENTS.md"] }, + }, + time: { created: 2, ran: 3, completed: 4 }, + } satisfies SessionMessageAssistantTool + const grep = { + type: "tool", + id: "tool_grep", + name: "grep", + state: { status: "running", input: {}, metadata: {} }, + time: { created: 5 }, + } satisfies SessionMessageAssistantTool + const source = [ + { id: "msg_user", type: "user", text: "inspect", time: { created: 1 } }, + { + id: "msg_assistant", + type: "assistant", + agent: "build", + model: { id: "model", providerID: "provider" }, + content: [read, grep], + time: { created: 2 }, + }, + ] satisfies SessionMessageInfo[] + + const groups = Timeline.constructSessionMessageRows(source, false, { type: "idle" }).rows.flatMap((row) => + row._tag === "AssistantPart" ? [row.group] : [], + ) + + expect(groups).toEqual([ + { + type: "part", + key: "part:msg_assistant:tool_read", + ref: { messageID: "msg_assistant", partID: "tool_read" }, + }, + { + type: "context", + key: "context:msg_assistant:tool_grep", + refs: [{ messageID: "msg_assistant", partID: "tool_grep" }], + }, + ]) + }) + test("keeps context row keys unique when tool IDs repeat across assistant messages", () => { const tool = (name: string) => ({ type: "tool" as const, diff --git a/packages/session-ui/src/tools/tool-renderer.tsx b/packages/session-ui/src/tools/tool-renderer.tsx index ecc0bc3e70..84db2530a4 100644 --- a/packages/session-ui/src/tools/tool-renderer.tsx +++ b/packages/session-ui/src/tools/tool-renderer.tsx @@ -795,14 +795,29 @@ ToolRegistry.register({ }} /> - {(filepath) => ( -
- - - {i18n.t("ui.tool.loaded")} {relativizeProjectPath(filepath, data.directory)} - -
- )} + {(filepath) => { + const relative = relativizeProjectPath(filepath, data.directory) + const path = relative === filepath ? relative : relative.replace(/^[/\\]/, "") + const marker = "__OPENCODE_LOADED_PATH__" + const parts = i18n.t("ui.tool.loadedFile", { path: marker }).split(marker) + return ( +
+ + + + {(suffix) => ( + + )} + +
+ ) + }}
) @@ -1672,34 +1687,29 @@ ToolRegistry.register({ const i18n = useI18n() const name = createMemo(() => skillToolName(props.input, props.metadata)) const running = createMemo(() => props.status === "streaming" || props.status === "running") + const marker = "__OPENCODE_LOADED_SKILL__" + const parts = createMemo(() => i18n.t("ui.tool.loadedSkill", { name: marker }).split(marker)) - const trigger = () => ( -
- - - {i18n.t("ui.tool.skill")} - - - {(name) => ( - <> - - - - )} - -
+ return ( + }> + {(name) => ( +
+ + + + {(suffix) => ( + + )} + +
+ )} +
) - - return }, }) diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts index 4dc1087366..d6479eced6 100644 --- a/packages/ui/src/i18n/en.ts +++ b/packages/ui/src/i18n/en.ts @@ -152,6 +152,8 @@ const source = { "ui.tool.read": "Read", "ui.tool.loaded": "Loaded", + "ui.tool.loadedFile": "Loaded {{path}}", + "ui.tool.loadedSkill": "Loaded {{name}} skill", "ui.tool.list": "List", "ui.tool.glob": "Glob", "ui.tool.grep": "Grep",