mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 11:41:04 +00:00
feat(webui): Add UMD build format and CDN usage support
This commit is contained in:
parent
1f9311d3ca
commit
8d2d29a422
13 changed files with 796 additions and 189 deletions
|
|
@ -192,6 +192,20 @@ export default tseslint.config(
|
|||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.cjs'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
module: 'readonly',
|
||||
require: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
'no-undef': 'off',
|
||||
},
|
||||
}, {
|
||||
files: ['packages/vscode-ide-companion/esbuild.js'],
|
||||
languageOptions: {
|
||||
|
|
|
|||
55
packages/webui/.npmignore
Normal file
55
packages/webui/.npmignore
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build outputs (already included in files array)
|
||||
# dist is included via package.json files array
|
||||
|
||||
# Development files
|
||||
*.test.*
|
||||
*.spec.*
|
||||
.storybook/
|
||||
.storybook-static/
|
||||
.storybook-build/
|
||||
storybook-static/
|
||||
docs/
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Editor files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Git files
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
.nyc_output/
|
||||
coverage/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Configs
|
||||
.vite/
|
||||
.eslintrc*
|
||||
.prettierrc*
|
||||
.editorconfig
|
||||
|
||||
# Examples (not needed in npm package)
|
||||
examples/
|
||||
|
||||
# Scripts
|
||||
scripts/
|
||||
|
||||
# Local development
|
||||
demo/
|
||||
dist-dev/
|
||||
dist-dev/**
|
||||
!dist/**
|
||||
|
|
@ -9,6 +9,8 @@ A shared React component library for Qwen Code applications, providing cross-pla
|
|||
- **Tailwind CSS**: Shared styling preset for consistent design
|
||||
- **TypeScript**: Full type definitions for all components
|
||||
- **Storybook**: Interactive component documentation and development
|
||||
- **Multiple Build Formats**: Supports ESM, CJS, and UMD formats for different environments
|
||||
- **CDN Usage**: Can be loaded directly in browsers via CDN
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -16,6 +18,141 @@ A shared React component library for Qwen Code applications, providing cross-pla
|
|||
npm install @qwen-code/webui
|
||||
```
|
||||
|
||||
## CDN Usage
|
||||
|
||||
You can also use this library directly in the browser via CDN:
|
||||
|
||||
### Option 1: With JSX Support (using Babel)
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- Load React -->
|
||||
<script
|
||||
crossorigin
|
||||
src="https://unpkg.com/react@18/umd/react.production.min.js"
|
||||
></script>
|
||||
<script
|
||||
crossorigin
|
||||
src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"
|
||||
></script>
|
||||
|
||||
<!-- Load Babel Standalone for JSX processing -->
|
||||
<script src="https://unpkg.com/@babel/standalone@7.23.6/babel.min.js"></script>
|
||||
|
||||
<!-- Manually create the jsxRuntime object to satisfy the dependency -->
|
||||
<script>
|
||||
// Provide a minimal JSX runtime for builds that expect react/jsx-runtime globals.
|
||||
const withKey = (props, key) =>
|
||||
key == null ? props : Object.assign({}, props, { key });
|
||||
const jsx = (type, props, key) =>
|
||||
React.createElement(type, withKey(props, key));
|
||||
const jsxRuntime = {
|
||||
Fragment: React.Fragment,
|
||||
jsx,
|
||||
jsxs: jsx,
|
||||
jsxDEV: jsx,
|
||||
};
|
||||
|
||||
window.ReactJSXRuntime = jsxRuntime;
|
||||
window['react/jsx-runtime'] = jsxRuntime;
|
||||
window['react/jsx-dev-runtime'] = jsxRuntime;
|
||||
</script>
|
||||
|
||||
<!-- Load the webui library -->
|
||||
<script src="https://unpkg.com/@qwen-code/webui@0.1.0-beta.2/dist/index.umd.js"></script>
|
||||
|
||||
<!-- Load the CSS -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/@qwen-code/webui@0.1.0-beta.2/dist/styles.css"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<script type="text/babel">
|
||||
// Access components from the global QwenCodeWebUI object
|
||||
const { ChatViewer } = QwenCodeWebUI;
|
||||
|
||||
// Use the components with JSX support
|
||||
const App = () => (
|
||||
<ChatViewer messages={/* your messages */} />
|
||||
);
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Option 2: Without JSX (using React.createElement directly)
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- Load React -->
|
||||
<script
|
||||
crossorigin
|
||||
src="https://unpkg.com/react@18/umd/react.production.min.js"
|
||||
></script>
|
||||
<script
|
||||
crossorigin
|
||||
src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"
|
||||
></script>
|
||||
|
||||
<!-- Manually create the jsxRuntime object to satisfy the dependency -->
|
||||
<script>
|
||||
// Provide a minimal JSX runtime for builds that expect react/jsx-runtime globals.
|
||||
const withKey = (props, key) =>
|
||||
key == null ? props : Object.assign({}, props, { key });
|
||||
const jsx = (type, props, key) =>
|
||||
React.createElement(type, withKey(props, key));
|
||||
const jsxRuntime = {
|
||||
Fragment: React.Fragment,
|
||||
jsx,
|
||||
jsxs: jsx,
|
||||
jsxDEV: jsx,
|
||||
};
|
||||
|
||||
window.ReactJSXRuntime = jsxRuntime;
|
||||
window['react/jsx-runtime'] = jsxRuntime;
|
||||
window['react/jsx-dev-runtime'] = jsxRuntime;
|
||||
</script>
|
||||
|
||||
<!-- Load the webui library -->
|
||||
<script src="https://unpkg.com/@qwen-code/webui@0.1.0-beta.2/dist/index.umd.js"></script>
|
||||
|
||||
<!-- Load the CSS -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/@qwen-code/webui@0.1.0-beta.2/dist/styles.css"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<script>
|
||||
// Access components from the global QwenCodeWebUI object
|
||||
const { ChatViewer } = QwenCodeWebUI;
|
||||
|
||||
// Use the components with React.createElement (no JSX)
|
||||
const App = React.createElement(ChatViewer, {
|
||||
messages: [
|
||||
/* your messages */
|
||||
],
|
||||
});
|
||||
|
||||
ReactDOM.render(App, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
For a complete working example, see [examples/cdn-usage-demo.html](./examples/cdn-usage-demo.html).
|
||||
|
||||
## Quick Start
|
||||
|
||||
```tsx
|
||||
|
|
|
|||
8
packages/webui/examples/README.md
Normal file
8
packages/webui/examples/README.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Examples
|
||||
|
||||
This directory contains example implementations demonstrating various ways to use the `@qwen-code/webui` library.
|
||||
|
||||
## Available Examples
|
||||
|
||||
- [`cdn-usage-demo.html`](./cdn-usage-demo.html) - Shows how to use the library directly via CDN in a browser environment, featuring the ChatViewer component
|
||||
- [`complex-chat-demo.html`](./complex-chat-demo.html) - Demonstrates a complex chat conversation with tool calls, code blocks, and mixed-language interactions
|
||||
142
packages/webui/examples/cdn-usage-demo.html
Normal file
142
packages/webui/examples/cdn-usage-demo.html
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@qwen-code/webui CDN Usage Example</title>
|
||||
<!-- Load React and ReactDOM from CDN -->
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
|
||||
<!-- Manually create the jsxRuntime object to satisfy the dependency -->
|
||||
<script>
|
||||
// Provide a minimal JSX runtime for builds that expect react/jsx-runtime globals.
|
||||
const withKey = (props, key) =>
|
||||
key == null ? props : Object.assign({}, props, { key });
|
||||
const jsx = (type, props, key) => React.createElement(type, withKey(props, key));
|
||||
const jsxRuntime = {
|
||||
Fragment: React.Fragment,
|
||||
jsx,
|
||||
jsxs: jsx,
|
||||
jsxDEV: jsx
|
||||
};
|
||||
|
||||
window.ReactJSXRuntime = jsxRuntime;
|
||||
window['react/jsx-runtime'] = jsxRuntime;
|
||||
window['react/jsx-dev-runtime'] = jsxRuntime;
|
||||
</script>
|
||||
|
||||
<!-- Load the webui library from CDN -->
|
||||
<script src="https://unpkg.com/@qwen-code/webui@0.1.0-beta.2/dist/index.umd.js"></script>
|
||||
|
||||
<!-- Load the CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/@qwen-code/webui@0.1.0-beta.2/dist/styles.css">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
height: 500px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>@qwen-code/webui CDN Usage Example</h1>
|
||||
<h2>ChatViewer Component Demo</h2>
|
||||
<div id="chat-root-no-babel" class="chat-container"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Same sample messages
|
||||
const sampleMessages = [
|
||||
{
|
||||
uuid: '1',
|
||||
type: 'user',
|
||||
timestamp: new Date().toISOString(),
|
||||
message: {
|
||||
role: 'user',
|
||||
parts: [{ text: 'Hello, how can I use the ChatViewer component without Babel?' }]
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: '2',
|
||||
type: 'assistant',
|
||||
timestamp: new Date(Date.now() + 1000).toISOString(),
|
||||
message: {
|
||||
role: 'assistant',
|
||||
parts: [{ text: 'You can use React.createElement directly, without needing Babel.' }]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Get the ChatViewer and Platform components from the global object
|
||||
const { ChatViewer, PlatformProvider } = QwenCodeWebUI;
|
||||
|
||||
// Define a minimal platform context for web usage
|
||||
const platformContext = {
|
||||
platform: 'web',
|
||||
postMessage: (message) => {
|
||||
// In a web context, you might want to handle messages differently
|
||||
console.log('Posted message:', message);
|
||||
},
|
||||
onMessage: (handler) => {
|
||||
// In a web context, you might listen for custom events
|
||||
window.addEventListener('message', handler);
|
||||
return () => window.removeEventListener('message', handler);
|
||||
},
|
||||
openFile: (path) => {
|
||||
console.log('Opening file:', path);
|
||||
},
|
||||
getResourceUrl: (resource) => {
|
||||
// Return URLs for platform-specific resources
|
||||
return null; // Use default resources
|
||||
},
|
||||
features: {
|
||||
canOpenFile: false,
|
||||
canCopy: true
|
||||
}
|
||||
};
|
||||
|
||||
// Render the ChatViewer component without Babel
|
||||
const rootElementNoBabel = document.getElementById('chat-root-no-babel');
|
||||
|
||||
// Create the ChatViewer element wrapped with PlatformProvider using React.createElement (no JSX)
|
||||
const ChatAppNoBabel = React.createElement(PlatformProvider, { value: platformContext },
|
||||
React.createElement(ChatViewer, {
|
||||
messages: sampleMessages,
|
||||
autoScroll: true,
|
||||
theme: 'light'
|
||||
})
|
||||
);
|
||||
|
||||
ReactDOM.render(ChatAppNoBabel, rootElementNoBabel);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
413
packages/webui/examples/complex-chat-demo.html
Normal file
413
packages/webui/examples/complex-chat-demo.html
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@qwen-code/webui Complex Chat Demo</title>
|
||||
<!-- Load React and ReactDOM from CDN -->
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<!-- Load the webui library from CDN -->
|
||||
<script src="https://unpkg.com/@qwen-code/webui@0.1.0-beta.2/dist/index.umd.js"></script>
|
||||
|
||||
<!-- Load the CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/@qwen-code/webui@0.1.0-beta.2/dist/styles.css">
|
||||
|
||||
<!-- Load Babel Standalone for JSX processing -->
|
||||
<script src="https://unpkg.com/@babel/standalone@7.23.6/babel.min.js"></script>
|
||||
|
||||
<!-- Manually create the jsxRuntime object to satisfy the dependency -->
|
||||
<script>
|
||||
// Provide a minimal JSX runtime for builds that expect react/jsx-runtime globals.
|
||||
const withKey = (props, key) =>
|
||||
key == null ? props : Object.assign({}, props, { key });
|
||||
const jsx = (type, props, key) => React.createElement(type, withKey(props, key));
|
||||
const jsxRuntime = {
|
||||
Fragment: React.Fragment,
|
||||
jsx,
|
||||
jsxs: jsx,
|
||||
jsxDEV: jsx
|
||||
};
|
||||
|
||||
window.ReactJSXRuntime = jsxRuntime;
|
||||
window['react/jsx-runtime'] = jsxRuntime;
|
||||
window['react/jsx-dev-runtime'] = jsxRuntime;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
height: 700px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>@qwen-code/webui Complex Chat Demo</h1>
|
||||
<h2>Real conversation example with tool calls</h2>
|
||||
<div id="complex-chat-root" class="chat-container"></div>
|
||||
|
||||
<h2>Alternative: With Full Tailwind Support</h2>
|
||||
<p>For full Tailwind utility class support (like gap-1.5, button classes, etc.), also include:</p>
|
||||
<pre><script src="https://cdn.tailwindcss.com"></script></pre>
|
||||
</div>
|
||||
|
||||
<script type="text/babel">
|
||||
// Complex real-world conversation example from Storybook
|
||||
const complexChatMessages = [
|
||||
{
|
||||
uuid: '20fb9426-40ca-4e85-92a1-7add59082167',
|
||||
parentUuid: null,
|
||||
sessionId: 'f05dd1fc-2947-44e0-9f31-cbddce461844',
|
||||
timestamp: '2026-01-09T02:58:32.415Z',
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: `Message received: Object from: Object
|
||||
service-worker.js:137 Attempting to connect to Native Host: com.qwen.cli.bridge
|
||||
service-worker.js:202 Sending handshake...
|
||||
service-worker.js:175 Native host disconnected
|
||||
service-worker.js:177 Disconnect error: Object
|
||||
service-worker.js:178 Disconnect error message: Native host has exited.
|
||||
service-worker.js:207 Handshake timeout - no response from Native Host
|
||||
|
||||
接收不到 host 的信息。正常应该是:
|
||||
service-worker.js:150 [Native Event] host_info Object
|
||||
service-worker.js:552 [Qwen] host_info Object
|
||||
service-worker.js:617 [Host] Info Object
|
||||
service-worker.js:164 [Native Message] handshake_response Object
|
||||
service-worker.js:231 Handshake successful: Object`,
|
||||
},
|
||||
},
|
||||
{
|
||||
uuid: '86f72c75-4626-4292-a26f-13db6f80d4be',
|
||||
parentUuid: '20fb9426-40ca-4e85-92a1-7add59082167',
|
||||
sessionId: 'f05dd1fc-2947-44e0-9f31-cbddce461844',
|
||||
timestamp: '2026-01-09T02:58:45.239Z',
|
||||
type: 'assistant',
|
||||
model: 'claude-opus-4-5-20251101',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: `I can see you're experiencing an issue with the Chrome extension's native host connection. The error shows that the native host is disconnecting with "Native host has exited" and there's a handshake timeout. However, the logs also show that sometimes the handshake is successful when the host_info is received properly.
|
||||
|
||||
Let me help you troubleshoot this issue. First, I'll explore the codebase to understand how the native host communication works.`,
|
||||
},
|
||||
},
|
||||
{
|
||||
uuid: '6ffff579-8020-4ccb-9b79-7c1da76bbf82',
|
||||
parentUuid: '86f72c75-4626-4292-a26f-13db6f80d4be',
|
||||
sessionId: 'f05dd1fc-2947-44e0-9f31-cbddce461844',
|
||||
timestamp: '2026-01-09T02:58:52.409Z',
|
||||
type: 'assistant',
|
||||
model: 'claude-opus-4-5-20251101',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: `Let me create a todo list to track the troubleshooting steps:
|
||||
|
||||
1. **Explore the codebase** to understand the native host implementation
|
||||
2. **Check the native host configuration** and setup
|
||||
3. **Analyze the service worker code** that handles native messaging
|
||||
4. **Identify why the native host exits** unexpectedly
|
||||
5. **Propose fixes** for the native host connection issue`,
|
||||
},
|
||||
},
|
||||
{
|
||||
uuid: '8fd3dc96-1d34-4dae-845a-4827f435b890',
|
||||
parentUuid: '6ffff579-8020-4ccb-9b79-7c1da76bbf82',
|
||||
sessionId: 'f05dd1fc-2947-44e0-9f31-cbddce461844',
|
||||
timestamp: '2026-01-09T02:58:57.366Z',
|
||||
type: 'assistant',
|
||||
model: 'claude-opus-4-5-20251101',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: `Let me start by exploring the codebase to understand the native host implementation.
|
||||
|
||||
Looking at the project structure, I found the following relevant files:
|
||||
|
||||
\`\`\`
|
||||
packages/chrome-extension/
|
||||
├── src/
|
||||
│ ├── service-worker.ts # Main service worker
|
||||
│ ├── native-messaging/
|
||||
│ │ ├── host.ts # Native host communication
|
||||
│ │ └── protocol.ts # Message protocol
|
||||
│ └── manifest.json # Extension manifest
|
||||
└── native-host/
|
||||
├── index.js # Native host entry
|
||||
└── com.qwen.cli.bridge.json # Host manifest
|
||||
\`\`\`
|
||||
|
||||
The issue appears to be in the handshake timing. The native host is exiting before the handshake completes.`,
|
||||
},
|
||||
},
|
||||
{
|
||||
uuid: 'user-follow-up-1',
|
||||
parentUuid: '8fd3dc96-1d34-4dae-845a-4827f435b890',
|
||||
sessionId: 'f05dd1fc-2947-44e0-9f31-cbddce461844',
|
||||
timestamp: '2026-01-09T03:00:15.000Z',
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: '那应该怎么修复这个问题?',
|
||||
},
|
||||
},
|
||||
{
|
||||
uuid: 'assistant-fix-1',
|
||||
parentUuid: 'user-follow-up-1',
|
||||
sessionId: 'f05dd1fc-2947-44e0-9f31-cbddce461844',
|
||||
timestamp: '2026-01-09T03:00:45.000Z',
|
||||
type: 'assistant',
|
||||
model: 'claude-opus-4-5-20251101',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: `Based on my analysis, here's how to fix the native host connection issue:
|
||||
|
||||
## Root Cause
|
||||
The native host process is exiting prematurely because it's not keeping the stdin stream open for communication.
|
||||
|
||||
## Solution
|
||||
|
||||
Update the native host's \`index.js\`:
|
||||
|
||||
\`\`\`javascript
|
||||
// native-host/index.js
|
||||
const { stdin, stdout } = process;
|
||||
|
||||
// Keep the process alive by not closing stdin
|
||||
stdin.resume();
|
||||
|
||||
// Handle incoming messages
|
||||
stdin.on('data', (data) => {
|
||||
try {
|
||||
const length = data.readUInt32LE(0);
|
||||
const message = JSON.parse(data.slice(4, 4 + length).toString());
|
||||
|
||||
if (message.type === 'handshake') {
|
||||
sendMessage({ type: 'handshake_response', status: 'ok' });
|
||||
}
|
||||
// Handle other messages...
|
||||
} catch (err) {
|
||||
console.error('Error parsing message:', err);
|
||||
}
|
||||
});
|
||||
|
||||
function sendMessage(msg) {
|
||||
const json = JSON.stringify(msg);
|
||||
const length = Buffer.alloc(4);
|
||||
length.writeUInt32LE(json.length, 0);
|
||||
stdout.write(Buffer.concat([length, Buffer.from(json)]));
|
||||
}
|
||||
|
||||
// Send host info on startup
|
||||
sendMessage({
|
||||
type: 'host_info',
|
||||
pid: process.pid,
|
||||
node: process.execPath
|
||||
});
|
||||
\`\`\`
|
||||
|
||||
## Key Changes
|
||||
1. Added \`stdin.resume()\` to prevent the process from exiting
|
||||
2. Properly handle the native messaging protocol (4-byte length prefix)
|
||||
3. Send \`host_info\` immediately on startup
|
||||
|
||||
Would you like me to apply these changes to your codebase?`,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Tool call conversation example
|
||||
const toolCallMessages = [
|
||||
{
|
||||
uuid: 'tc-1',
|
||||
timestamp: '2026-01-15T14:00:00.000Z',
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
parts: [
|
||||
{ text: '帮我看看 package.json 文件,然后添加一个 lodash 依赖' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
uuid: 'tc-2',
|
||||
timestamp: '2026-01-15T14:00:05.000Z',
|
||||
type: 'assistant',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: '好的,我先读取 package.json 文件来查看当前的依赖配置。',
|
||||
},
|
||||
},
|
||||
{
|
||||
uuid: 'tc-3',
|
||||
timestamp: '2026-01-15T14:00:06.000Z',
|
||||
type: 'tool_call',
|
||||
toolCall: {
|
||||
toolCallId: 'read-1',
|
||||
kind: 'read',
|
||||
title: 'package.json',
|
||||
status: 'completed',
|
||||
content: [
|
||||
{
|
||||
type: 'content',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `{
|
||||
"name": "my-project",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
locations: [{ path: 'package.json', line: 1 }],
|
||||
},
|
||||
},
|
||||
{
|
||||
uuid: 'tc-4',
|
||||
timestamp: '2026-01-15T14:00:10.000Z',
|
||||
type: 'assistant',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: '我看到当前项目使用 React 18 和 TypeScript。现在我来添加 lodash 依赖。',
|
||||
},
|
||||
},
|
||||
{
|
||||
uuid: 'tc-5',
|
||||
timestamp: '2026-01-15T14:00:11.000Z',
|
||||
type: 'tool_call',
|
||||
toolCall: {
|
||||
toolCallId: 'edit-1',
|
||||
kind: 'edit',
|
||||
title: 'package.json',
|
||||
status: 'completed',
|
||||
content: [
|
||||
{
|
||||
type: 'diff',
|
||||
path: 'package.json',
|
||||
oldText: ` "dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},`,
|
||||
newText: ` "dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},`,
|
||||
},
|
||||
],
|
||||
locations: [{ path: 'package.json', line: 4 }],
|
||||
},
|
||||
},
|
||||
{
|
||||
uuid: 'tc-6',
|
||||
timestamp: '2026-01-15T14:00:15.000Z',
|
||||
type: 'assistant',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: `已经成功添加 lodash 依赖到 package.json。
|
||||
|
||||
你需要运行以下命令来安装新的依赖:
|
||||
|
||||
\`\`\`bash
|
||||
npm install
|
||||
\`\`\`
|
||||
|
||||
或者如果你使用 pnpm:
|
||||
|
||||
\`\`\`bash
|
||||
pnpm install
|
||||
\`\`\``,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Combine all messages for a comprehensive example
|
||||
const combinedMessages = [
|
||||
...complexChatMessages,
|
||||
...toolCallMessages
|
||||
];
|
||||
|
||||
// Get the ChatViewer and Platform components from the global object
|
||||
const { ChatViewer, PlatformProvider } = QwenCodeWebUI;
|
||||
|
||||
// Define a minimal platform context for web usage
|
||||
const platformContext = {
|
||||
platform: 'web',
|
||||
postMessage: (message) => {
|
||||
// In a web context, you might want to handle messages differently
|
||||
console.log('Posted message:', message);
|
||||
},
|
||||
onMessage: (handler) => {
|
||||
// In a web context, you might listen for custom events
|
||||
window.addEventListener('message', handler);
|
||||
return () => window.removeEventListener('message', handler);
|
||||
},
|
||||
openFile: (path) => {
|
||||
console.log('Opening file:', path);
|
||||
},
|
||||
getResourceUrl: (resource) => {
|
||||
// Return URLs for platform-specific resources
|
||||
return null; // Use default resources
|
||||
},
|
||||
features: {
|
||||
canOpenFile: false,
|
||||
canCopy: true
|
||||
}
|
||||
};
|
||||
|
||||
// Render the ChatViewer component
|
||||
const rootElement = document.getElementById('complex-chat-root');
|
||||
|
||||
// Create the ChatViewer element wrapped with PlatformProvider with complex data
|
||||
const ChatApp = React.createElement(PlatformProvider, { value: platformContext },
|
||||
React.createElement(ChatViewer, {
|
||||
messages: combinedMessages,
|
||||
autoScroll: true,
|
||||
theme: 'light',
|
||||
emptyMessage: 'Loading conversation...'
|
||||
})
|
||||
);
|
||||
|
||||
ReactDOM.render(ChatApp, rootElement);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@qwen-code/webui",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.0-beta.2",
|
||||
"description": "Shared UI components for Qwen Code packages",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
|
|
@ -18,8 +18,7 @@
|
|||
"require": "./dist/components/icons/index.cjs"
|
||||
},
|
||||
"./tailwind.preset": "./tailwind.preset.cjs",
|
||||
"./styles.css": "./dist/styles.css",
|
||||
"./webview.css": "./dist/webview.css"
|
||||
"./styles.css": "./dist/styles.css"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
|
@ -28,6 +27,9 @@
|
|||
"sideEffects": [
|
||||
"**/*.css"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build": "vite build",
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@
|
|||
|
||||
.markdown-content p {
|
||||
margin-top: 0;
|
||||
/* margin-bottom: 1em; */
|
||||
}
|
||||
|
||||
.markdown-content ul,
|
||||
|
|
|
|||
|
|
@ -82,7 +82,8 @@ const parsePlanEntries = (textOutputs: string[]): PlanEntry[] => {
|
|||
const entries: PlanEntry[] = [];
|
||||
|
||||
// Accept [ ], [x]/[X] and in-progress markers [-] or [*]
|
||||
const todoRe = /^(?:\s*(?:[-*]|\d+[.)])\s*)?\[( |x|X|-|\*)\]\s+(.*)$/;
|
||||
const todoRe =
|
||||
/^(?:\s{0,10}(?:[-*]|\d{1,3}[.)])\s{0,10})?\[( |x|X|-|\*)\]\s+(.{0,500})$/;
|
||||
for (const line of lines) {
|
||||
const m = line.match(todoRe);
|
||||
if (m) {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,9 @@ export const extractCommandOutput = (text: string): string => {
|
|||
}
|
||||
|
||||
// Second try: Extract from structured text format
|
||||
const outputMatch = text.match(/Output:[ \t]*(.+?)(?=\nError:|$)/i);
|
||||
const outputMatch = text.match(
|
||||
/Output:[ \t]{0,20}(.{0,1000}?)(?=\nError:|$)/i,
|
||||
);
|
||||
if (outputMatch && outputMatch[1]) {
|
||||
const output = outputMatch[1].trim();
|
||||
if (output && output !== '(none)' && output.length > 0) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/**
|
||||
* Default CSS variables for @qwen-code/webui
|
||||
* These provide fallback values when running outside of VSCode.
|
||||
|
|
|
|||
|
|
@ -1,178 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Isolated styles for VSCode webview environments to prevent conflicts
|
||||
*/
|
||||
|
||||
/* Isolate all webui styles under a specific namespace to prevent global conflicts in webviews */
|
||||
.qwen-webui-container {
|
||||
/* Apply all CSS variables in the isolated namespace */
|
||||
--app-primary: var(--app-primary, #3b82f6);
|
||||
--app-primary-hover: var(--app-primary-hover, #2563eb);
|
||||
--app-primary-foreground: var(--app-primary-foreground, #e4e4e7);
|
||||
--app-secondary-foreground: var(--app-secondary-foreground, #a1a1aa);
|
||||
|
||||
--app-background: var(--app-background, #1e1e1e);
|
||||
--app-primary-background: var(--app-primary-background, #1e1e1e);
|
||||
--app-background-secondary: var(--app-background-secondary, #252526);
|
||||
--app-secondary-background: var(--app-secondary-background, #252526);
|
||||
--app-background-tertiary: var(--app-background-tertiary, #2d2d2d);
|
||||
|
||||
--app-foreground: var(--app-foreground, #e4e4e7);
|
||||
--app-foreground-secondary: var(--app-foreground-secondary, #a1a1aa);
|
||||
--app-foreground-muted: var(--app-foreground-muted, #71717a);
|
||||
|
||||
--app-border: var(--app-border, #3f3f46);
|
||||
--app-border-focus: var(--app-border-focus, #3b82f6);
|
||||
--app-primary-border-color: var(--app-primary-border-color, #3f3f46);
|
||||
|
||||
--app-success: var(--app-success, #10b981);
|
||||
--app-warning: var(--app-warning, #f59e0b);
|
||||
--app-error: var(--app-error, #ef4444);
|
||||
--app-info: var(--app-info, #3b82f6);
|
||||
|
||||
--app-font-sans: var(--app-font-sans, system-ui, -apple-system, sans-serif);
|
||||
--app-font-mono: var(--app-font-mono, ui-monospace, 'SF Mono', monospace);
|
||||
--app-monospace-font-size: var(--app-monospace-font-size, 13px);
|
||||
|
||||
--app-link-foreground: var(--app-link-foreground, #007acc);
|
||||
--app-link-active-foreground: var(--app-link-active-foreground, #005a9e);
|
||||
|
||||
--app-qwen-ivory: var(--app-qwen-ivory, #f5f5dc);
|
||||
|
||||
--app-radius-sm: var(--app-radius-sm, 0.25rem);
|
||||
--app-radius-md: var(--app-radius-md, 0.375rem);
|
||||
--app-radius-lg: var(--app-radius-lg, 0.5rem);
|
||||
--qwen-corner-radius-small: var(--qwen-corner-radius-small, 6px);
|
||||
--qwen-corner-radius-medium: var(--qwen-corner-radius-medium, 8px);
|
||||
|
||||
--app-spacing-xs: var(--app-spacing-xs, 0.25rem);
|
||||
--app-spacing-sm: var(--app-spacing-sm, 0.5rem);
|
||||
--app-spacing-md: var(--app-spacing-md, 1rem);
|
||||
--app-spacing-medium: var(--app-spacing-medium, 8px);
|
||||
--app-spacing-lg: var(--app-spacing-lg, 1.5rem);
|
||||
--app-spacing-xl: var(--app-spacing-xl, 2rem);
|
||||
|
||||
--app-input-background: var(--app-input-background, #3c3c3c);
|
||||
--app-input-secondary-background: var(--app-input-secondary-background, #2d2d2d);
|
||||
--app-input-border: var(--app-input-border, #3f3f46);
|
||||
--app-input-foreground: var(--app-input-foreground, #e4e4e7);
|
||||
--app-input-placeholder-foreground: var(--app-input-placeholder-foreground, #71717a);
|
||||
|
||||
--app-ghost-button-hover-background: var(--app-ghost-button-hover-background, rgba(90, 93, 94, 0.31));
|
||||
--app-button-background: var(--app-button-background, #3c3c3c);
|
||||
--app-button-foreground: var(--app-button-foreground, #ffffff);
|
||||
--app-transparent-inner-border: var(--app-transparent-inner-border, rgba(255, 255, 255, 0.1));
|
||||
|
||||
--app-header-background: var(--app-header-background, #252526);
|
||||
|
||||
--app-list-padding: var(--app-list-padding, 0px);
|
||||
--app-list-item-padding: var(--app-list-item-padding, 4px 8px);
|
||||
--app-list-border-color: var(--app-list-border-color, transparent);
|
||||
--app-list-border-radius: var(--app-list-border-radius, 4px);
|
||||
--app-list-hover-background: var(--app-list-hover-background, rgba(90, 93, 94, 0.31));
|
||||
--app-list-active-background: var(--app-list-active-background, #094771);
|
||||
--app-list-active-foreground: var(--app-list-active-foreground, #ffffff);
|
||||
--app-list-gap: var(--app-list-gap, 2px);
|
||||
|
||||
--app-menu-background: var(--app-menu-background, #252526);
|
||||
--app-menu-border: var(--app-menu-border, #454545);
|
||||
--app-menu-foreground: var(--app-menu-foreground, #cccccc);
|
||||
--app-menu-selection-background: var(--app-menu-selection-background, #094771);
|
||||
--app-menu-selection-foreground: var(--app-menu-selection-foreground, #ffffff);
|
||||
|
||||
--app-tool-background: var(--app-tool-background, #1e1e1e);
|
||||
--app-code-background: var(--app-code-background, #2d2d2d);
|
||||
|
||||
--app-warning-background: var(--app-warning-background, rgba(255, 204, 0, 0.1));
|
||||
--app-warning-border: var(--app-warning-border, #ffcc00);
|
||||
--app-warning-foreground: var(--app-warning-foreground, #ffcc00);
|
||||
}
|
||||
|
||||
/* Reset potential conflicts with VSCode webview styles */
|
||||
.qwen-webui-container *,
|
||||
.qwen-webui-container *::before,
|
||||
.qwen-webui-container *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Prevent styles from bleeding out of the container */
|
||||
.qwen-webui-container {
|
||||
all: inherit;
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
letter-spacing: inherit;
|
||||
color: var(--app-foreground);
|
||||
background: var(--app-background);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Isolated component styles */
|
||||
.qwen-webui-container .code-block {
|
||||
font-family: var(--app-font-mono);
|
||||
font-size: var(--app-monospace-font-size, 13px);
|
||||
background: var(--app-primary-background);
|
||||
border: 1px solid var(--app-input-border);
|
||||
border-radius: var(--app-radius-sm, 4px);
|
||||
padding: var(--app-spacing-medium, 8px);
|
||||
overflow-x: auto;
|
||||
margin: 4px 0 0 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.qwen-webui-container .diff-display-container {
|
||||
margin: 8px 0;
|
||||
border: 1px solid var(--app-input-border);
|
||||
border-radius: var(--app-radius-md, 6px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qwen-webui-container .diff-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: var(--app-input-secondary-background, var(--app-background-secondary));
|
||||
border-bottom: 1px solid var(--app-input-border);
|
||||
}
|
||||
|
||||
.qwen-webui-container .diff-file-path {
|
||||
font-family: var(--app-font-mono);
|
||||
font-size: 13px;
|
||||
color: var(--app-primary-foreground);
|
||||
}
|
||||
|
||||
.qwen-webui-container .open-diff-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--app-input-border);
|
||||
border-radius: var(--app-radius-sm, 4px);
|
||||
color: var(--app-primary-foreground);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.qwen-webui-container .open-diff-button:hover {
|
||||
background: var(--app-ghost-button-hover-background);
|
||||
}
|
||||
|
||||
.qwen-webui-container .diff-label {
|
||||
padding: 8px 12px;
|
||||
background: var(--app-primary-background);
|
||||
border-bottom: 1px solid var(--app-input-border);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--app-secondary-foreground);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
|
|
@ -15,12 +15,16 @@ import { resolve } from 'path';
|
|||
* Build outputs:
|
||||
* - ESM: dist/index.js (primary format)
|
||||
* - CJS: dist/index.cjs (compatibility)
|
||||
* - UMD: dist/index.umd.js (for CDN usage)
|
||||
* - TypeScript declarations: dist/index.d.ts
|
||||
* - CSS: dist/styles.css (optional styles)
|
||||
*/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
// Use the plugin with development mode settings for UMD builds
|
||||
react({
|
||||
jsxRuntime: 'classic', // Use classic JSX runtime for better CDN compatibility
|
||||
}),
|
||||
dts({
|
||||
include: ['src'],
|
||||
outDir: 'dist',
|
||||
|
|
@ -32,16 +36,20 @@ export default defineConfig({
|
|||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
name: 'QwenCodeWebUI',
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (format) => `index.${format === 'es' ? 'js' : 'cjs'}`,
|
||||
formats: ['es', 'cjs', 'umd'],
|
||||
fileName: (format) => {
|
||||
if (format === 'es') return 'index.js';
|
||||
if (format === 'cjs') return 'index.cjs';
|
||||
if (format === 'umd') return 'index.umd.js';
|
||||
return 'index.js';
|
||||
},
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['react', 'react-dom', 'react/jsx-runtime'],
|
||||
external: ['react', 'react-dom'],
|
||||
output: {
|
||||
globals: {
|
||||
react: 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
'react/jsx-runtime': 'jsxRuntime',
|
||||
},
|
||||
assetFileNames: 'styles.[ext]',
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue