mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-23 20:21:11 +00:00
feat(ui): show the live plan checklist inside the chat thread (#109343)
The run-scoped plan snapshot already fed the sticky composer bar; it now also renders as an always-expanded card inside the active run's work group, so plan progress is visible in scroll context. The bar and card share one extracted renderer (variant: bar | card) instead of duplicating the template, and the card clears with the run through existing plan lifecycle.
This commit is contained in:
committed by
GitHub
parent
d6e9cbd775
commit
02adefa39a
@@ -55,7 +55,8 @@ export type ChatItem =
|
||||
timestamp: number;
|
||||
}
|
||||
| { kind: "stream"; key: string; text: string; startedAt: number; isStreaming: boolean }
|
||||
| { kind: "reading-indicator"; key: string; startedAt: number };
|
||||
| { kind: "reading-indicator"; key: string; startedAt: number }
|
||||
| { kind: "plan"; key: string };
|
||||
|
||||
export type ChatStreamSegment = {
|
||||
text: string;
|
||||
|
||||
@@ -556,6 +556,24 @@ describe("buildCachedChatItems working spark", () => {
|
||||
expect(hasReadingIndicator({ runWorking: true })).toBe(true);
|
||||
});
|
||||
|
||||
it("adds the plan to the active stream run and removes it when the run stops", () => {
|
||||
const planStatus = {
|
||||
steps: [{ step: "Inspect the route", status: "in_progress" as const }],
|
||||
};
|
||||
const activeItems = buildCachedChatItems(
|
||||
createProps({ runActive: true, runWorking: true, planStatus }),
|
||||
);
|
||||
|
||||
expect(coalesceStreamRuns(activeItems)).toMatchObject([
|
||||
{ kind: "stream-run", parts: [{ kind: "reading-indicator" }, { kind: "plan" }] },
|
||||
]);
|
||||
|
||||
const idleItems = buildCachedChatItems(
|
||||
createProps({ runActive: false, runWorking: false, planStatus }),
|
||||
);
|
||||
expect(idleItems.some((item) => item.kind === "plan")).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps the run start time on the working indicator", () => {
|
||||
const indicator = buildCachedChatItems(
|
||||
createProps({ runWorking: true, streamStartedAt: 42_000 }),
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
shouldRenderQueuedSendInThread,
|
||||
} from "./chat-progress.ts";
|
||||
import { getOrCreateSessionCacheValue } from "./session-cache.ts";
|
||||
import type { PlanStatus } from "./tool-stream.ts";
|
||||
import { buildUserChatMessageContentBlocks } from "./user-message-content.ts";
|
||||
|
||||
type BuildChatItemsProps = {
|
||||
@@ -60,6 +61,9 @@ type BuildChatItemsProps = {
|
||||
showToolCalls: boolean;
|
||||
/** True while the agent is visibly working (isChatRunWorking). */
|
||||
runWorking?: boolean;
|
||||
/** True while the current session has an abortable live run. */
|
||||
runActive?: boolean;
|
||||
planStatus?: PlanStatus | null;
|
||||
/** True while chat history is loading (initial load or background reload). */
|
||||
loading?: boolean;
|
||||
searchOpen?: boolean;
|
||||
@@ -77,7 +81,9 @@ type RenderChatItem = ReturnType<typeof buildChatItems>[number];
|
||||
type StreamRunRenderItem = {
|
||||
kind: "stream-run";
|
||||
key: string;
|
||||
parts: Array<Extract<ChatItem, { kind: "stream" } | { kind: "reading-indicator" }>>;
|
||||
parts: Array<
|
||||
Extract<ChatItem, { kind: "stream" } | { kind: "reading-indicator" } | { kind: "plan" }>
|
||||
>;
|
||||
};
|
||||
|
||||
const chatItemsByPane = new Map<string, Map<string, CachedChatItems>>();
|
||||
@@ -1095,6 +1101,7 @@ function chatItemTimestamp(item: ChatItem): number | null {
|
||||
case "stream":
|
||||
return item.startedAt;
|
||||
case "reading-indicator":
|
||||
case "plan":
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
@@ -1469,6 +1476,9 @@ function buildChatItems(props: BuildChatItemsProps): Array<ChatItem | MessageGro
|
||||
items.push({ kind: "reading-indicator", key, startedAt });
|
||||
}
|
||||
}
|
||||
if (props.runActive === true && props.planStatus && props.planStatus.steps.length > 0) {
|
||||
items.push({ kind: "plan", key: `plan:${props.sessionKey}:active` });
|
||||
}
|
||||
|
||||
return annotateToolTurnOutcome(
|
||||
groupMessages(
|
||||
@@ -1532,6 +1542,8 @@ function sameChatItem(previous: RenderChatItem, next: RenderChatItem): boolean {
|
||||
);
|
||||
case "reading-indicator":
|
||||
return previous.kind === "reading-indicator" && previous.startedAt === next.startedAt;
|
||||
case "plan":
|
||||
return previous.kind === "plan";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1632,6 +1644,8 @@ function sameChatItemsStructuralInput(
|
||||
previous.queue === next.queue &&
|
||||
previous.showToolCalls === next.showToolCalls &&
|
||||
previous.runWorking === next.runWorking &&
|
||||
previous.runActive === next.runActive &&
|
||||
Boolean(previous.planStatus?.steps.length) === Boolean(next.planStatus?.steps.length) &&
|
||||
previous.loading === next.loading &&
|
||||
previous.searchOpen === next.searchOpen &&
|
||||
previous.searchQuery === next.searchQuery
|
||||
@@ -1732,7 +1746,7 @@ export function coalesceStreamRuns(
|
||||
): Array<RenderChatItem | StreamRunRenderItem> {
|
||||
const result: Array<RenderChatItem | StreamRunRenderItem> = [];
|
||||
let run: StreamRunRenderItem["parts"] = [];
|
||||
// Contiguous in-flight stream and reading-indicator items render under one
|
||||
// Contiguous in-flight stream, plan, and reading-indicator items render under one
|
||||
// assistant avatar; messages, groups, and dividers intentionally break the run.
|
||||
const flush = () => {
|
||||
const [first] = run;
|
||||
@@ -1742,7 +1756,7 @@ export function coalesceStreamRuns(
|
||||
}
|
||||
};
|
||||
for (const item of items) {
|
||||
if (item.kind === "stream" || item.kind === "reading-indicator") {
|
||||
if (item.kind === "stream" || item.kind === "reading-indicator" || item.kind === "plan") {
|
||||
run.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -280,6 +280,7 @@ export function renderChat(props: ChatProps) {
|
||||
showToolCalls: props.showToolCalls,
|
||||
runActive: Boolean(props.canAbort),
|
||||
runWorking: isChatRunWorking(props),
|
||||
planStatus: props.planStatus,
|
||||
sessions: props.sessions,
|
||||
sessionHost: props.sessionHost,
|
||||
assistantName: props.assistantName,
|
||||
|
||||
@@ -56,6 +56,7 @@ import {
|
||||
renderChatAttachmentInputs,
|
||||
renderChatAttachmentMenu,
|
||||
} from "./chat-attachments.ts";
|
||||
import { renderChatPlanChecklist } from "./chat-plan-checklist.ts";
|
||||
import {
|
||||
renderChatVoiceError,
|
||||
renderMicrophoneActivity,
|
||||
@@ -1160,68 +1161,6 @@ function renderFallbackIndicator(status: FallbackStatus | null | undefined) {
|
||||
`;
|
||||
}
|
||||
|
||||
function renderPlanChecklist(status: PlanStatus | null | undefined, active: boolean) {
|
||||
if (!active || !status || status.steps.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
const completed = status.steps.filter((step) => step.status === "completed").length;
|
||||
let current = status.steps.find((step) => step.status === "in_progress");
|
||||
if (!current) {
|
||||
for (let index = status.steps.length - 1; index >= 0; index -= 1) {
|
||||
const step = status.steps[index];
|
||||
if (step?.status === "completed") {
|
||||
current = step;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
current ??= status.steps[0];
|
||||
if (!current) {
|
||||
return nothing;
|
||||
}
|
||||
const statusLabels: Record<PlanStatus["steps"][number]["status"], string> = {
|
||||
completed: "completed",
|
||||
in_progress: "in progress",
|
||||
pending: "pending",
|
||||
};
|
||||
return html`
|
||||
<details class="plan-checklist">
|
||||
<summary
|
||||
class="plan-checklist__summary"
|
||||
aria-label=${`Plan: ${current.step}. ${completed} of ${status.steps.length} completed`}
|
||||
>
|
||||
<span class="plan-checklist__current-marker" aria-hidden="true">▸</span>
|
||||
<span class="plan-checklist__current">${current.step}</span>
|
||||
<span class="plan-checklist__count">${completed}/${status.steps.length}</span>
|
||||
</summary>
|
||||
<div class="plan-checklist__body">
|
||||
${status.explanation
|
||||
? html`<div class="plan-checklist__explanation">${status.explanation}</div>`
|
||||
: nothing}
|
||||
<ol class="plan-checklist__steps">
|
||||
${status.steps.map(
|
||||
(step) => html`
|
||||
<li
|
||||
class=${`plan-checklist__step plan-checklist__step--${step.status}`}
|
||||
aria-label=${`${step.step}, ${statusLabels[step.status]}`}
|
||||
>
|
||||
<span class="plan-checklist__step-marker" aria-hidden="true"
|
||||
>${step.status === "completed"
|
||||
? "✓"
|
||||
: step.status === "in_progress"
|
||||
? "▸"
|
||||
: "▢"}</span
|
||||
>
|
||||
<span class="plan-checklist__step-text">${step.step}</span>
|
||||
</li>
|
||||
`,
|
||||
)}
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
`;
|
||||
}
|
||||
|
||||
type ContextNoticeOptions = {
|
||||
compactBusy?: boolean;
|
||||
compactDisabled?: boolean;
|
||||
@@ -2256,7 +2195,10 @@ export function renderChatComposer(props: ChatComposerProps) {
|
||||
`
|
||||
: nothing}
|
||||
<div class="agent-chat__composer-status-stack">
|
||||
${renderPlanChecklist(props.planStatus, showAbortableUi)}
|
||||
${renderChatPlanChecklist(props.planStatus, {
|
||||
active: showAbortableUi,
|
||||
variant: "bar",
|
||||
})}
|
||||
${renderFallbackIndicator(props.fallbackStatus)}
|
||||
${renderCompactionIndicator(props.compactionStatus)}
|
||||
${renderChatGoal(state, activeSession?.goal, {
|
||||
|
||||
@@ -1074,6 +1074,34 @@ describe("grouped chat rendering", () => {
|
||||
expect(container.querySelector(".chat-group-footer")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders the active plan card inside the working stream group", () => {
|
||||
const container = document.createElement("div");
|
||||
|
||||
render(
|
||||
renderStreamGroup(
|
||||
[
|
||||
{ kind: "reading-indicator", key: "reading", startedAt: 1_000 },
|
||||
{ kind: "plan", key: "plan:main:active" },
|
||||
],
|
||||
{
|
||||
planActive: true,
|
||||
planStatus: {
|
||||
explanation: "Keep the change focused",
|
||||
steps: [
|
||||
{ step: "Inspect", status: "completed" },
|
||||
{ step: "Implement", status: "in_progress" },
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
container,
|
||||
);
|
||||
|
||||
expect(container.querySelector(".chat-group--working .plan-checklist--card")).not.toBeNull();
|
||||
expect(container.querySelectorAll(".plan-checklist__step")).toHaveLength(2);
|
||||
expect(container.querySelectorAll(".chat-avatar.assistant")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("keeps the avatar once a stream part joins the reading indicator", () => {
|
||||
const container = document.createElement("div");
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@ import { stripThinkingTags } from "../../../lib/strip-thinking-tags.ts";
|
||||
import { detectTextDirection } from "../../../lib/text-direction.ts";
|
||||
import { getSafeLocalStorage } from "../../../local-storage.ts";
|
||||
import { renderChatAvatar } from "../chat-avatar.ts";
|
||||
import type { PlanStatus } from "../tool-stream.ts";
|
||||
import { renderChatPlanChecklist } from "./chat-plan-checklist.ts";
|
||||
import type { SidebarContent } from "./chat-sidebar.ts";
|
||||
import {
|
||||
isRunningToolCard,
|
||||
@@ -556,13 +558,18 @@ function extractTranscriptAttachments(message: unknown): AttachmentItem[] {
|
||||
}
|
||||
|
||||
/** A contiguous run of in-flight streaming items rendered under one assistant group. */
|
||||
type StreamGroupPart = Extract<ChatItem, { kind: "stream" } | { kind: "reading-indicator" }>;
|
||||
type StreamGroupPart = Extract<
|
||||
ChatItem,
|
||||
{ kind: "stream" } | { kind: "reading-indicator" } | { kind: "plan" }
|
||||
>;
|
||||
|
||||
type StreamGroupOptions = {
|
||||
onOpenSidebar?: (content: SidebarContent) => void;
|
||||
assistant?: AssistantIdentity;
|
||||
basePath?: string;
|
||||
authToken?: string | null;
|
||||
planStatus?: PlanStatus | null;
|
||||
planActive?: boolean;
|
||||
};
|
||||
|
||||
// One assistant group per contiguous run of streaming items: a reply that
|
||||
@@ -578,14 +585,14 @@ export function renderStreamGroup(parts: StreamGroupPart[], opts: StreamGroupOpt
|
||||
// While the agent works with nothing streamed yet the run is pure claw: no
|
||||
// avatar next to it - the punching pincer is the whole signal. The avatar
|
||||
// arrives with the first stream part.
|
||||
const indicatorOnly = parts.every((part) => part.kind === "reading-indicator");
|
||||
const avatar = indicatorOnly
|
||||
const workingOnly = parts.every((part) => part.kind !== "stream");
|
||||
const avatar = workingOnly
|
||||
? nothing
|
||||
: renderChatAvatar("assistant", assistant, undefined, basePath, authToken);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="chat-group assistant ${indicatorOnly ? "chat-group--working" : ""}"
|
||||
class="chat-group assistant ${workingOnly ? "chat-group--working" : ""}"
|
||||
data-chat-row-key=${parts[0]?.key ?? nothing}
|
||||
>
|
||||
${avatar}
|
||||
@@ -593,16 +600,21 @@ export function renderStreamGroup(parts: StreamGroupPart[], opts: StreamGroupOpt
|
||||
${parts.map((part) =>
|
||||
part.kind === "reading-indicator"
|
||||
? renderChatWorkingIndicator(part)
|
||||
: renderGroupedMessage(
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: part.text }],
|
||||
timestamp: part.startedAt,
|
||||
},
|
||||
part.key,
|
||||
{ isStreaming: part.isStreaming, showReasoning: false },
|
||||
onOpenSidebar,
|
||||
),
|
||||
: part.kind === "plan"
|
||||
? renderChatPlanChecklist(opts.planStatus, {
|
||||
active: opts.planActive === true,
|
||||
variant: "card",
|
||||
})
|
||||
: renderGroupedMessage(
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: part.text }],
|
||||
timestamp: part.startedAt,
|
||||
},
|
||||
part.key,
|
||||
{ isStreaming: part.isStreaming, showReasoning: false },
|
||||
onOpenSidebar,
|
||||
),
|
||||
)}
|
||||
${footerStartedAt !== null
|
||||
? html`
|
||||
|
||||
51
ui/src/pages/chat/components/chat-plan-checklist.test.ts
Normal file
51
ui/src/pages/chat/components/chat-plan-checklist.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render } from "lit";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { PlanStatus } from "../tool-stream.ts";
|
||||
import { renderChatPlanChecklist } from "./chat-plan-checklist.ts";
|
||||
|
||||
const planStatus: PlanStatus = {
|
||||
explanation: "Keep the change focused",
|
||||
steps: [
|
||||
{ step: "Inspect the route", status: "completed" },
|
||||
{ step: "Wire the checklist", status: "in_progress" },
|
||||
{ step: "Run focused tests", status: "pending" },
|
||||
],
|
||||
};
|
||||
|
||||
function renderChecklist(status: PlanStatus | null, active: boolean) {
|
||||
const container = document.createElement("div");
|
||||
render(renderChatPlanChecklist(status, { active, variant: "card" }), container);
|
||||
return container;
|
||||
}
|
||||
|
||||
describe("renderChatPlanChecklist", () => {
|
||||
it("renders the full card plan with explanation and step statuses", () => {
|
||||
const container = renderChecklist(planStatus, true);
|
||||
const card = container.querySelector(".plan-checklist--card");
|
||||
|
||||
expect(card).not.toBeNull();
|
||||
expect(card).not.toBeInstanceOf(HTMLDetailsElement);
|
||||
expect(card?.querySelector(".plan-checklist__current")?.textContent).toBe("Wire the checklist");
|
||||
expect(card?.querySelector(".plan-checklist__count")?.textContent).toBe("1/3");
|
||||
expect(card?.querySelector(".plan-checklist__explanation")?.textContent).toBe(
|
||||
"Keep the change focused",
|
||||
);
|
||||
expect(
|
||||
[...(card?.querySelectorAll(".plan-checklist__step") ?? [])].map((step) => ({
|
||||
label: step.getAttribute("aria-label"),
|
||||
status: [...step.classList].find((name) => name.startsWith("plan-checklist__step--")),
|
||||
})),
|
||||
).toEqual([
|
||||
{ label: "Inspect the route, completed", status: "plan-checklist__step--completed" },
|
||||
{ label: "Wire the checklist, in progress", status: "plan-checklist__step--in_progress" },
|
||||
{ label: "Run focused tests, pending", status: "plan-checklist__step--pending" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("hides the checklist without a plan or active run", () => {
|
||||
expect(renderChecklist(null, true).querySelector(".plan-checklist")).toBeNull();
|
||||
expect(renderChecklist(planStatus, false).querySelector(".plan-checklist")).toBeNull();
|
||||
});
|
||||
});
|
||||
89
ui/src/pages/chat/components/chat-plan-checklist.ts
Normal file
89
ui/src/pages/chat/components/chat-plan-checklist.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { html, nothing } from "lit";
|
||||
import type { PlanStatus } from "../tool-stream.ts";
|
||||
|
||||
type ChatPlanChecklistVariant = "bar" | "card";
|
||||
|
||||
type ChatPlanChecklistOptions = {
|
||||
active: boolean;
|
||||
variant: ChatPlanChecklistVariant;
|
||||
};
|
||||
|
||||
function renderPlanChecklistBody(status: PlanStatus) {
|
||||
const statusLabels: Record<PlanStatus["steps"][number]["status"], string> = {
|
||||
completed: "completed",
|
||||
in_progress: "in progress",
|
||||
pending: "pending",
|
||||
};
|
||||
return html`
|
||||
<div class="plan-checklist__body">
|
||||
${status.explanation
|
||||
? html`<div class="plan-checklist__explanation">${status.explanation}</div>`
|
||||
: nothing}
|
||||
<ol class="plan-checklist__steps">
|
||||
${status.steps.map(
|
||||
(step) => html`
|
||||
<li
|
||||
class=${`plan-checklist__step plan-checklist__step--${step.status}`}
|
||||
aria-label=${`${step.step}, ${statusLabels[step.status]}`}
|
||||
>
|
||||
<span class="plan-checklist__step-marker" aria-hidden="true"
|
||||
>${step.status === "completed"
|
||||
? "✓"
|
||||
: step.status === "in_progress"
|
||||
? "▸"
|
||||
: "▢"}</span
|
||||
>
|
||||
<span class="plan-checklist__step-text">${step.step}</span>
|
||||
</li>
|
||||
`,
|
||||
)}
|
||||
</ol>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function renderChatPlanChecklist(
|
||||
status: PlanStatus | null | undefined,
|
||||
options: ChatPlanChecklistOptions,
|
||||
) {
|
||||
if (!options.active || !status || status.steps.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
const completed = status.steps.filter((step) => step.status === "completed").length;
|
||||
let current = status.steps.find((step) => step.status === "in_progress");
|
||||
if (!current) {
|
||||
for (let index = status.steps.length - 1; index >= 0; index -= 1) {
|
||||
const step = status.steps[index];
|
||||
if (step?.status === "completed") {
|
||||
current = step;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
current ??= status.steps[0];
|
||||
if (!current) {
|
||||
return nothing;
|
||||
}
|
||||
const label = `Plan: ${current.step}. ${completed} of ${status.steps.length} completed`;
|
||||
const summary = html`
|
||||
<span class="plan-checklist__current-marker" aria-hidden="true">▸</span>
|
||||
<span class="plan-checklist__current">${current.step}</span>
|
||||
<span class="plan-checklist__count">${completed}/${status.steps.length}</span>
|
||||
`;
|
||||
const body = renderPlanChecklistBody(status);
|
||||
|
||||
if (options.variant === "card") {
|
||||
return html`
|
||||
<section class="plan-checklist plan-checklist--card" aria-label=${label}>
|
||||
<div class="plan-checklist__summary">${summary}</div>
|
||||
${body}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<details class="plan-checklist plan-checklist--bar">
|
||||
<summary class="plan-checklist__summary" aria-label=${label}>${summary}</summary>
|
||||
${body}
|
||||
</details>
|
||||
`;
|
||||
}
|
||||
@@ -56,6 +56,7 @@ import { DeletedMessages } from "../deleted-messages.ts";
|
||||
import { PinnedMessages } from "../pinned-messages.ts";
|
||||
import type { RealtimeTalkConversationEntry } from "../realtime-talk-conversation.ts";
|
||||
import { getOrCreateSessionCacheValue } from "../session-cache.ts";
|
||||
import type { PlanStatus } from "../tool-stream.ts";
|
||||
import { getToolTitlesVersion } from "../tool-titles.ts";
|
||||
import { renderBackgroundTasksStatusRow } from "./chat-background-tasks-status.ts";
|
||||
import type { BackgroundTasksProps } from "./chat-background-tasks.ts";
|
||||
@@ -108,6 +109,7 @@ type ChatThreadProps = {
|
||||
runActive?: boolean;
|
||||
/** True while the agent is visibly working (isChatRunWorking); shows the working spark. */
|
||||
runWorking?: boolean;
|
||||
planStatus?: PlanStatus | null;
|
||||
sessions: SessionsListResult | null;
|
||||
/** Host context resolving global-alias session keys (scope=global fleets). */
|
||||
/** Includes assistantAgentId so bare-global welcome recents scope to the selected agent. */
|
||||
@@ -972,6 +974,8 @@ function renderChatThreadContents(
|
||||
queue: props.queue,
|
||||
showToolCalls: props.showToolCalls,
|
||||
runWorking: Boolean(props.runWorking),
|
||||
runActive: Boolean(props.runActive),
|
||||
planStatus: props.planStatus,
|
||||
loading: props.loading,
|
||||
searchOpen: state.searchOpen,
|
||||
searchQuery: state.searchQuery,
|
||||
@@ -1050,6 +1054,8 @@ function renderChatThreadContents(
|
||||
}
|
||||
if (item.kind === "stream-run") {
|
||||
return renderStreamGroup(item.parts, {
|
||||
planStatus: props.planStatus,
|
||||
planActive: Boolean(props.runActive),
|
||||
onOpenSidebar: props.onOpenSidebar,
|
||||
assistant: assistantIdentity,
|
||||
basePath: props.basePath,
|
||||
@@ -1117,6 +1123,7 @@ function renderChatThreadContents(
|
||||
props.showToolCalls,
|
||||
Boolean(props.runActive),
|
||||
Boolean(props.runWorking),
|
||||
props.planStatus,
|
||||
Boolean(props.autoExpandToolCalls),
|
||||
props.assistantName,
|
||||
assistantIdentity.avatar,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.plan-checklist {
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
@@ -9,6 +10,24 @@
|
||||
animation: fade-in 0.2s var(--ease-out);
|
||||
}
|
||||
|
||||
.plan-checklist--card {
|
||||
flex: none;
|
||||
width: min(760px, 100%);
|
||||
margin-top: 4px;
|
||||
border-color: color-mix(in srgb, var(--border) 82%, transparent);
|
||||
border-radius: var(--radius-md);
|
||||
background: color-mix(in srgb, var(--card, var(--bg)) 55%, transparent);
|
||||
}
|
||||
|
||||
.plan-checklist--card .plan-checklist__summary {
|
||||
cursor: default;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.plan-checklist--card .plan-checklist__body {
|
||||
border-top-color: color-mix(in srgb, var(--border) 62%, transparent);
|
||||
}
|
||||
|
||||
.agent-chat__composer-status-stack:has(.plan-checklist) {
|
||||
display: flex;
|
||||
}
|
||||
@@ -49,7 +68,7 @@
|
||||
transition: transform var(--duration-fast) ease;
|
||||
}
|
||||
|
||||
.plan-checklist[open] .plan-checklist__current-marker {
|
||||
.plan-checklist--bar[open] .plan-checklist__current-marker {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user