Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/design/web-shell-attachment-cards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Web Shell attachment and file reference cards

Attachments currently render as single-line chips. Render pending and sent file
attachments as rounded cards with an inset file-type icon and a filename/type
stack, matching the supplied reference. Workspace upload progress retains its
status and cancel action. Completed uploads still insert ordinary file references.

File references remain compact icon-and-title chips in the composer and sent
messages, sharing the existing FileTypeIcon mapping used by artifacts. Preserve
custom renderers/icons, directory semantics, full-path serialization, preview,
removal, keyboard access, image thumbnails, and theme isolation.

Changes are confined to Web Shell presentation, shared attachment content, and
focused regression tests. No daemon, transport, or artifact behavior changes.

FileTypeIcon shares the existing artifact
component's format resolver and URL map; formats without specific artwork keep
the existing Lucide fallback. The outer SVG preserves existing sizing, CSS and
ARIA props while referencing the same asset as artifact cards.

The shared artwork intentionally keeps its format colors across file icon consumers,
including artifact file tabs, queued prompts and the environment panel. Lucide
stroke styling continues to apply to formats without dedicated artwork.
6 changes: 4 additions & 2 deletions docs/design/web-shell-drop-intent-choice.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ dialog closes if the composer target changes or upload becomes unavailable.

When workspace upload is unavailable, drops keep the existing attachment
behavior instead of showing an upload action that cannot succeed. Host-level
`fileUploadEnabled={false}` retains its existing contract and disables all
file drag-in. Clipboard paste and the `@` panel upload item are unchanged.
`fileUploadEnabled={false}` disables only the upload lane; drops fall back to
attachments when `attachmentsEnabled` permits them. Clipboard paste is unchanged,
and the `@` panel upload item remains hidden when uploads are disabled. See
[Default file drop action](web-shell-file-drop-action.md).

File attachment chips are interactive before and after optimistic submission.
Opening one shows the referenced file in the right-side preview panel.
Expand Down
19 changes: 19 additions & 0 deletions docs/design/web-shell-file-drop-action.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Default file drop action

Expose `fileDropAction?: 'upload' | 'attach'` on WebShell and its shared
customization context so main and split composers behave identically. Omission
preserves the choice dialog when both destinations are available. A configured
available destination runs immediately; if only one destination is available,
use it regardless of the preference. Neither available means no ingestion.

Workspace upload still requires the existing capability, selected workspace and
trust checks. Attachment availability remains governed by `attachmentsEnabled`.
`fileUploadEnabled=false` disables workspace upload without disabling attachments.
Comment thread
ytahdn marked this conversation as resolved.
Resolved
Keep the add menu's attachment availability consistent with drag-and-drop.
Cancel a pending choice if availability, default action, session or workspace
changes; do not apply an old drop to a newly selected destination.

Use existing upload and attachment ingestion callbacks. Preserve batch filtering,
drop cancellation, disabled behavior, image paste, and upload directory handling.
Validate the routing matrix with component tests and public prop/context tests;
use mocked browser drag events for direct-action and dialog flows.
2 changes: 1 addition & 1 deletion docs/design/web-shell-file-upload.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ The Web Shell is multi-workspace, so uploads must use the same target as the com
- Only the existing legacy composer path with no `atWorkspaceCwd` uses `workspace.client.uploadWorkspaceFile(...)`; a modern multi-workspace composer with a missing cwd is unsupported rather than silently targeting the primary workspace.
- Drag-and-drop and the @ panel entry share the selected client. The @ panel additionally supplies a directory within that workspace.
- A legacy target requires `workspace_file_upload`; a cwd-qualified target requires both `workspace_file_upload` and `workspace_qualified_rest_core`. The selected workspace must also be present exactly once and trusted in the capabilities snapshot. Otherwise hide both upload entry points.
- Host control: the web-shell accepts an optional `fileUploadEnabled` prop (threaded through the customization context). It is an additional gate, not a replacement for the capability: `fileUploadEnabled === false` force-hides both entry points AND disables file drag-and-drop entirely — no drag highlight, no upload, and no inline image/text ingestion from dropped files — even when the daemon advertises `workspace_file_upload`, while `true`/omitted still requires the capability (and the trust / qualified-route checks above). It never bypasses the capability. Clipboard paste of images/text is unaffected.
- Host control: the web-shell accepts an optional `fileUploadEnabled` prop (threaded through the customization context). It is an additional gate, not a replacement for the capability: `fileUploadEnabled === false` force-hides workspace-upload entry points and disables the upload lane even when the daemon advertises `workspace_file_upload`, while `true`/omitted still requires the capability (and the trust / qualified-route checks above). It never bypasses the capability. Attachment drag/add remains governed by `attachmentsEnabled`; see [Default file drop action](web-shell-file-drop-action.md). Clipboard paste of images/text is unaffected.
- Upload directory: an optional `fileUploadDirectory` prop (threaded through the customization context) sets the directory that drag-and-dropped files upload into. It is a **relative path without a leading `/`** (`'uploads'`, `'uploads/images'`); a leading-slash absolute path is rejected by the daemon as outside the workspace. Omitted (or `'.'`) uploads into the workspace root. The daemon creates the directory (including intermediate components) on upload when it does not exist, so a configured drop folder needs no manual setup.

