qwen-code/packages/web-shell/client/components/dialogs/McpDialog.module.css
Shaojin Wen ef0c39cf96
feat(web-shell): browse MCP server resources in the /mcp dialog (#5879)
* feat(web-shell): browse MCP server resources in the /mcp dialog

Port the TUI's MCP resource browser (#5544/#5635) to the Web Shell. The
/mcp dialog now shows per-server resource and prompt counts plus an
expandable resource browser (URI, MIME type, size, description, and the
@server:uri chat reference), reaching parity with the terminal UI.

Resources and prompts were never serialized past the in-process core
registries, so this wires the data end to end: per-server resourceCount
and promptCount ride the existing /workspace/mcp status, and a new
qwen/status/workspace/mcp/resources ext-method (mirroring the tools
drill-down) carries the resource list through the daemon, SDK, and webui
hook into the Web Shell McpDialog. All additions are protocol-additive;
older daemons 404 the new route and the client degrades gracefully.

* fix(web-shell): make resourcesByServer optional and pluralize 1-byte size

Address review on #5879:
- SerializedMcpStatusMessage.resourcesByServer is now optional, matching
  its JSDoc ("older clients omit it") so TypeScript enforces the `?? {}`
  defensive read at every consumer.
- mcp.resource.bytes now pluralizes ("1 byte" vs "2 bytes"), consistent
  with the mcp.resourceCount / mcp.promptCount strings in this PR.

* fix(web-shell): address MCP resources review — remove non-functional @ref, harden fallbacks, add tests

Address review on #5879:
- [Critical] Remove the "@server:uri" chat-reference UI from the resource
  browser: the Web Shell submits prompts as a plain text block and the
  daemon forwards it verbatim (the TUI's atCommandProcessor resolution is
  TUI-client-side only), so the reference never injected resource content.
  The browser stays as read-only metadata; @-reference injection is a
  follow-up that needs the resolver wired into the Web Shell prompt path.
- [Critical] reloadServer now isolates the resource refetch in its own
  try/catch so a failed resource load can't report a successful
  reconnect/enable as failed.
- [Critical] resolveServerMcpResources/Prompts skip a throwing session in
  the pool-mode fallback so one degraded session can't blank the base
  /workspace/mcp status (which now carries the counts).
- [Suggestion] Resource section renders an "unavailable" state when a
  server advertises a count but the drill-down list is empty.
- [Suggestion] Add SDK client URL-encoding test (workspaceMcpResources),
  route-table matching test, and an unconfigured-server error-branch test.

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-06-26 17:12:01 +00:00

368 lines
5.3 KiB
CSS

.layout {
display: flex;
flex-direction: column;
min-height: 0;
gap: 10px;
}
.muted {
color: var(--muted-foreground);
font-size: 12px;
}
.notice {
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--warning-color);
}
.list {
display: flex;
flex-direction: column;
min-height: 0;
}
.group {
display: flex;
flex-direction: column;
gap: 3px;
padding: 2px 0 8px;
}
.groupTitle {
color: var(--muted-foreground);
font-size: 12px;
padding: 0 8px 2px;
}
.server {
display: flex;
flex-direction: column;
}
.serverCard {
display: flex;
flex-direction: column;
border-radius: 6px;
}
.serverCardExpanded {
background: var(--secondary);
padding-bottom: 8px;
}
.row {
display: flex;
align-items: center;
gap: 8px;
min-height: 32px;
width: 100%;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--foreground);
cursor: pointer;
font: inherit;
padding: 5px 8px;
text-align: left;
}
.row:hover,
.expandedRow {
background: var(--secondary);
}
.serverRow {
font-size: 13px;
cursor: pointer;
}
.rowMain {
display: flex;
min-width: 0;
flex: 1 1 auto;
align-items: center;
gap: 8px;
border: 0;
background: transparent;
color: inherit;
cursor: pointer;
font: inherit;
padding: 0;
text-align: left;
}
.rowIcon {
position: relative;
flex: 0 0 auto;
width: 14px;
height: 14px;
border: 1.5px solid currentColor;
border-radius: 4px;
color: var(--muted-foreground);
}
.rowIcon::before,
.rowIcon::after {
position: absolute;
left: 3px;
right: 3px;
height: 1.5px;
border-radius: 999px;
background: currentColor;
content: '';
}
.rowIcon::before {
top: 4px;
}
.rowIcon::after {
top: 8px;
}
.chevronCell {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
color: var(--muted-foreground);
}
.toolRow {
min-height: 28px;
color: var(--muted-foreground);
font-size: 12px;
}
.chevron {
flex: 0 0 auto;
width: 14px;
height: 14px;
color: var(--muted-foreground);
stroke-width: 1.8;
transition: transform 120ms ease;
}
.chevronExpanded {
transform: rotate(90deg);
}
.disabled {
opacity: 0.58;
}
.name {
min-width: 0;
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.serverName {
flex: 0 1 auto;
}
.status,
.badge {
flex: 0 0 auto;
color: var(--muted-foreground);
font-size: 12px;
white-space: nowrap;
}
.status {
border-radius: 4px;
background: var(--muted);
line-height: 18px;
padding: 0 6px;
}
.toolCount {
margin-left: 2px;
text-align: right;
}
.resourceCount,
.promptCount {
margin-left: 6px;
text-align: right;
}
.success {
color: var(--success-color);
}
.warning {
color: var(--warning-color);
}
.error {
color: var(--error-color);
}
.serverDetail {
display: flex;
flex-direction: column;
gap: 8px;
margin: 8px;
padding: 8px;
border-radius: 6px;
background: var(--background);
}
.serverDetailHeader {
display: flex;
justify-content: flex-end;
}
.serverActions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
}
.field {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
padding: 4px 6px;
}
.label {
color: var(--foreground);
font-size: 12px;
font-weight: 500;
}
.value {
min-width: 0;
overflow-wrap: anywhere;
}
.button {
border: 1px solid var(--border);
border-radius: 5px;
background: var(--muted);
color: var(--foreground);
cursor: pointer;
font: inherit;
font-size: 12px;
padding: 4px 9px;
}
.button:hover:not(:disabled) {
border-color: var(--agent-blue-500);
}
.button:disabled {
cursor: default;
opacity: 0.55;
}
.message,
.schema {
margin: 0;
overflow: auto;
border-radius: 6px;
background: var(--background);
color: var(--muted-foreground);
font-size: 12px;
padding: 8px 10px;
white-space: pre-wrap;
}
.tools {
display: flex;
flex-direction: column;
gap: 2px;
}
.tool {
display: flex;
flex-direction: column;
}
.resources {
display: flex;
flex-direction: column;
gap: 2px;
margin-top: 4px;
padding-top: 4px;
border-top: 1px solid var(--border-color, var(--bg-secondary));
}
.resources > .sectionTitle {
padding: 2px 8px;
color: var(--text-secondary);
}
.resourceUri {
font-family:
ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.resourceLabel {
flex: 0 1 auto;
margin-left: 8px;
color: var(--text-secondary);
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.toolDetail {
display: flex;
flex-direction: column;
gap: 7px;
margin: 8px;
padding: 8px;
border-radius: 6px;
background: var(--background);
}
.section {
display: flex;
flex-direction: column;
gap: 5px;
padding: 4px 6px;
}
.sectionTitle {
color: var(--foreground);
font-size: 12px;
font-weight: 500;
}
.description {
color: var(--foreground);
font-size: 12px;
white-space: pre-wrap;
}
.parameter {
color: var(--muted-foreground);
font-size: 12px;
white-space: pre-wrap;
}
.empty,
.emptyTools {
color: var(--muted-foreground);
padding: 12px 8px;
}
.emptyTools {
padding: 2px 0;
font-size: 12px;
}