From 1c31b4c2b1fc825014f53ab0d4e030b79ff946af Mon Sep 17 00:00:00 2001 From: AprilNEA Date: Fri, 31 Jul 2026 12:42:08 +0000 Subject: [PATCH 1/2] feat(resources): show current plan in panel --- .../src/renderer/src/shell/desktop-shell.tsx | 2 + .../webview/src/shell/web-workbench-shell.tsx | 2 + .../src/surface/task-resources-panel.tsx | 5 +- .../workbench/src/surface/workbench.tsx | 7 ++- packages/presentation/i18n/src/locales/en.ts | 2 + .../presentation/i18n/src/locales/zh-cn.ts | 2 + .../shell/__tests__/question-prompt.test.tsx | 27 ++++++++++ .../__tests__/task-resources-panel.test.tsx | 49 +++++++++++++++++++ .../ui/src/shell/conversation-prompt-dock.tsx | 4 +- .../ui/src/shell/conversation-surface.tsx | 4 ++ .../presentation/ui/src/shell/shell-frame.tsx | 4 ++ .../ui/src/shell/task-resources-panel.tsx | 47 ++++++++++++++++++ 12 files changed, 152 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/renderer/src/shell/desktop-shell.tsx b/apps/desktop/src/renderer/src/shell/desktop-shell.tsx index 36359d26f..c62e3f334 100644 --- a/apps/desktop/src/renderer/src/shell/desktop-shell.tsx +++ b/apps/desktop/src/renderer/src/shell/desktop-shell.tsx @@ -310,6 +310,7 @@ export function DesktopShell({ rightPanelOpen: rightPanel.open, }); const resourcesFloatingOpen = resourcesPresentation === 'floating' && resourcesOpen; + const resourcesSurfaceOpen = resourcesPresentation !== 'hidden' && resourcesOpen; const titledSession = active?.title === undefined ? null : active; const hideMainTitle = draft !== null || (active === null ? false : titledSession === null); const isRunning = conversation.status === 'running' || conversation.status === 'starting'; @@ -466,6 +467,7 @@ export function DesktopShell({ isRunning={isRunning} mentionItems={mentionItems} onMentionQueryChange={(query) => onMentionQueryChange(active?.cwd, query)} + showPlanInPromptDock={!resourcesSurfaceOpen} onRespondPermission={onRespondPermission} onRespondQuestion={onRespondQuestion} onOpenFileArtifact={openFileArtifact} diff --git a/apps/webview/src/shell/web-workbench-shell.tsx b/apps/webview/src/shell/web-workbench-shell.tsx index aeb356869..6d938b6ee 100644 --- a/apps/webview/src/shell/web-workbench-shell.tsx +++ b/apps/webview/src/shell/web-workbench-shell.tsx @@ -37,6 +37,7 @@ export function WebWorkbenchShell({ floatingSpaceAvailable, }); const resourcesFloatingOpen = resourcesPresentation === 'floating' && resourcesOpen; + const resourcesSurfaceOpen = resourcesPresentation !== 'hidden' && resourcesOpen; const resourcesButton = ( { void navigate('/automations'); }} diff --git a/packages/client/workbench/src/surface/task-resources-panel.tsx b/packages/client/workbench/src/surface/task-resources-panel.tsx index 6f0b2daf2..8a4fbb9dc 100644 --- a/packages/client/workbench/src/surface/task-resources-panel.tsx +++ b/packages/client/workbench/src/surface/task-resources-panel.tsx @@ -2,7 +2,7 @@ import { useLinkCodeClient } from '@linkcode/client-core'; import type { SessionId, SessionResource } from '@linkcode/schema'; import { MAX_ATTACHMENT_BYTES, SessionResourceIdSchema } from '@linkcode/schema'; import { hostResource, listResources, removeResource, uploadSource } from '@linkcode/sdk'; -import type { ResourceItem } from '@linkcode/ui'; +import type { CurrentPlan, ResourceItem } from '@linkcode/ui'; import { readFileAsBase64, TaskResourcesPanel } from '@linkcode/ui'; import { toastManager } from 'coss-ui/components/toast'; import { useEffect } from 'foxact/use-abortable-effect'; @@ -51,8 +51,10 @@ function useSessionResources(sessionId: SessionId) { /** Runtime-backed adapter from the session resource data plane into the pure shared panel. */ export function RuntimeTaskResourcesPanel({ sessionId, + plan, }: { sessionId: SessionId; + plan?: CurrentPlan | null; }): React.ReactNode { const t = useTranslations('workbench.resources'); const { data, mutate } = useSessionResources(sessionId); @@ -121,6 +123,7 @@ export function RuntimeTaskResourcesPanel({ return ( resourceItem(resource, t('localFile')))} onAddSource={(files) => { void addSources(files); diff --git a/packages/client/workbench/src/surface/workbench.tsx b/packages/client/workbench/src/surface/workbench.tsx index dbb85dffd..6f0798f0b 100644 --- a/packages/client/workbench/src/surface/workbench.tsx +++ b/packages/client/workbench/src/surface/workbench.tsx @@ -29,6 +29,7 @@ import type { ComposerAttachment, ComposerDirectiveControls, ConversationComposerController, + CurrentPlan, NewSessionDraft, NewSessionSubmission, PermissionDecision, @@ -39,6 +40,7 @@ import { extractPinnedGroup, failedComposerAttachmentFromPath, groupThreadsByWorkspace, + selectCurrentPlan, useKeyboardShortcutLabel, } from '@linkcode/ui'; import { noop } from 'foxact/noop'; @@ -234,6 +236,7 @@ function WorkbenchSessionSurface({ if (message) visibleResponseErrors.set(requestId, message); } const active = sessions.active; + const currentPlan: CurrentPlan | null = selectCurrentPlan(conversation); const { mentionItems, onMentionQueryChange } = useFileMentionSource(); const newSessionDefaultModels = useConfiguredDefaultModels(); const sdkClient = useWorkbenchSdkClient(); @@ -614,7 +617,9 @@ function WorkbenchSessionSurface({ return ( : undefined + activeSessionId ? ( + + ) : undefined } attachmentSupport={ATTACHMENT_SUPPORT} threadGroups={threadGroups} diff --git a/packages/presentation/i18n/src/locales/en.ts b/packages/presentation/i18n/src/locales/en.ts index d48e53f0b..83f1cee3f 100644 --- a/packages/presentation/i18n/src/locales/en.ts +++ b/packages/presentation/i18n/src/locales/en.ts @@ -352,6 +352,8 @@ export const en = { }, resources: { sources: 'Sources', + plan: 'PLAN', + planProgress: 'Plan step {current} of {total}', outputs: 'Outputs', emptySources: 'Attach files or connect apps', emptyOutputs: 'Create a file or site', diff --git a/packages/presentation/i18n/src/locales/zh-cn.ts b/packages/presentation/i18n/src/locales/zh-cn.ts index a10ad107a..4bccddd96 100644 --- a/packages/presentation/i18n/src/locales/zh-cn.ts +++ b/packages/presentation/i18n/src/locales/zh-cn.ts @@ -342,6 +342,8 @@ export const zhCN = { }, resources: { sources: '来源', + plan: '计划', + planProgress: '计划步骤 {current}/{total}', outputs: '输出', emptySources: '附加文件或连接应用', emptyOutputs: '创建文件或网站', diff --git a/packages/presentation/ui/src/shell/__tests__/question-prompt.test.tsx b/packages/presentation/ui/src/shell/__tests__/question-prompt.test.tsx index 2a32f0f53..5a8fd166d 100644 --- a/packages/presentation/ui/src/shell/__tests__/question-prompt.test.tsx +++ b/packages/presentation/ui/src/shell/__tests__/question-prompt.test.tsx @@ -562,6 +562,33 @@ describe('ConversationPromptDock', () => { ).toBe(true); }); + it('keeps actionable prompts when another surface presents the plan', () => { + const currentPlan: ConversationViewModel['items'][number] = { + kind: 'plan', + id: 'plan-1', + turnId: null, + plan: { + planId: 'plan-1', + entries: [{ content: 'Shown in Resources', priority: 'high', status: 'in_progress' }], + }, + }; + + render( + , + ); + + expect(screen.queryByText('Shown in Resources')).toBeNull(); + expect(screen.getByText('Pick features')).toBeDefined(); + }); + it('shows an authoritative busy state without a local response snapshot', () => { render( { expect(screen.getByText('recently')).toBeDefined(); }); + it('renders the current plan between sources and outputs', () => { + render( + , + ); + + const source = screen.getByText('brief.pdf'); + const plan = screen.getByText('plan'); + const output = screen.getByText('report.md'); + expect(source.compareDocumentPosition(plan)).toBe(Node.DOCUMENT_POSITION_FOLLOWING); + expect(plan.compareDocumentPosition(output)).toBe(Node.DOCUMENT_POSITION_FOLLOWING); + expect(screen.getByText('Build the panel')).toBeDefined(); + expect(screen.getByRole('progressbar', { name: 'planProgress' })).toBeDefined(); + }); + it('shows a failure reason and retries only the failed resource', () => { const onRetry = vi.fn(); const failed = { diff --git a/packages/presentation/ui/src/shell/conversation-prompt-dock.tsx b/packages/presentation/ui/src/shell/conversation-prompt-dock.tsx index d9a58146a..a0d3248c9 100644 --- a/packages/presentation/ui/src/shell/conversation-prompt-dock.tsx +++ b/packages/presentation/ui/src/shell/conversation-prompt-dock.tsx @@ -21,18 +21,20 @@ const EMPTY_RESPONSE_ERRORS = new Map(); /** Actionable prompts pinned above the composer: the current plan and pending permission/question asks. */ export function ConversationPromptDock({ conversation, + showPlan = true, respondingRequestIds, responseErrors = EMPTY_RESPONSE_ERRORS, onRespondPermission, onRespondQuestion, }: { conversation: ConversationViewModel; + showPlan?: boolean; respondingRequestIds: ReadonlySet; responseErrors?: ReadonlyMap; onRespondPermission: (requestId: string, decision: PermissionDecision) => void; onRespondQuestion: (requestId: string, outcome: QuestionOutcome) => void; }): React.ReactNode { - const plan = selectCurrentPlan(conversation); + const plan = showPlan ? selectCurrentPlan(conversation) : null; const pendingPrompts = selectPendingPromptItems(conversation); const currentPrompt = pendingPrompts.at(0); diff --git a/packages/presentation/ui/src/shell/conversation-surface.tsx b/packages/presentation/ui/src/shell/conversation-surface.tsx index 20eae10e0..504e34ef5 100644 --- a/packages/presentation/ui/src/shell/conversation-surface.tsx +++ b/packages/presentation/ui/src/shell/conversation-surface.tsx @@ -58,6 +58,8 @@ export interface ConversationSurfaceProps { mentionItems?: MentionItem[]; /** Reports the live `@` query so the app can fetch `mentionItems` for it. */ onMentionQueryChange?: (query: string | null) => void; + /** Keeps the compact plan above the composer when another visible surface does not own it. */ + showPlanInPromptDock?: boolean; onRespondPermission: (requestId: string, decision: PermissionDecision) => void; onRespondQuestion: (requestId: string, outcome: QuestionOutcome) => void; /** Opens a produced-file artifact in the shell's viewer (desktop right panel). Absent @@ -98,6 +100,7 @@ export function ConversationSurface({ TerminalBlockComponent, mentionItems, onMentionQueryChange, + showPlanInPromptDock = true, onRespondPermission, onRespondQuestion, onOpenFileArtifact, @@ -142,6 +145,7 @@ export function ConversationSurface({ void; + /** Whether the compact current plan remains above the composer. */ + showPlanInPromptDock?: boolean; /** Complete active-session composer behavior, forwarded atomically by every shell. */ conversationComposer: ConversationComposerController; onRespondPermission: (requestId: string, decision: PermissionDecision) => void; @@ -157,6 +159,7 @@ export function ShellFrame({ onTogglePreviewExpanded, mentionItems, onMentionQueryChange, + showPlanInPromptDock, conversationComposer, onRespondPermission, onRespondQuestion, @@ -253,6 +256,7 @@ export function ShellFrame({ TerminalBlockComponent={TerminalBlockComponent} mentionItems={mentionItems} onMentionQueryChange={(query) => onMentionQueryChange(active?.cwd, query)} + showPlanInPromptDock={showPlanInPromptDock} onRespondPermission={onRespondPermission} onRespondQuestion={onRespondQuestion} onHostArtifact={onHostArtifact} diff --git a/packages/presentation/ui/src/shell/task-resources-panel.tsx b/packages/presentation/ui/src/shell/task-resources-panel.tsx index 6b52a8480..018a73396 100644 --- a/packages/presentation/ui/src/shell/task-resources-panel.tsx +++ b/packages/presentation/ui/src/shell/task-resources-panel.tsx @@ -18,6 +18,10 @@ import { } from 'lucide-react'; import { useRef } from 'react'; import { useTranslations } from 'use-intl'; +import { CircularProgress } from '../chat/circular-progress'; +import type { CurrentPlan } from '../chat/conversation-prompts'; +import { StepItem } from '../chat/step'; +import { cn } from '../lib/cn'; import { useRelativeTimeLabel } from './use-relative-time-label'; export type ResourceDirection = 'source' | 'output'; @@ -40,6 +44,7 @@ export interface ResourceItem { export interface TaskResourcesPanelProps { resources: ResourceItem[]; + plan?: CurrentPlan | null; onAddSource?: (files: File[]) => void; onCreateOutput?: (kind: OutputResourceKind) => void; onOpen?: (resource: ResourceItem) => void; @@ -60,6 +65,7 @@ const KIND_ICONS: Record = { export function TaskResourcesPanel({ resources, + plan, onAddSource, onCreateOutput, onOpen, @@ -83,6 +89,7 @@ export function TaskResourcesPanel({ onRemove={onRemove} onRetry={onRetry} /> + {plan ? : null} +
+

{t('plan')}

+
+ + + {progress}/{plan.total} + +
+
+
+ {plan.item.plan.entries.map((entry, index) => ( + + {entry.content} + + ))} +
+ + ); +} + function ResourceSection({ direction, resources, From c2200c6c4916918a555bca6bf66ca9897cd72704 Mon Sep 17 00:00:00 2001 From: AprilNEA Date: Fri, 31 Jul 2026 17:53:13 +0000 Subject: [PATCH 2/2] fix(resources): deduplicate docked plan --- apps/desktop/src/renderer/src/shell/desktop-shell.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/src/shell/desktop-shell.tsx b/apps/desktop/src/renderer/src/shell/desktop-shell.tsx index c62e3f334..e6d31df03 100644 --- a/apps/desktop/src/renderer/src/shell/desktop-shell.tsx +++ b/apps/desktop/src/renderer/src/shell/desktop-shell.tsx @@ -310,7 +310,9 @@ export function DesktopShell({ rightPanelOpen: rightPanel.open, }); const resourcesFloatingOpen = resourcesPresentation === 'floating' && resourcesOpen; - const resourcesSurfaceOpen = resourcesPresentation !== 'hidden' && resourcesOpen; + const resourcesSurfaceOpen = + (resourcesPresentation !== 'hidden' && resourcesOpen) || + (rightPanel.open && rightPanel.activeSection === 'resources'); const titledSession = active?.title === undefined ? null : active; const hideMainTitle = draft !== null || (active === null ? false : titledSession === null); const isRunning = conversation.status === 'running' || conversation.status === 'starting';