### Upload versus `@` consumption
Expand Down
19 changes: 19 additions & 0 deletions packages/web-shell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,25 @@ const projection = projectChatRecordsToDaemonTranscript(records);
宿主应显示 `projection.diagnostics`,并在 `complete=false` 或 `truncated=true` 时提示
历史可能不完整。组件需要一个可用高度;自定义 renderer 的副作用仍由宿主负责。

## 拖入文件的默认行为

通过 `fileDropAction` 指定拖入文件时的默认去向,适用于 `WebShell` 和
`WebShellWithProviders`:

```tsx
<WebShellWithProviders fileDropAction="upload" fileUploadDirectory="uploads" />
<WebShellWithProviders fileDropAction="attach" />
```

- `upload`:直接上传到工作区,并插入 `@文件` 引用。
- `attach`:直接添加为当前消息的附件。
- 不传:仅当上传和附件都可用时显示选择弹窗。

只有一种方式可用时直接使用它,即使配置的默认去向是另一种;两种都不可用时
不接收拖入文件。`fileUploadEnabled={false}` 只关闭工作区上传,不再关闭附件
拖入或添加附件入口。上传仍受 daemon 能力、工作区信任及目标路径检查约束。
修改默认去向或可用方式时,会关闭已经打开的选择弹窗;需要重新拖入文件。

## 消息操作

