mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-06 15:25:34 +00:00
* feat(web-shell): add contextual task panels * fix(web-shell): harden contextual task panels * fix(web-shell): preserve side task titles * fix(web-shell): address review feedback on context panels PR (#7929) - Add POST /session/:id/side-task to telemetry route catalog (51 routes) - Increase SDK browser bundle size limit to 184KB - Fix duplicated data-testid="chat-pane" → "chat-pane-container" on container - Gate sourceType behind session_source_metadata capability check - Add removeSession cleanup after killSession in !res.writable path - Add i18n key sideTask.renameFailed for error fallback - Add unit tests for selectVisibleHistoryRecords invariant * fix(cli): update telemetry-catalog route drift guard to 51 routes (#7929) * fix(web-shell): address review feedback round 2 on context panels PR (#7929) - Fix /fork sider discarding createSideTask() return value: show toast when side tasks are unavailable - Fix layout feedback loop: availableWidth no longer depends on environmentPanelVisible since the CSS overlay does not change the chat pane DOM width - Remove dead environmentPanelSuppressed state (never set to true) - Restore setArtifactPanelOpen(false) in closeArtifactPanelTab when the last tab is closed - Extract agentDisplayName(task) to a local variable to avoid triple invocation per render * fix(web-shell): dedupe completed background agents in environment panel (#7929) getEnvironmentAgentTasks correlated a transcript tool card with the live /tasks snapshot only on toolUseId, the notification taskId, and a <subagentType>-<callId> derived id. A completed background agent can lose that linkage (its live task carries no usable toolUseId and its daemon id is general-purpose-<internalId>), so the trailing loop appended the live task as a second entry. Add a conservative content fallback (prompt, or description+subagentType) mirroring the daemon's legacy resolver. * feat(web-shell): support side tasks during active turns * fix(web-shell): deduplicate completed subagents and gate sourceType on capability (#7929) * fix(web-shell): restore background agent reconciliation and fix agent dedupe (#7929) Restore the one-shot subagent reconciliation for inline background Agent tool cards. Persisted notification records do not always retain a toolUseId, so the SSE discrete-notification path alone can leave a card stuck in Running; the documented fallback resolves pending cards through the subagent endpoint after catch-up, reconnect, and terminal notifications. Also stop the loose description content fallback in getEnvironmentAgentTasks from claiming a live task that another transcript tool call already links precisely (by toolUseId, message taskId, or derived id). Two agents sharing a description previously collapsed into one: the fallback stole the linked task, its owner re-matched the same task, and the orphan was dropped. * fix(web-shell): address critical review feedback on context panels (#7929) * fix(web-shell): reconcile side-task state across sessions and listings (#7929) * fix(web-shell): preserve contextual panel fallbacks --------- Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com> Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com> Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
508 lines
14 KiB
TypeScript
508 lines
14 KiB
TypeScript
import {
|
|
createContext,
|
|
useContext,
|
|
type CSSProperties,
|
|
type ComponentType,
|
|
type ReactNode,
|
|
} from 'react';
|
|
import type { Components, Options } from 'react-markdown';
|
|
import type {
|
|
ChartRendererRegistry,
|
|
MarkdownChartLabelOverrides,
|
|
} from '@datafe-open/markdown-chart';
|
|
import type { MarkdownChartReactErrorHandler } from '@datafe-open/markdown-chart-react';
|
|
import type { DaemonInputAnnotation } from '@qwen-code/sdk/daemon';
|
|
import type { DaemonStreamingState } from '@qwen-code/webui/daemon-react-sdk';
|
|
import type { ACPToolCall } from './adapters/types';
|
|
import type { WelcomeHeaderProps } from './components/WelcomeHeader';
|
|
import type { WebShellTheme } from './themeContext';
|
|
|
|
export type MarkdownContentSource = 'assistant' | 'thinking';
|
|
|
|
export interface MarkdownRenderContext {
|
|
source: MarkdownContentSource;
|
|
}
|
|
|
|
export interface WebShellCodeBlockRenderInfo {
|
|
/**
|
|
* Raw fenced-code language from the markdown class name, restricted to safe
|
|
* fence-language characters.
|
|
*/
|
|
language: string;
|
|
/**
|
|
* Canonical Shiki language id after applying built-in aliases, or `text`
|
|
* when the language is unsupported by the fallback highlighter.
|
|
*/
|
|
resolvedLanguage: string;
|
|
className?: string;
|
|
code: string;
|
|
/** True while the assistant message is still streaming partial content. */
|
|
isStreaming: boolean;
|
|
/** True only when this block is the active unterminated tail fence. */
|
|
isIncomplete: boolean;
|
|
source: MarkdownContentSource;
|
|
theme: WebShellTheme;
|
|
}
|
|
|
|
/**
|
|
* Return a React node to replace the default code block rendering. Return
|
|
* `null`, `undefined`, or `false` to decline and fall back to the built-in code
|
|
* block renderer. Expensive renderers should defer parsing while
|
|
* `info.isIncomplete` is true; `info.isStreaming` describes the surrounding
|
|
* assistant message and may remain true after this block has closed.
|
|
*/
|
|
export type CodeBlockRenderer = (
|
|
info: WebShellCodeBlockRenderInfo,
|
|
) => ReactNode | null | undefined;
|
|
|
|
export interface WebShellMarkdownChartCustomization {
|
|
registry: ChartRendererRegistry;
|
|
loadingLabel?: string;
|
|
labels?: MarkdownChartLabelOverrides;
|
|
onError?: MarkdownChartReactErrorHandler;
|
|
chartClassName?: string;
|
|
chartStyle?: CSSProperties;
|
|
}
|
|
|
|
export interface WebShellMarkdownCustomization {
|
|
transformMarkdown?: (
|
|
markdown: string,
|
|
context: MarkdownRenderContext,
|
|
) => string;
|
|
/**
|
|
* Override Web Shell's built-in Markdown chart registry or its presentation
|
|
* callbacks. The complete chart customization object must remain
|
|
* referentially stable while a chart is mounted.
|
|
*/
|
|
chart?: WebShellMarkdownChartCustomization;
|
|
renderCodeBlock?: CodeBlockRenderer;
|
|
/**
|
|
* Custom markdown components override Web Shell's built-ins. In particular,
|
|
* `components.code` replaces the default code renderer, so `renderCodeBlock`
|
|
* will not be called for that source.
|
|
*/
|
|
components?: Components;
|
|
remarkPlugins?: Options['remarkPlugins'];
|
|
rehypePlugins?: Options['rehypePlugins'];
|
|
}
|
|
|
|
export type MarkdownTableMode = 'basic' | 'advanced';
|
|
|
|
export type ToolHeaderKind =
|
|
| 'agent'
|
|
| 'ask'
|
|
| 'edit'
|
|
| 'fetch'
|
|
| 'read'
|
|
| 'shell'
|
|
| 'todo'
|
|
| 'write'
|
|
| 'other';
|
|
|
|
export interface ToolHeaderExtraRenderInfo {
|
|
kind: ToolHeaderKind;
|
|
tool: ACPToolCall;
|
|
displayName: string;
|
|
description: string;
|
|
elapsed: string;
|
|
workspaceCwd?: string;
|
|
}
|
|
|
|
export type ToolHeaderExtraRenderer = (
|
|
info: ToolHeaderExtraRenderInfo,
|
|
) => ReactNode;
|
|
|
|
export type WelcomeHeaderRenderer = (props: WelcomeHeaderProps) => ReactNode;
|
|
export type WelcomeFooterRenderer = (props: WelcomeHeaderProps) => ReactNode;
|
|
|
|
export type WebShellChatHeaderItem = 'title' | 'environment' | 'rightPanel';
|
|
|
|
export interface WebShellChatHeaderOptions {
|
|
/** Built-in header actions to show. Defaults to all actions. */
|
|
items?: readonly WebShellChatHeaderItem[];
|
|
}
|
|
|
|
export type WebShellRightPanelItem = 'review' | 'sideTask';
|
|
|
|
export interface WebShellRightPanelOptions {
|
|
/** Empty-state actions to show. Defaults to all actions. */
|
|
items?: readonly WebShellRightPanelItem[];
|
|
}
|
|
|
|
export type WebShellEnvironmentPanelItem =
|
|
| 'environment'
|
|
| 'subagents'
|
|
| 'backgroundTasks';
|
|
|
|
export interface WebShellEnvironmentPanelOptions {
|
|
/** Sections to show. Defaults to all sections. */
|
|
items?: readonly WebShellEnvironmentPanelItem[];
|
|
}
|
|
|
|
/** Context passed to the chat header renderer. */
|
|
export interface ChatHeaderRenderInfo {
|
|
/** Current session id, if connected. */
|
|
sessionId?: string;
|
|
/** Display name for the current session. */
|
|
sessionName?: string;
|
|
/** Workspace cwd for the current session. */
|
|
workspaceCwd?: string;
|
|
/** Header actions enabled by the host. */
|
|
items: readonly WebShellChatHeaderItem[];
|
|
/** Whether the environment panel is currently open. */
|
|
environmentPanelOpen: boolean;
|
|
/** Whether the right extension panel is currently open. */
|
|
rightPanelOpen: boolean;
|
|
/** Opens or closes the environment panel. */
|
|
onEnvironmentPanelOpenChange: (open: boolean) => void;
|
|
/** Opens or closes the right extension panel. */
|
|
onRightPanelOpenChange: (open: boolean) => void;
|
|
}
|
|
|
|
/**
|
|
* Replaces the complete persistent chat header. Only rendered when a session
|
|
* is active (not in the welcome/empty state).
|
|
*/
|
|
export type ChatHeaderRenderer = (info: ChatHeaderRenderInfo) => ReactNode;
|
|
|
|
export interface UserMessageContentRenderInfo {
|
|
content: string;
|
|
images?: readonly { data: string; mimeType: string }[];
|
|
inputAnnotations?: readonly DaemonInputAnnotation[];
|
|
}
|
|
|
|
export type UserMessageContentRenderer = (
|
|
info: UserMessageContentRenderInfo,
|
|
) => ReactNode;
|
|
|
|
export interface WebShellBottomStatusItem {
|
|
id: string;
|
|
label: ReactNode;
|
|
title?: string;
|
|
ariaLabel?: string;
|
|
onClick?: () => void;
|
|
}
|
|
|
|
export type WebShellIconSource = string;
|
|
|
|
export interface WebShellAssistantMessageInfo {
|
|
id: string;
|
|
content: string;
|
|
isStreaming?: boolean;
|
|
timestamp?: number;
|
|
}
|
|
|
|
export interface WebShellAssistantTurnFooterRenderInfo {
|
|
/** User-message id for the head of the completed turn. */
|
|
turnId: string;
|
|
message: WebShellAssistantMessageInfo;
|
|
}
|
|
|
|
export type AssistantTurnFooterRenderer = (
|
|
info: WebShellAssistantTurnFooterRenderInfo,
|
|
) => ReactNode | null | undefined;
|
|
|
|
export type WebShellBuiltinComposerTagKind =
|
|
| 'extension'
|
|
| 'mcp'
|
|
| 'file'
|
|
| 'skill';
|
|
|
|
export type WebShellComposerTagKind =
|
|
| WebShellBuiltinComposerTagKind
|
|
| (string & {});
|
|
|
|
export type WebShellComposerTagIconMap = Readonly<Record<string, string>>;
|
|
|
|
export interface WebShellComposerTag {
|
|
id: string;
|
|
label?: string;
|
|
value?: string;
|
|
removable?: boolean;
|
|
kind?: WebShellComposerTagKind;
|
|
icon?: WebShellIconSource;
|
|
metadata?: unknown;
|
|
serialized?: string;
|
|
}
|
|
|
|
export type WebShellComposerTagPlacementContext = 'composer' | 'user-message';
|
|
|
|
export interface WebShellComposerTagRenderInfo {
|
|
tag: WebShellComposerTag;
|
|
placement: WebShellComposerTagPlacementContext;
|
|
readonly: boolean;
|
|
anchorRect?: DOMRectReadOnly;
|
|
}
|
|
|
|
/**
|
|
* Custom composer tag content. Inline composer tags are mounted from
|
|
* CodeMirror-managed React roots, so JSX returned for inline tags must not
|
|
* depend on React context from the surrounding app tree.
|
|
*/
|
|
export type ComposerTagRenderer = (
|
|
info: WebShellComposerTagRenderInfo,
|
|
) => ReactNode | null | undefined;
|
|
|
|
export type ComposerTagClickHandler = (
|
|
info: WebShellComposerTagRenderInfo,
|
|
) => void;
|
|
|
|
export type WebShellUserMessagePart =
|
|
| { type: 'text'; text: string }
|
|
| {
|
|
type: 'tag';
|
|
tag: WebShellComposerTag;
|
|
sourceRange?: readonly [number, number];
|
|
};
|
|
|
|
export type UserMessageContentParser = (
|
|
content: string,
|
|
) => readonly WebShellUserMessagePart[] | undefined | null;
|
|
|
|
export type WebShellComposerTagPlacement = 'top' | 'inline';
|
|
|
|
export interface WebShellComposerTagOptions {
|
|
placement?: WebShellComposerTagPlacement;
|
|
}
|
|
|
|
export interface WebShellComposerTextOptions {
|
|
mode?: 'append' | 'replace';
|
|
}
|
|
|
|
export interface WebShellComposerInput {
|
|
text?: string;
|
|
tags?: readonly WebShellComposerTag[];
|
|
tagPlacement?: WebShellComposerTagPlacement;
|
|
submit?: boolean;
|
|
}
|
|
|
|
export interface WebShellAtItem {
|
|
id: string;
|
|
label: string;
|
|
subtitle?: string;
|
|
description?: string;
|
|
detail?: string;
|
|
icon?: WebShellIconSource;
|
|
iconMode?: 'mask' | 'image';
|
|
iconColor?: string;
|
|
iconSpin?: boolean;
|
|
iconTooltip?: string;
|
|
insertText?: string;
|
|
composerTag?: WebShellComposerTag;
|
|
}
|
|
|
|
export type WebShellBuiltinAtProviderId =
|
|
| 'files'
|
|
| 'extensions'
|
|
| 'mcp-resources';
|
|
|
|
export type WebShellBuiltinAtProvidersConfig =
|
|
| boolean
|
|
| readonly WebShellBuiltinAtProviderId[]
|
|
| {
|
|
enabled?: boolean;
|
|
include?: readonly WebShellBuiltinAtProviderId[];
|
|
exclude?: readonly WebShellBuiltinAtProviderId[];
|
|
};
|
|
|
|
export interface WebShellAtProviderTab {
|
|
id: string;
|
|
label: ReactNode;
|
|
textValue?: string;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export interface WebShellAtItemRenderInfo {
|
|
item: WebShellAtItem;
|
|
provider: WebShellAtProvider;
|
|
selected: boolean;
|
|
}
|
|
|
|
export type WebShellAtItemRenderer = (
|
|
info: WebShellAtItemRenderInfo,
|
|
) => ReactNode | null | undefined;
|
|
|
|
export interface WebShellAtProvider {
|
|
id: string;
|
|
label: ReactNode;
|
|
textValue?: string;
|
|
description?: string;
|
|
order?: number;
|
|
tabs?: readonly WebShellAtProviderTab[];
|
|
renderItem?: WebShellAtItemRenderer;
|
|
search(params: {
|
|
query: string;
|
|
signal: AbortSignal;
|
|
tabId?: string;
|
|
}): Promise<readonly WebShellAtItem[]>;
|
|
}
|
|
|
|
export interface WebShellComposerApi {
|
|
insertText(text: string, options?: WebShellComposerTextOptions): void;
|
|
setText(text: string): void;
|
|
addTags(
|
|
tags: readonly WebShellComposerTag[],
|
|
options?: WebShellComposerTagOptions,
|
|
): void;
|
|
removeTag(id: string): void;
|
|
/** Clears text and/or top tags. Inline tags are part of the editor text. */
|
|
clear(options?: { text?: boolean; tags?: boolean }): void;
|
|
submit(input?: WebShellComposerInput): void;
|
|
}
|
|
|
|
export interface WebShellComposerToolbarRenderInfo {
|
|
disabled: boolean;
|
|
isRunning: boolean;
|
|
currentMode: string;
|
|
currentModel: string;
|
|
sessionName?: string;
|
|
}
|
|
|
|
export type WebShellComposerToolbarStartRenderInfo =
|
|
WebShellComposerToolbarRenderInfo;
|
|
|
|
export type WebShellComposerToolbarRightRenderInfo =
|
|
WebShellComposerToolbarRenderInfo;
|
|
|
|
export type ComposerToolbarStartRenderer =
|
|
ComponentType<WebShellComposerToolbarStartRenderInfo>;
|
|
|
|
export type ComposerToolbarEndRenderer =
|
|
ComponentType<WebShellComposerToolbarRenderInfo>;
|
|
|
|
export type ComposerToolbarRightRenderer =
|
|
ComponentType<WebShellComposerToolbarRightRenderInfo>;
|
|
|
|
export type ComposerHeaderRenderer =
|
|
ComponentType<WebShellComposerToolbarRenderInfo>;
|
|
|
|
export type ComposerFooterRenderer =
|
|
ComponentType<WebShellComposerToolbarRenderInfo>;
|
|
|
|
// ---- Background task info (public type for footer renderer) ----
|
|
|
|
interface WebShellTaskBase {
|
|
id: string;
|
|
label: string;
|
|
description: string;
|
|
runtimeMs: number;
|
|
startTime: number;
|
|
endTime?: number;
|
|
error?: string;
|
|
}
|
|
|
|
export interface WebShellAgentTask extends WebShellTaskBase {
|
|
kind: 'agent';
|
|
status: 'running' | 'paused' | 'completed' | 'failed' | 'cancelled';
|
|
subagentType?: string;
|
|
isBackgrounded: boolean;
|
|
prompt?: string;
|
|
}
|
|
|
|
export interface WebShellShellTask extends WebShellTaskBase {
|
|
kind: 'shell';
|
|
status: 'running' | 'completed' | 'failed' | 'cancelled';
|
|
command: string;
|
|
cwd: string;
|
|
pid?: number;
|
|
exitCode?: number;
|
|
}
|
|
|
|
export interface WebShellMonitorTask extends WebShellTaskBase {
|
|
kind: 'monitor';
|
|
status: 'running' | 'completed' | 'failed' | 'cancelled';
|
|
command: string;
|
|
pid?: number;
|
|
exitCode?: number;
|
|
}
|
|
|
|
export type WebShellTaskInfo =
|
|
| WebShellAgentTask
|
|
| WebShellShellTask
|
|
| WebShellMonitorTask;
|
|
|
|
// ---- Model info (public type for footer renderer) ----
|
|
|
|
export interface WebShellModelInfo {
|
|
id: string;
|
|
label?: string;
|
|
contextWindow?: number;
|
|
}
|
|
|
|
// ---- Skill info (public type for footer renderer) ----
|
|
|
|
export interface WebShellSkillInfo {
|
|
name: string;
|
|
description: string;
|
|
}
|
|
|
|
// ---- Footer renderer ----
|
|
|
|
export interface WebShellFooterRenderInfo {
|
|
connected: boolean;
|
|
mode: string;
|
|
model: string;
|
|
streamingState: DaemonStreamingState;
|
|
contextUsageRatio: number;
|
|
activeGoal: { condition: string; setAt: number } | null;
|
|
tasks: readonly WebShellTaskInfo[];
|
|
availableModes: readonly string[];
|
|
availableModels: readonly WebShellModelInfo[];
|
|
skills: readonly WebShellSkillInfo[];
|
|
|
|
onSelectMode: (mode: string) => void;
|
|
onSelectModel: (model: string) => void;
|
|
}
|
|
|
|
export type FooterRenderer = ComponentType<WebShellFooterRenderInfo>;
|
|
|
|
// ---- Loading phrases ----
|
|
|
|
/**
|
|
* Resolves the witty phrases cycled while a prompt is streaming. Receives the
|
|
* resolved UI language. Return phrases to override the built-in defaults, an
|
|
* empty array to hide the phrase entirely, or `undefined`/`null` to fall back
|
|
* to the built-in defaults for that language.
|
|
*/
|
|
export type LoadingPhrasesResolver = (
|
|
language: string,
|
|
) => readonly string[] | undefined | null;
|
|
|
|
export interface WebShellCustomization {
|
|
renderToolHeaderExtra?: ToolHeaderExtraRenderer;
|
|
renderWelcomeHeader?: WelcomeHeaderRenderer;
|
|
renderWelcomeFooter?: WelcomeFooterRenderer;
|
|
parseUserMessageContent?: UserMessageContentParser;
|
|
renderUserMessageContent?: UserMessageContentRenderer;
|
|
composerTagIcons?: WebShellComposerTagIconMap;
|
|
renderComposerTag?: ComposerTagRenderer;
|
|
renderComposerTagTooltip?: ComposerTagRenderer;
|
|
onComposerTagClick?: ComposerTagClickHandler;
|
|
renderAssistantTurnFooter?: AssistantTurnFooterRenderer;
|
|
renderComposerToolbarStart?: ComposerToolbarStartRenderer;
|
|
renderComposerToolbarEnd?: ComposerToolbarEndRenderer;
|
|
renderComposerToolbarRight?: ComposerToolbarRightRenderer;
|
|
renderComposerHeader?: ComposerHeaderRenderer;
|
|
renderComposerFooter?: ComposerFooterRenderer;
|
|
renderFooter?: FooterRenderer;
|
|
compactThinking?: boolean;
|
|
/**
|
|
* Auto-collapse each completed turn's intermediate steps (thinking, tool
|
|
* calls, mid-turn assistant text) behind a toggle on the prompt row, leaving
|
|
* just the prompt and the final answer. The active turn always stays
|
|
* expanded. Defaults to enabled when unset.
|
|
*/
|
|
collapseCompletedTurns?: boolean;
|
|
markdownTableMode?: MarkdownTableMode;
|
|
markdown?: WebShellMarkdownCustomization;
|
|
loadingPhrases?: LoadingPhrasesResolver;
|
|
}
|
|
|
|
const WebShellCustomizationContext = createContext<WebShellCustomization>({});
|
|
|
|
export const WebShellCustomizationProvider =
|
|
WebShellCustomizationContext.Provider;
|
|
|
|
export function useWebShellCustomization(): WebShellCustomization {
|
|
return useContext(WebShellCustomizationContext);
|
|
}
|