mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 03:56:55 +03:00
feat(app): show background activity capsule above composer
This commit is contained in:
@@ -545,6 +545,13 @@ export const dict = {
|
||||
|
||||
"session.messages.loading": "Loading messages…",
|
||||
"session.messages.jumpToLatest": "Jump to latest",
|
||||
"session.activity.subagents.one": "{{count}} subagent",
|
||||
"session.activity.subagents.other": "{{count}} subagents",
|
||||
"session.activity.shells.one": "{{count}} shell",
|
||||
"session.activity.shells.other": "{{count}} shells",
|
||||
"session.activity.summary": "{{subagents}} · {{shells}}",
|
||||
"session.activity.section.subagents": "Subagents",
|
||||
"session.activity.section.shells": "Shells",
|
||||
|
||||
"session.question.progress": "{{current}} of {{total}} questions",
|
||||
"session.question.title": "Question",
|
||||
|
||||
@@ -277,6 +277,16 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
||||
const plural = (key: PluralKey, count: number, params?: Record<string, string | number | boolean>) =>
|
||||
pluralForm(key, pluralCategory(intl(), count), { ...params, count })
|
||||
|
||||
// "2 subagents · 1 shell", or one group alone, as the iOS app's background-work capsule reads.
|
||||
const activitySummary = (counts: { subagents: number; shells: number }) => {
|
||||
const subagents = counts.subagents ? plural("session.activity.subagents", counts.subagents) : undefined
|
||||
const shells = counts.shells ? plural("session.activity.shells", counts.shells) : undefined
|
||||
|
||||
if (subagents && shells) return t("session.activity.summary", { subagents, shells })
|
||||
|
||||
return subagents ?? shells ?? ""
|
||||
}
|
||||
|
||||
const tDynamic = <Key extends Extract<keyof Dictionary, string>>(
|
||||
key: TranslationKey<Key>,
|
||||
source: string,
|
||||
@@ -326,6 +336,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
||||
tDynamic,
|
||||
plural,
|
||||
pluralForm,
|
||||
activitySummary,
|
||||
rich,
|
||||
list,
|
||||
setLocale(next: Locale) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
[data-component="session-activity-glyph"] {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* One wave runs diagonally across the 5×5 grid, as in the iOS app's agent activity glyph. */
|
||||
[data-component="session-activity-glyph"] [data-dot] {
|
||||
animation: session-activity-glyph 1.1s linear infinite;
|
||||
animation-delay: calc(var(--session-activity-glyph-phase, 0ms) + (var(--diagonal) / 8 - 1) * 1.1s);
|
||||
}
|
||||
|
||||
[data-component="session-activity-glyph"] [data-reduced-motion] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
[data-component="session-activity-glyph"] [data-dot] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-component="session-activity-glyph"] [data-reduced-motion] {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* opacity = 0.16 + 0.84 * wave³, wave = (1 + cos 2πt) / 2 */
|
||||
@keyframes session-activity-glyph {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
5%,
|
||||
95% {
|
||||
opacity: 0.94;
|
||||
}
|
||||
10%,
|
||||
90% {
|
||||
opacity: 0.782;
|
||||
}
|
||||
15%,
|
||||
85% {
|
||||
opacity: 0.58;
|
||||
}
|
||||
20%,
|
||||
80% {
|
||||
opacity: 0.396;
|
||||
}
|
||||
25%,
|
||||
75% {
|
||||
opacity: 0.265;
|
||||
}
|
||||
30%,
|
||||
70% {
|
||||
opacity: 0.195;
|
||||
}
|
||||
35%,
|
||||
65% {
|
||||
opacity: 0.167;
|
||||
}
|
||||
40%,
|
||||
60% {
|
||||
opacity: 0.16;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import type { SessionMessageAssistantTool, SessionMessageInfo } from "@opencode/client/promise"
|
||||
import type { SessionDocument } from "@opencode/session-ui/document"
|
||||
import {
|
||||
CURRENT_SESSION_ID,
|
||||
STORY_MODEL,
|
||||
STORY_TIME,
|
||||
editThenTestDocument,
|
||||
largeCompletedDocument,
|
||||
} from "@opencode/session-ui/storybook"
|
||||
import { timelinePresets } from "@opencode/session-ui/timeline/detail"
|
||||
import { createMemo, onCleanup, onMount } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { SessionPreview } from "@/session/story-model"
|
||||
import type { SessionActivityItem } from "./activity-chips"
|
||||
|
||||
// The app's default timeline preset.
|
||||
const timelineDetail = timelinePresets[2].value
|
||||
|
||||
const subagents = [
|
||||
{ id: "ses_child_audit", agent: "explore", description: "Audit the timeline projection" },
|
||||
{ id: "ses_child_review", agent: "general", description: "Review the composer dock" },
|
||||
{ id: "ses_child_search", agent: "explore", description: "Search for shell metadata" },
|
||||
]
|
||||
|
||||
const shells = [
|
||||
{ id: "shl_dev", command: "bun run dev -- --port 4444" },
|
||||
{ id: "shl_test", command: "bun test --watch src/session" },
|
||||
]
|
||||
|
||||
const subagentItem = (index: number): SessionActivityItem => {
|
||||
const value = subagents[index % subagents.length]
|
||||
|
||||
return {
|
||||
key: `${value.id}_${index}`,
|
||||
type: "subagent",
|
||||
label: value.description,
|
||||
agent: value.agent,
|
||||
sessionID: value.id,
|
||||
target: value.id,
|
||||
}
|
||||
}
|
||||
|
||||
const shellItem = (index: number): SessionActivityItem => ({
|
||||
key: `${shells[index % shells.length].id}_${index}`,
|
||||
type: "shell",
|
||||
label: shells[index % shells.length].command,
|
||||
target: shells[index % shells.length].id,
|
||||
})
|
||||
|
||||
// The timeline follows new work at the bottom, like a pinned session.
|
||||
// Rows measure after the first frame, so wait past it.
|
||||
const pin = (behavior: ScrollBehavior) =>
|
||||
setTimeout(() => {
|
||||
const timeline = document.querySelector('[data-slot="story-timeline"]')
|
||||
|
||||
timeline?.scrollTo({ top: timeline.scrollHeight, behavior })
|
||||
}, 150)
|
||||
|
||||
const preview = (stage: number, subagentCount: number, shellCount: number) => () => {
|
||||
const value = liveDocument(stages[stage])
|
||||
|
||||
onMount(() => pin("auto"))
|
||||
|
||||
return (
|
||||
<SessionPreview
|
||||
title="Run background work"
|
||||
description="opencode · activity chips"
|
||||
document={value}
|
||||
timelineDetail={timelineDetail}
|
||||
activity={{
|
||||
subagents: Array.from({ length: subagentCount }, (_, index) => subagentItem(index)),
|
||||
shells: Array.from({ length: shellCount }, (_, index) => shellItem(index)),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Each stage lists the work still running; finished work stays in the timeline as completed calls.
|
||||
const stages = [
|
||||
{ started: 0, finished: 0, shells: 0, finishedShells: 0 },
|
||||
{ started: 1, finished: 0, shells: 0, finishedShells: 0 },
|
||||
{ started: 2, finished: 0, shells: 0, finishedShells: 0 },
|
||||
{ started: 2, finished: 0, shells: 1, finishedShells: 0 },
|
||||
{ started: 2, finished: 0, shells: 2, finishedShells: 0 },
|
||||
{ started: 2, finished: 1, shells: 2, finishedShells: 0 },
|
||||
{ started: 2, finished: 2, shells: 2, finishedShells: 0 },
|
||||
{ started: 2, finished: 2, shells: 2, finishedShells: 1 },
|
||||
{ started: 2, finished: 2, shells: 2, finishedShells: 2 },
|
||||
]
|
||||
|
||||
type Stage = (typeof stages)[number]
|
||||
|
||||
function liveDocument(stage: Stage): SessionDocument {
|
||||
const done = stage.finished === stage.started && stage.finishedShells === stage.shells && stage.started > 0
|
||||
|
||||
const tools = [
|
||||
...subagents.slice(0, stage.started).map(
|
||||
(value, index): SessionMessageAssistantTool => ({
|
||||
type: "tool",
|
||||
id: `tool_${value.id}`,
|
||||
name: "subagent",
|
||||
state:
|
||||
index < stage.finished
|
||||
? {
|
||||
status: "completed",
|
||||
input: { agent: value.agent, description: value.description, prompt: value.description },
|
||||
content: [{ type: "text", text: "No issues found." }],
|
||||
metadata: { sessionID: value.id, status: "completed" },
|
||||
}
|
||||
: {
|
||||
status: "running",
|
||||
input: { agent: value.agent, description: value.description, prompt: value.description },
|
||||
metadata: { sessionID: value.id },
|
||||
},
|
||||
time: { created: STORY_TIME + 301_000 + index * 1_000, ran: STORY_TIME + 301_100 + index * 1_000 },
|
||||
}),
|
||||
),
|
||||
...shells.slice(0, stage.shells).map(
|
||||
(value, index): SessionMessageAssistantTool => ({
|
||||
type: "tool",
|
||||
id: `tool_${value.id}`,
|
||||
name: "shell",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { command: value.command, description: value.command },
|
||||
content: [{ type: "text", text: index === 0 ? "Local: http://localhost:4444/" : "Watching src/session" }],
|
||||
metadata: { shellID: value.id, status: index < stage.finishedShells ? "exited" : "running" },
|
||||
},
|
||||
time: { created: STORY_TIME + 304_000 + index * 1_000, ran: STORY_TIME + 304_100 + index * 1_000 },
|
||||
}),
|
||||
),
|
||||
]
|
||||
|
||||
const messages: SessionMessageInfo[] = [
|
||||
...editThenTestDocument.messages,
|
||||
...largeCompletedDocument.messages.slice(-8),
|
||||
{
|
||||
id: "msg_user_activity",
|
||||
type: "user",
|
||||
text: "Audit the activity chips and keep the dev server and tests running while you work.",
|
||||
time: { created: STORY_TIME + 300_000 },
|
||||
metadata: { agent: "build", model: STORY_MODEL },
|
||||
},
|
||||
{
|
||||
id: "msg_assistant_activity",
|
||||
type: "assistant",
|
||||
agent: "build",
|
||||
model: STORY_MODEL,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "I'll delegate the audit to subagents and keep the dev server and tests running in the background.",
|
||||
},
|
||||
...tools,
|
||||
...(done
|
||||
? [{ type: "text" as const, text: "Both audits passed and the background commands have exited." }]
|
||||
: []),
|
||||
],
|
||||
time: { created: STORY_TIME + 300_500, completed: done ? STORY_TIME + 310_000 : undefined },
|
||||
},
|
||||
]
|
||||
|
||||
return { sessionID: CURRENT_SESSION_ID, messages, status: done ? { type: "idle" } : { type: "busy" }, diffs: [] }
|
||||
}
|
||||
|
||||
// Scripted with `activity-story-stage` events on `document` so a recording can step through the work.
|
||||
function LiveActivity() {
|
||||
const [story, setStory] = createStore({ stage: 0 })
|
||||
const stage = createMemo(() => stages[story.stage])
|
||||
const value = createMemo(() => liveDocument(stage()))
|
||||
|
||||
onMount(() => {
|
||||
const step = (event: Event) => {
|
||||
if (!(event instanceof CustomEvent)) return
|
||||
|
||||
setStory("stage", Number(event.detail))
|
||||
pin("smooth")
|
||||
}
|
||||
|
||||
document.addEventListener("activity-story-stage", step)
|
||||
onCleanup(() => document.removeEventListener("activity-story-stage", step))
|
||||
pin("auto")
|
||||
})
|
||||
|
||||
return (
|
||||
<SessionPreview
|
||||
title="Audit the activity chips"
|
||||
description="opencode · activity chips"
|
||||
document={value()}
|
||||
timelineDetail={timelineDetail}
|
||||
activity={{
|
||||
subagents: subagents
|
||||
.slice(stage().finished, stage().started)
|
||||
.map((_, index) => subagentItem(index + stage().finished)),
|
||||
shells: shells
|
||||
.slice(stage().finishedShells, stage().shells)
|
||||
.map((_, index) => shellItem(index + stage().finishedShells)),
|
||||
onOpen: (item) => {
|
||||
// The app opens the shell's tool call, or the group holding it, and centers it in the timeline.
|
||||
const part = document.querySelector(
|
||||
`[data-slot="story-timeline"] [data-timeline-part-ids*="tool_${item.target}"]`,
|
||||
)
|
||||
|
||||
const trigger = part?.querySelector<HTMLElement>('[data-slot="collapsible-trigger"][aria-expanded="false"]')
|
||||
|
||||
trigger?.click()
|
||||
requestAnimationFrame(() => part?.scrollIntoView({ block: "center", behavior: "smooth" }))
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: "OpenCode/Session/Activity chips",
|
||||
id: "session-activity-chips",
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Running subagents and background shells in one capsule above the production Composer. It opens a menu of the work.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const NoActivity = { render: preview(0, 0, 0) }
|
||||
|
||||
export const OneSubagent = { render: preview(1, 1, 0) }
|
||||
|
||||
export const TwoSubagents = { render: preview(2, 2, 0) }
|
||||
|
||||
export const OneShell = { render: preview(7, 0, 1) }
|
||||
|
||||
export const SubagentsAndShells = { render: preview(3, 2, 1) }
|
||||
|
||||
export const ManySubagents = { render: preview(4, 12, 2) }
|
||||
|
||||
export const Live = { render: () => <LiveActivity /> }
|
||||
@@ -0,0 +1,116 @@
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
import { Menu } from "@opencode/ui/menu"
|
||||
import { For, Index, Show } from "solid-js"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import "./activity-chips.css"
|
||||
|
||||
export type SessionActivityItem = {
|
||||
key: string
|
||||
type: "subagent" | "shell"
|
||||
label?: string
|
||||
agent?: string
|
||||
sessionID?: string
|
||||
target: string
|
||||
}
|
||||
|
||||
// Live work above the composer, one capsule like the iOS app's background-work summary.
|
||||
export function SessionActivityChips(props: {
|
||||
subagents: readonly SessionActivityItem[]
|
||||
shells: readonly SessionActivityItem[]
|
||||
onOpen: (item: SessionActivityItem) => void
|
||||
}) {
|
||||
const language = useLanguage()
|
||||
|
||||
const label = () => language.activitySummary({ subagents: props.subagents.length, shells: props.shells.length })
|
||||
|
||||
return (
|
||||
<Show when={props.subagents.length > 0 || props.shells.length > 0}>
|
||||
<div data-component="session-activity-chips" class="flex items-center pb-2">
|
||||
<Menu gutter={6} placement="top-start">
|
||||
<Menu.Trigger
|
||||
as="button"
|
||||
type="button"
|
||||
data-slot="session-activity-chip"
|
||||
class="flex h-7 min-w-0 max-w-full items-center gap-1.5 rounded-full border border-v2-border-border-base bg-v2-background-bg-base ps-2 pe-2.5 text-[13px] font-[530] leading-text-compact tracking-[-0.04px] whitespace-nowrap tabular-nums text-v2-text-text-base shadow-[var(--v2-elevation-raised)] outline-none duration-150 ease-out animate-in fade-in motion-reduce:animate-none hover:bg-v2-background-bg-layer-01 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-v2-border-border-focus data-[expanded]:bg-v2-background-bg-layer-01"
|
||||
>
|
||||
<SessionActivityGlyph />
|
||||
<span class="truncate">{label()}</span>
|
||||
</Menu.Trigger>
|
||||
<Menu.Portal>
|
||||
<Menu.Content class="w-72 max-w-[calc(100vw-24px)]" aria-label={label()}>
|
||||
<Show when={props.subagents.length > 0}>
|
||||
<Menu.Group>
|
||||
<Menu.GroupLabel>{language.t("session.activity.section.subagents")}</Menu.GroupLabel>
|
||||
<For each={props.subagents}>
|
||||
{(item) => (
|
||||
<Menu.Item onSelect={() => props.onOpen(item)}>
|
||||
<SessionActivityGlyph class="text-v2-icon-icon-muted" />
|
||||
<span class="shrink-0 font-[530]">
|
||||
{item.agent
|
||||
? `${item.agent[0].toUpperCase()}${item.agent.slice(1)}`
|
||||
: language.t("ui.tool.agent.default")}
|
||||
</span>
|
||||
<span dir="auto" class="me-1.5 min-w-0 flex-1 truncate text-v2-text-text-muted">
|
||||
{item.label}
|
||||
</span>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</For>
|
||||
</Menu.Group>
|
||||
</Show>
|
||||
<Show when={props.subagents.length > 0 && props.shells.length > 0}>
|
||||
<Menu.Separator />
|
||||
</Show>
|
||||
<Show when={props.shells.length > 0}>
|
||||
<Menu.Group>
|
||||
<Menu.GroupLabel>{language.t("session.activity.section.shells")}</Menu.GroupLabel>
|
||||
<For each={props.shells}>
|
||||
{(item) => (
|
||||
<Menu.Item onSelect={() => props.onOpen(item)}>
|
||||
<Icon name="console" class="shrink-0 text-v2-icon-icon-muted" />
|
||||
<span dir="ltr" class="me-1.5 min-w-0 flex-1 truncate font-mono text-v2-text-text-base">
|
||||
{item.label}
|
||||
</span>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</For>
|
||||
</Menu.Group>
|
||||
</Show>
|
||||
</Menu.Content>
|
||||
</Menu.Portal>
|
||||
</Menu>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
// A 5×5 dot matrix with a diagonal wave, ported from the iOS app's AgentActivityGlyph.
|
||||
function SessionActivityGlyph(props: { class?: string }) {
|
||||
// Glyphs mounted at different times share one phase, like the iOS glyph's clock-driven wave.
|
||||
const phase = `${-(performance.now() % 1100)}ms`
|
||||
|
||||
return (
|
||||
<svg
|
||||
data-component="session-activity-glyph"
|
||||
class={props.class}
|
||||
width={15}
|
||||
height={15}
|
||||
viewBox="0 0 5 5"
|
||||
aria-hidden="true"
|
||||
style={{ "--session-activity-glyph-phase": phase }}
|
||||
>
|
||||
<Index each={Array.from({ length: 25 })}>
|
||||
{(_, index) => (
|
||||
<circle
|
||||
data-dot
|
||||
cx={(index % 5) + 0.5}
|
||||
cy={Math.floor(index / 5) + 0.5}
|
||||
r={0.31}
|
||||
style={{ "--diagonal": (index % 5) + Math.floor(index / 5) }}
|
||||
/>
|
||||
)}
|
||||
</Index>
|
||||
<circle data-reduced-motion cx={2.5} cy={2.5} r={0.75} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -29,6 +29,8 @@ import { createSessionRevert } from "../revert"
|
||||
import { SessionComposerRegion } from "./session-composer-region"
|
||||
import { createSessionComposerController, type SessionComposerController } from "./controller"
|
||||
import { SessionQueuePanel } from "./queue-panel"
|
||||
import { SessionActivityChips, type SessionActivityItem } from "./activity-chips"
|
||||
import { useOpenSessionRoute } from "../session-identity-header"
|
||||
import { resolveSessionComposerSelection } from "./selection"
|
||||
import { createSessionRequestModel } from "../requests/model"
|
||||
import type { Region } from "@/runtime/extension/panels"
|
||||
@@ -295,6 +297,8 @@ export function ActiveSessionComposerRegion(props: {
|
||||
suggestionBoundary: () => HTMLElement | undefined
|
||||
}) {
|
||||
const location = useWorkspaceLocation()
|
||||
const data = useData()
|
||||
const openRoute = useOpenSessionRoute()
|
||||
|
||||
// Only the server's LocationNotFoundError for the session's own Location replaces the composer.
|
||||
const missing = createMemo(() => {
|
||||
@@ -306,6 +310,51 @@ export function ActiveSessionComposerRegion(props: {
|
||||
return { directory: info.location.directory, sessionID: info.id, projectID: info.projectID }
|
||||
})
|
||||
|
||||
// Subagents count while their call still blocks the run; foreground shells stay out, the timeline shows them.
|
||||
const subagents = createMemo<SessionActivityItem[]>(() => [
|
||||
...props.model.region.state.background.blocking().flatMap((task) =>
|
||||
task.type === "subagent"
|
||||
? [
|
||||
{
|
||||
...task,
|
||||
key: task.id ?? task.partID,
|
||||
agent: task.id ? data.session.get(task.id)?.agent : undefined,
|
||||
sessionID: task.id,
|
||||
target: task.partID,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
),
|
||||
...props.model.region.state.background.tasks().flatMap((task) =>
|
||||
task.type === "subagent"
|
||||
? [
|
||||
{
|
||||
...task,
|
||||
key: task.id,
|
||||
agent: task.agent ?? data.session.get(task.id)?.agent,
|
||||
sessionID: task.id,
|
||||
target: task.id,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
),
|
||||
])
|
||||
|
||||
const shells = createMemo<SessionActivityItem[]>(() =>
|
||||
props.model.region.state.background
|
||||
.tasks()
|
||||
.flatMap((task) => (task.type === "shell" ? [{ ...task, key: task.id, target: task.id }] : [])),
|
||||
)
|
||||
|
||||
const open = (item: SessionActivityItem) => {
|
||||
const current = props.session.identity.params.id
|
||||
|
||||
if (!current) return
|
||||
|
||||
// Shells and starting subagent calls live in this timeline, which reveals them once opened.
|
||||
openRoute(current, item.sessionID ?? current, item.sessionID ? undefined : item.target)
|
||||
}
|
||||
|
||||
return (
|
||||
<SessionComposerRegion
|
||||
controller={props.model.region}
|
||||
@@ -315,6 +364,7 @@ export function ActiveSessionComposerRegion(props: {
|
||||
keyed
|
||||
fallback={
|
||||
<div class="relative">
|
||||
<SessionActivityChips subagents={subagents()} shells={shells()} onOpen={open} />
|
||||
<SessionQueuePanel queue={props.model.queue} />
|
||||
<div class="relative z-10">
|
||||
<Composer
|
||||
|
||||
@@ -6,9 +6,11 @@ import {
|
||||
SessionComposerRegion,
|
||||
type SessionComposerRegionViewController,
|
||||
} from "@/session/composer/session-composer-region"
|
||||
import { SessionActivityChips, type SessionActivityItem } from "@/session/composer/activity-chips"
|
||||
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 { CurrentSessionProviders, STORY_MODEL } from "@opencode/session-ui/storybook"
|
||||
import { SessionTimeline } from "@opencode/session-ui/timeline"
|
||||
import { createComposerEditor } from "@/composer/editor/interaction"
|
||||
@@ -85,6 +87,12 @@ export type SessionPreviewProps = {
|
||||
reviewOpened?: boolean
|
||||
child?: { parentID: string }
|
||||
terminal?: { title: string; lines: string[] }
|
||||
activity?: {
|
||||
subagents: SessionActivityItem[]
|
||||
shells: SessionActivityItem[]
|
||||
onOpen?: (item: SessionActivityItem) => void
|
||||
}
|
||||
timelineDetail?: TimelineDetail
|
||||
}
|
||||
|
||||
export function SessionPreview(props: SessionPreviewProps) {
|
||||
@@ -187,7 +195,12 @@ function SessionSurfaceState(props: SessionPreviewProps & { onReset: () => void
|
||||
|
||||
const prompt = createPromptController({
|
||||
initial: props.draft ?? "",
|
||||
placeholder: language.t("prompt.placeholder.normal"),
|
||||
placeholder: language.t(
|
||||
props.document.status.type === "idle"
|
||||
? "ui.promptInput.placeholder.normal"
|
||||
: "ui.promptInput.placeholder.followUp",
|
||||
{ slash: "/", at: "@" },
|
||||
),
|
||||
status: () => props.document.status,
|
||||
onActivity: (activity) => setState("activity", activity),
|
||||
onSubmit: (text) => setState("activity", `Submitted locally: ${text}`),
|
||||
@@ -256,14 +269,15 @@ function SessionSurfaceState(props: SessionPreviewProps & { onReset: () => void
|
||||
<div class="flex min-h-0 flex-1">
|
||||
<section
|
||||
classList={{
|
||||
"min-w-0 flex-1 flex-col bg-background-base": true,
|
||||
"min-w-0 flex-1 flex-col bg-v2-background-bg-base": true,
|
||||
flex: !state.reviewOpened,
|
||||
"hidden md:flex": state.reviewOpened,
|
||||
}}
|
||||
>
|
||||
<div class="min-h-0 flex-1 overflow-y-auto py-6">
|
||||
<div data-slot="story-timeline" class="min-h-0 flex-1 overflow-y-auto py-6">
|
||||
<SessionTimeline
|
||||
document={props.document}
|
||||
timelineDetail={props.timelineDetail}
|
||||
editToolDefaultOpen
|
||||
shellToolDefaultOpen
|
||||
class="mx-auto w-full max-w-[840px]"
|
||||
@@ -271,7 +285,22 @@ function SessionSurfaceState(props: SessionPreviewProps & { onReset: () => void
|
||||
</div>
|
||||
<SessionComposerRegion
|
||||
controller={region}
|
||||
composer={<Composer model={prompt.controller} borderUnderlay />}
|
||||
composer={
|
||||
<div class="relative">
|
||||
<SessionActivityChips
|
||||
subagents={props.activity?.subagents ?? []}
|
||||
shells={props.activity?.shells ?? []}
|
||||
onOpen={(item) =>
|
||||
props.activity?.onOpen
|
||||
? props.activity.onOpen(item)
|
||||
: setState("activity", `Opened ${item.label ?? item.key}`)
|
||||
}
|
||||
/>
|
||||
<div class="relative z-10">
|
||||
<Composer model={prompt.controller} borderUnderlay />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
<Show when={state.reviewOpened}>
|
||||
|
||||
Reference in New Issue
Block a user