- 已完成的 assistant 消息支持复制;具备持久化 checkpoint 时还支持分支。
Expand Down
13 changes: 13 additions & 0 deletions packages/web-shell/client/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,7 @@ vi.mock('./components/ChatEditor', async () => {
'data-artifact-image-renderer': String(
Boolean(customization.artifact?.renderImage),
),
'data-file-drop-action': customization.fileDropAction,
},
React.createElement(
'button',
Expand Down Expand Up @@ -33830,6 +33831,18 @@ describe('App manual-run orchestration (scheduled tasks)', () => {
});

describe('fileUploadEnabled customization plumbing', () => {
it.each(['upload', 'attach'] as const)(
'passes the %s drop preference to composers',
(fileDropAction) => {
const { container } = renderApp({ fileDropAction });
expect(
container
.querySelector('[data-web-shell-composer]')
?.getAttribute('data-file-drop-action'),
).toBe(fileDropAction);
},
);

it('reaches the composer customization when the host disables upload', () => {
const { container } = renderApp({ fileUploadEnabled: false });
const composer = container.querySelector('[data-web-shell-composer]');
Expand Down
10 changes: 9 additions & 1 deletion packages/web-shell/client/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1184,12 +1184,17 @@ export interface WebShellProps {
builtinAtProviders?: WebShellBuiltinAtProvidersConfig;
/**
* Controls whether the composer's file-upload entry points (drag-and-drop
* and the @ panel upload item) are enabled. Works alongside the daemon's
* and the @ panel upload item) are enabled. Does not disable attachments.
* Works alongside the daemon's
* `workspace_file_upload` capability, not instead of it: `false` force-
* disables upload even when the daemon advertises the capability, while
* `true`/omitted still requires the capability 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.
Expand Down Expand Up @@ -2882,6 +2887,7 @@ export function App({
fileUploadEnabled,
fileUploadDirectory,
artifact,
fileDropAction,
renderToolHeaderExtra,
renderWelcomeHeader,
renderWelcomeFooter,
Expand Down Expand Up @@ -3162,6 +3168,7 @@ export function App({
loadingPhrases,
fileUploadEnabled,
fileUploadDirectory,
fileDropAction,
}),
[
artifact,
Expand Down Expand Up @@ -3191,6 +3198,7 @@ export function App({
loadingPhrases,
fileUploadEnabled,
fileUploadDirectory,
fileDropAction,
],
);
const mainChatCustomization = useMemo(
Expand Down
122 changes: 54 additions & 68 deletions packages/web-shell/client/components/ChatEditor.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2154,13 +2154,12 @@
position: relative;
display: inline-flex;
align-items: center;
gap: 6px;
max-width: 240px;
padding: 4px 8px;
box-sizing: border-box;
width: 272px;
max-width: 100%;
padding: 8px 32px 8px 8px;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping the existing scrollable attachment height budget in this PR. More card content requires scrolling, and the parent composer height still constrains short viewports even with a larger budget. The proposed combined-occupancy resize is deferred; no height change is claimed here.

border: 1px solid var(--chat-editor-border-color);
border-radius: 6px;
font-size: 12px;
line-height: 16px;
border-radius: 16px;
color: var(--chat-editor-text-primary);
}

Expand All @@ -2176,8 +2175,9 @@
.fileChipPreview {
display: inline-flex;
min-width: 0;
width: 100%;
align-items: center;
gap: 6px;
gap: 12px;
padding: 0;
border: 0;
background: transparent;
Expand All @@ -2190,39 +2190,17 @@
cursor: default;
}

.fileChipIcon {
flex: 0 0 auto;
color: var(--chat-editor-text-secondary, currentColor);
}

.fileChipName {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.fileChipRemove {
position: absolute;
top: 50%;
right: 4px;
top: 8px;
right: 8px;
z-index: 1;
transform: translateY(-50%);
pointer-events: none;
}

.fileChip:hover .fileChipRemove:not(:disabled),
.fileChipRemove:focus-visible {
width: 20px;
height: 20px;
opacity: 1;
pointer-events: auto;
}

@media (hover: none) {
.fileChipRemove:not(:disabled) {
opacity: 1;
pointer-events: auto;
}
}

.searchPanel {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -2341,24 +2319,18 @@
align-items: center;
gap: 10px;
min-width: 0;
padding: 7px 10px;
border: 1px solid color-mix(in srgb, var(--agent-blue-500) 24%, transparent);
box-sizing: border-box;
width: 100%;
max-width: 100%;
padding: 8px;
border: 1px solid var(--chat-editor-border-color);
border-radius: 10px;
background: color-mix(
in srgb,
var(--agent-blue-500) 8%,
var(--chat-editor-bg-primary)
);
background: var(--chat-editor-bg-primary);
color: var(--chat-editor-text-primary);
font-size: 12px;
line-height: 1.4;
}

.uploadRow[data-status='done'] {
border-color: color-mix(in srgb, var(--success-color) 24%, transparent);
background: var(--success-bg);
}

.uploadRow[data-status='error'] {
border-color: var(--error-border);
background: var(--error-bg);
Expand Down Expand Up @@ -2387,11 +2359,25 @@
font: inherit;
}

.uploadRowIcon {
flex: 0 0 auto;
}

.uploadRowName {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}

.uploadRowPreview:disabled {
cursor: default;
}

.uploadRow svg {
.uploadRowAction svg,
.uploadRowSpinner {
flex: 0 0 auto;
width: 14px;
height: 14px;
Expand All @@ -2416,41 +2402,28 @@
}
}

.uploadRowName {
.uploadRowStatus {
Comment thread
ytahdn marked this conversation as resolved.
Resolved
display: inline-flex;
align-items: center;
gap: 4px;
min-width: 0;
flex: 0 1 auto;
max-width: 40%;
flex: 0 0 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--font-mono);
color: var(--chat-editor-text-dimmed);
}

.uploadRowStatus {
.uploadRowStatusText {
min-width: 0;
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--chat-editor-text-dimmed);
text-align: right;
}

.uploadRow[data-status='error'] .uploadRowStatus {
color: var(--error-color);
}

.uploadRow[data-status='done'] .uploadRowStatus {
color: var(--success-color);
}

.uploadRow[data-status='done'] .uploadRowPreview > svg {
color: var(--success-color);
}

.uploadRow[data-status='error'] .uploadRowPreview > svg {
color: var(--error-color);
}

.uploadRowAction {
display: inline-flex;
align-items: center;
Expand Down Expand Up @@ -2490,3 +2463,16 @@
font-size: 13px;
pointer-events: none;
}

.fileTag {
background: var(--chat-editor-bg-primary);
min-height: 28px;
border-radius: 8px;
font-family: var(--font-sans, system-ui, sans-serif);
}

.fileTagIcon {
flex: 0 0 auto;
margin-left: 8px;
color: var(--muted-foreground);
}
Loading
Loading