mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* feat(web-shell): Add workspace file browser and previews * fix(web-shell): Restore source preview CI fixtures and notices * fix(web-shell): Address file pane review blockers * fix(web-shell): Resolve review regressions and strengthen file checks --------- Co-authored-by: 钉萁 <[email protected]>
786 lines
24 KiB
TypeScript
786 lines
24 KiB
TypeScript
import type { ArtifactFilter } from './components/artifacts/TurnOutputs';
|
||
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,
|
||
DaemonSessionArtifact,
|
||
DaemonSessionAttachmentReference,
|
||
SessionSource,
|
||
GoalSnapshotV2,
|
||
} from '@qwen-code/sdk/daemon';
|
||
import type { DaemonStreamingState } from '@qwen-code/web-shell/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;
|
||
/**
|
||
* 当前消息的生成态;历史或静态内容为 false。所属 MessageList 空闲
|
||
* (isResponding=false)时,恢复 replay 中残留的 streaming 标记也会被
|
||
* 收口为 false,且已收口的行在会话重新响应时、内容不变的前提下保持收口。
|
||
*/
|
||
isStreaming: boolean;
|
||
}
|
||
|
||
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 WebShellFootnote {
|
||
readonly id: string;
|
||
readonly number: number;
|
||
readonly definitionMarkdown: string;
|
||
readonly title?: string;
|
||
readonly summary: string;
|
||
readonly href?: string;
|
||
readonly source?: string;
|
||
readonly image?: string;
|
||
}
|
||
|
||
/** Synchronous, side-effect-free selection for a complete group of footnotes. */
|
||
export type WebShellFootnoteIconResolver = (
|
||
footnotes: readonly WebShellFootnote[],
|
||
) => WebShellIconSource | null | undefined;
|
||
|
||
export type WebShellSource =
|
||
| { readonly type: 'source'; readonly source: SessionSource }
|
||
| {
|
||
readonly type: 'attachment';
|
||
readonly attachment: DaemonSessionAttachmentReference;
|
||
};
|
||
|
||
export interface WebShellSourceReference {
|
||
readonly sessionId: string;
|
||
readonly turnId: string;
|
||
readonly sourceId: string;
|
||
}
|
||
|
||
export type WebShellSourceIconResolver = (
|
||
sources: readonly WebShellSource[],
|
||
) => WebShellIconSource | null | undefined;
|
||
|
||
export interface WebShellFootnotePreviewInfo {
|
||
readonly footnotes: readonly WebShellFootnote[];
|
||
readonly footnote: WebShellFootnote;
|
||
readonly index: number;
|
||
readonly title: string;
|
||
readonly sourceLabel: string;
|
||
/** Place this Qwen-managed element in the layout without replacing its children. */
|
||
readonly sourceLink: HTMLElement;
|
||
}
|
||
|
||
export interface WebShellFootnotePreviewHandle {
|
||
update(info: WebShellFootnotePreviewInfo): void;
|
||
dispose(): void;
|
||
}
|
||
|
||
/** Mounts current-page content only. Keep this synchronous function stable. */
|
||
export type WebShellFootnotePreviewMount = (
|
||
container: HTMLElement,
|
||
info: WebShellFootnotePreviewInfo,
|
||
) => WebShellFootnotePreviewHandle | null | undefined;
|
||
|
||
export interface WebShellMarkdownCustomization {
|
||
getInlineFootnoteIcon?: WebShellFootnoteIconResolver;
|
||
mountFootnotePreview?: WebShellFootnotePreviewMount;
|
||
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. Internal footnote references and
|
||
* backreferences keep the built-in link behavior. Providing `components.sup`
|
||
* disables footnote grouping.
|
||
*/
|
||
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'
|
||
| 'tokenUsage'
|
||
| 'contextUsage';
|
||
|
||
export interface WebShellChatHeaderOptions {
|
||
/** Show the mobile-access QR entry in chat headers. Defaults to false. */
|
||
showMobileAccess?: boolean;
|
||
/** Built-in header actions to show. Token and context usage are opt-in. */
|
||
items?: readonly WebShellChatHeaderItem[];
|
||
}
|
||
|
||
export type WebShellRightPanelItem =
|
||
| 'files'
|
||
| 'review'
|
||
| 'sideTask'
|
||
| 'terminal'
|
||
| 'webPreview'
|
||
| 'trajectory';
|
||
|
||
export interface WebShellRightPanelOptions {
|
||
/** Empty-state actions to show. Defaults to review and sideTask. */
|
||
items?: readonly WebShellRightPanelItem[];
|
||
}
|
||
|
||
export type WebShellEnvironmentPanelItem =
|
||
| 'environment'
|
||
| 'sources'
|
||
| 'subagents'
|
||
| 'backgroundTasks'
|
||
/** Legacy attachment-only view in the Sources section. */
|
||
| 'attachments'
|
||
| 'artifacts';
|
||
|
||
export interface WebShellEnvironmentPanelOptions {
|
||
/**
|
||
* Panel sections to show. Sources includes attachments; both keys render one
|
||
* section. Omitting both keys does not disable the turn source footer or its
|
||
* metadata loading.
|
||
*/
|
||
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;
|
||
/** Opens token usage for the current session, when available. */
|
||
onOpenTokenUsage?: () => void;
|
||
/** Opens context usage for the current session, when available. */
|
||
onOpenContextUsage?: () => void;
|
||
/** Opens Settings deep-linked to Local Control (Daemon category). */
|
||
onOpenLocalControlSettings?: () => 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 }[];
|
||
files?: readonly {
|
||
name: string;
|
||
mimeType: string;
|
||
data?: Blob;
|
||
text?: string;
|
||
attachmentId?: string;
|
||
}[];
|
||
inputAnnotations?: readonly DaemonInputAnnotation[];
|
||
}
|
||
|
||
export interface WebShellPreparedSubmit {
|
||
prompt: string;
|
||
inputAnnotations?: readonly DaemonInputAnnotation[];
|
||
}
|
||
|
||
export interface WebShellSubmitSnapshot {
|
||
sessionId?: string;
|
||
prompt: 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 type WebShellAssistantTurnOutcome = 'completed' | 'cancelled' | 'failed';
|
||
|
||
export interface WebShellAssistantTurnSettledEvent {
|
||
sessionId: string;
|
||
/** Daemon terminal prompt identifier and stable host idempotency key. */
|
||
promptId: string;
|
||
outcome: WebShellAssistantTurnOutcome;
|
||
/** Daemon terminal reason. Present for completed and cancelled turns. */
|
||
stopReason?: string;
|
||
/** Final visible assistant message when retained in the mounted transcript. */
|
||
message?: WebShellAssistantMessageInfo;
|
||
error?: {
|
||
message: string;
|
||
code?: string;
|
||
};
|
||
}
|
||
|
||
export interface WebShellAssistantTurnFooterRenderInfo {
|
||
/** User-message id for the head of the completed turn. */
|
||
turnId: string;
|
||
message: WebShellAssistantMessageInfo;
|
||
}
|
||
|
||
export type WebShellSessionArtifactsChangeReason = 'restore' | 'change';
|
||
|
||
export interface WebShellSessionArtifactsChange {
|
||
reason: WebShellSessionArtifactsChangeReason;
|
||
sessionId: string;
|
||
sequence: number;
|
||
artifacts: readonly DaemonSessionArtifact[];
|
||
artifactsByTurn: ReadonlyMap<string, readonly DaemonSessionArtifact[]>;
|
||
}
|
||
|
||
export type AssistantTurnFooterRenderer = (
|
||
info: WebShellAssistantTurnFooterRenderInfo,
|
||
) => ReactNode | null | undefined;
|
||
|
||
export type WebShellAssistantFeedbackRating = 'up' | 'down';
|
||
|
||
/** The prompt that started a marked turn. */
|
||
export interface WebShellAssistantFeedbackUserMessage {
|
||
/**
|
||
* The prompt's most recent characters, or a placeholder naming what the
|
||
* prompt carried when it had no text at all.
|
||
*/
|
||
text: string;
|
||
/** Wall-clock epoch ms of the prompt, when the transcript knows it. */
|
||
timestamp?: number;
|
||
}
|
||
|
||
export interface WebShellAssistantFeedbackInfo {
|
||
/** The new mark; `null` when the user cleared it by clicking the lit icon. */
|
||
rating: WebShellAssistantFeedbackRating | null;
|
||
/** The mark the turn carried before this change, when there was one. */
|
||
previousRating?: WebShellAssistantFeedbackRating;
|
||
/** The session the marked turn belongs to. */
|
||
sessionId?: string;
|
||
/**
|
||
* Admitted prompt id of the marked turn. This is the daemon's own per-turn
|
||
* identity: unlike a transcript block id it survives a reload, and it is
|
||
* what the daemon addresses a turn by (`/session/:id/turns/:promptId`).
|
||
*/
|
||
promptId: string;
|
||
/** The prompt that started the marked turn. */
|
||
userMessage: WebShellAssistantFeedbackUserMessage;
|
||
}
|
||
|
||
export type AssistantFeedbackHandler = (
|
||
info: WebShellAssistantFeedbackInfo,
|
||
) => void;
|
||
|
||
export interface WebShellAssistantFeedbackOptions {
|
||
/** Passing the object shows the icons; `false` hides them again. */
|
||
enabled?: boolean;
|
||
/**
|
||
* Notified after every change, including clearing a mark. This is a
|
||
* notification, not a gate: the icon is already lit (or dark) when it runs,
|
||
* and neither a throw nor a later rejection changes that.
|
||
*/
|
||
onRate?: AssistantFeedbackHandler;
|
||
}
|
||
|
||
/** Return custom artifact artwork, or null/undefined/false for the built-in icon. */
|
||
export type ArtifactImageRenderer = (
|
||
artifact: DaemonSessionArtifact,
|
||
) => ReactNode | null | undefined;
|
||
|
||
export interface WebShellArtifactCustomization {
|
||
renderImage?: ArtifactImageRenderer;
|
||
}
|
||
|
||
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;
|
||
/**
|
||
* Inline placement only: insert at the caret (default, synchronous user
|
||
* gestures) or append after the document end (asynchronous producers,
|
||
* which must not interrupt typing or steal focus).
|
||
*/
|
||
position?: 'caret' | 'end';
|
||
}
|
||
|
||
export interface WebShellComposerTextOptions {
|
||
mode?: 'append' | 'replace';
|
||
}
|
||
|
||
export interface WebShellComposerInput {
|
||
text?: string;
|
||
tags?: readonly WebShellComposerTag[];
|
||
tagPlacement?: WebShellComposerTagPlacement;
|
||
clearAttachments?: boolean;
|
||
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;
|
||
/**
|
||
* Makes the item an action instead of a reference: choosing it removes the
|
||
* typed `@query` and calls this, inserting nothing.
|
||
*/
|
||
onSelect?: () => void;
|
||
}
|
||
|
||
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;
|
||
/**
|
||
* Claims a typed `@query` that names no category, so it searches this
|
||
* provider instead of falling back to files.
|
||
*/
|
||
claimsTypedQuery?(query: string): boolean;
|
||
search(params: {
|
||
query: string;
|
||
signal: AbortSignal;
|
||
tabId?: string;
|
||
}): Promise<readonly WebShellAtItem[]>;
|
||
}
|
||
|
||
export interface WebShellComposerApi {
|
||
focus?(): void;
|
||
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 interface WebShellWorkflowTask extends WebShellTaskBase {
|
||
kind: 'workflow';
|
||
status:
|
||
| 'running'
|
||
| 'pausing'
|
||
| 'paused'
|
||
| 'completed'
|
||
| 'failed'
|
||
| 'cancelled';
|
||
currentPhase?: string;
|
||
agentsDispatched: number;
|
||
agentsCompleted: number;
|
||
tokensSpent: number;
|
||
tokenBudgetTotal?: number;
|
||
}
|
||
|
||
export type WebShellTaskInfo =
|
||
| WebShellAgentTask
|
||
| WebShellShellTask
|
||
| WebShellMonitorTask
|
||
| WebShellWorkflowTask;
|
||
|
||
// ---- 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;
|
||
/** Canonical daemon-owned Goal state for the active session. */
|
||
goalSnapshot: GoalSnapshotV2 | null;
|
||
/** @deprecated Prefer goalSnapshot. */
|
||
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 {
|
||
filterArtifact?: ArtifactFilter;
|
||
artifact?: WebShellArtifactCustomization;
|
||
/** Host-specific label for the Ask User Question free-text choice. */
|
||
askUserFreeTextLabel?: string;
|
||
renderToolHeaderExtra?: ToolHeaderExtraRenderer;
|
||
renderWelcomeHeader?: WelcomeHeaderRenderer;
|
||
renderWelcomeFooter?: WelcomeFooterRenderer;
|
||
parseUserMessageContent?: UserMessageContentParser;
|
||
renderUserMessageContent?: UserMessageContentRenderer;
|
||
composerTagIcons?: WebShellComposerTagIconMap;
|
||
/**
|
||
* Built-in / host @ mention providers. Split-view panes share this context
|
||
* so they match the main composer without ChatPane prop drilling.
|
||
*/
|
||
builtinAtProviders?: WebShellBuiltinAtProvidersConfig;
|
||
atProviders?: readonly WebShellAtProvider[];
|
||
renderComposerTag?: ComposerTagRenderer;
|
||
renderComposerTagTooltip?: ComposerTagRenderer;
|
||
onComposerTagClick?: ComposerTagClickHandler;
|
||
renderAssistantTurnFooter?: AssistantTurnFooterRenderer;
|
||
/**
|
||
* Satisfied / not-satisfied marks on each completed assistant turn. Omit the
|
||
* object to leave Web Shell's answer footer unchanged.
|
||
*/
|
||
assistantFeedback?: WebShellAssistantFeedbackOptions;
|
||
getAssistantSourcesIcon?: WebShellSourceIconResolver;
|
||
sourceReferences?: readonly WebShellSourceReference[];
|
||
renderComposerToolbarStart?: ComposerToolbarStartRenderer;
|
||
renderComposerToolbarEnd?: ComposerToolbarEndRenderer;
|
||
renderComposerToolbarRight?: ComposerToolbarRightRenderer;
|
||
renderComposerHeader?: ComposerHeaderRenderer;
|
||
renderComposerFooter?: ComposerFooterRenderer;
|
||
renderFooter?: FooterRenderer;
|
||
compactThinking?: boolean;
|
||
hostOwnsEditDiffPreview?: 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;
|
||
/**
|
||
* Controls whether the composer's file-upload entry points (drag-and-drop
|
||
* and the @ panel upload item) are enabled. Does not disable attachments.
|
||
* Works alongside the daemon's
|
||
* `workspace_file_upload` capability, not instead of it: setting `false`
|
||
* force-disables upload even when the daemon advertises the capability,
|
||
* while `true`/omitted still requires the capability (and the workspace
|
||
* trust / qualified-route safety checks) to be satisfied.
|
||
*/
|
||
fileUploadEnabled?: boolean;
|
||
/** Preferred file-drop destination. Omitted: ask only when both are available.
|
||
* If the preference is unavailable, use the sole available destination.
|
||
*/
|
||
fileDropAction?: 'upload' | 'attach';
|
||
/**
|
||
* Directory that drag-and-dropped files upload into, **relative to the
|
||
* workspace root**. Use a relative path WITHOUT a leading `/` — e.g.
|
||
* `'uploads'`, `'uploads/images'`, or omit it to upload into the
|
||
* workspace root (the default). A leading-slash path like `'/uploads'`
|
||
* is rejected by the daemon as outside the workspace. The directory
|
||
* (including intermediate components) is created automatically on upload
|
||
* when it does not exist.
|
||
*/
|
||
fileUploadDirectory?: string;
|
||
}
|
||
|
||
const WebShellCustomizationContext = createContext<WebShellCustomization>({});
|
||
|
||
export const WebShellCustomizationProvider =
|
||
WebShellCustomizationContext.Provider;
|
||
|
||
export function useWebShellCustomization(): WebShellCustomization {
|
||
return useContext(WebShellCustomizationContext);
|
||
}
|