@sanqianx/claude-ai-workbench 0.13.2 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -201
- package/README.md +156 -156
- package/apps/agent-terminal/app.css +636 -613
- package/apps/agent-terminal/app.js +125 -65
- package/apps/agent-terminal/conversation-composer.js +44 -0
- package/apps/agent-terminal/index.html +171 -170
- package/apps/agent-terminal/panel-host-proof.html +152 -152
- package/apps/agent-terminal/panel-proof-content.html +41 -41
- package/apps/agent-terminal/right-workspace-dock.css +187 -159
- package/apps/agent-terminal/right-workspace-dock.js +623 -519
- package/apps/ai-profile-settings-host/index.html +25 -25
- package/apps/ai-profile-settings-host/page.css +1 -1
- package/apps/ai-profile-settings-host/page.js +113 -113
- package/apps/minimal-test-host/README.md +15 -15
- package/apps/minimal-test-host/host.css +2 -2
- package/apps/minimal-test-host/host.js +35 -35
- package/apps/minimal-test-host/index.html +58 -58
- package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -15
- package/docs/sdk/FILE_LAYOUT.md +31 -31
- package/docs/sdk/INTEGRATION.md +126 -126
- package/docs/sdk/README.md +21 -21
- package/docs/sdk/SESSION_EVENTS.md +13 -13
- package/docs/sdk/openapi.yaml +8 -1
- package/package.json +1 -1
- package/packages/client/README.md +29 -29
- package/packages/client/index.d.ts +122 -122
- package/packages/client/index.js +260 -260
- package/packages/client/package.json +2 -2
- package/packages/contracts/README.md +9 -9
- package/packages/contracts/index.d.ts +519 -517
- package/packages/contracts/index.js +312 -312
- package/packages/contracts/package.json +1 -1
- package/packages/core/README.md +8 -8
- package/packages/core/index.d.ts +76 -76
- package/packages/core/index.js +222 -222
- package/packages/core/package.json +2 -2
- package/packages/server/README.md +17 -17
- package/packages/server/bin/agent-terminal-server.js +42 -42
- package/packages/server/bin/claude-workbench.js +225 -225
- package/packages/server/index.d.ts +171 -171
- package/packages/server/index.js +230 -230
- package/packages/server/lib/agent-discovery.js +133 -133
- package/packages/server/lib/agent-registry.js +143 -143
- package/packages/server/lib/agent-spawn-env.js +17 -17
- package/packages/server/lib/attachment-store.js +78 -78
- package/packages/server/lib/bridges/app-server-bridge.js +373 -373
- package/packages/server/lib/bridges/opencode-server-bridge.js +506 -506
- package/packages/server/lib/capture-settings-store.js +35 -35
- package/packages/server/lib/claude-history-recovery.js +87 -87
- package/packages/server/lib/cli-session-recovery.js +28 -28
- package/packages/server/lib/context-resolver.js +55 -55
- package/packages/server/lib/conversation-capture.js +51 -51
- package/packages/server/lib/delegation-phase-store.js +357 -357
- package/packages/server/lib/delegation-store.js +474 -468
- package/packages/server/lib/diagnostics.js +32 -32
- package/packages/server/lib/drivers/cli-json-runner.js +974 -974
- package/packages/server/lib/drivers/codex-driver.js +1442 -1442
- package/packages/server/lib/drivers/opencode-driver.js +1554 -1554
- package/packages/server/lib/drivers/zcode-driver.js +1250 -1250
- package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -71
- package/packages/server/lib/event-stream.js +138 -138
- package/packages/server/lib/folder-picker.js +623 -623
- package/packages/server/lib/http-handler.js +339 -339
- package/packages/server/lib/json-file.js +26 -26
- package/packages/server/lib/knowledge-context.js +33 -33
- package/packages/server/lib/model-detection.js +295 -295
- package/packages/server/lib/parent-context.js +44 -0
- package/packages/server/lib/profile-store.js +283 -283
- package/packages/server/lib/project-store.js +102 -102
- package/packages/server/lib/runtime-loader.js +120 -120
- package/packages/server/lib/session-archive-store.js +53 -53
- package/packages/server/lib/session-history.js +612 -612
- package/packages/server/lib/system-doctor.js +125 -125
- package/packages/server/lib/ui-preferences-store.js +48 -48
- package/packages/server/lib/workbench-service.js +1696 -1675
- package/packages/server/package.json +2 -2
- package/packages/ui/README.md +30 -30
- package/packages/ui/ai-profile-settings.d.ts +45 -45
- package/packages/ui/ai-profile-settings.js +283 -283
- package/packages/ui/claude-terminal.d.ts +31 -31
- package/packages/ui/claude-terminal.js +343 -343
- package/packages/ui/index.d.ts +18 -18
- package/packages/ui/index.js +51 -51
- package/packages/ui/package.json +2 -2
- package/packages/ui/theme.css +13 -13
- package/src/assets/claude-prompts.json +21 -21
- package/src/backend/http/ai-profile-routes.js +61 -61
- package/src/backend/http/claude-routes.js +170 -170
- package/src/backend/http/node-helpers.js +45 -45
- package/src/backend/index.js +21 -21
- package/src/backend/lib/ai-adapter.js +144 -144
- package/src/backend/lib/ai-profile-store.js +259 -259
- package/src/backend/lib/ai-vendor-presets.js +83 -83
- package/src/backend/lib/ai-workspace.js +140 -140
- package/src/backend/lib/automation-config.js +253 -253
- package/src/backend/lib/claude-cli-runner.js +2392 -2392
- package/src/backend/lib/data-dir.js +169 -169
- package/src/backend/lib/llm-client.js +176 -176
- package/src/backend/lib/model-context-windows.js +82 -82
- package/src/backend/lib/native-task-control.js +217 -217
- package/src/backend/lib/native-task-history-index.js +475 -475
- package/src/backend/lib/native-task-history-reader.js +526 -526
- package/src/backend/lib/native-task-messages.js +457 -457
- package/src/backend/lib/native-task-registry.js +325 -325
- package/src/backend/lib/native-task-runtime.js +242 -242
- package/src/backend/lib/native-task-trace.js +440 -440
- package/src/backend/lib/native-task-transcript.js +393 -393
- package/src/backend/lib/prompt-registry.js +166 -166
- package/src/backend/lib/runtime-config.js +121 -121
- package/src/backend/lib/sdk-session-stream.js +143 -143
- package/src/backend/lib/sdk-turn-stream.js +16 -16
- package/src/backend/lib/store-scope.js +131 -131
- package/src/backend/lib/task-identity.js +104 -104
- package/src/backend/lib/task-state-projection.js +58 -58
- package/src/index.js +7 -7
- package/src/sdk-wave-a.ts +1 -1
- package/src/sdk-wave-b.ts +1 -1
- package/src/sdk-wave-c.ts +1 -1
- package/src/sdk-wave-d.ts +1 -1
- package/src/sdk-wave-e.ts +1 -1
|
@@ -1,343 +1,343 @@
|
|
|
1
|
-
(function (global, factory) {
|
|
2
|
-
let core = global.ClaudeWorkbenchCore;
|
|
3
|
-
if (!core && typeof require === 'function') {
|
|
4
|
-
try { core = require('@claude-ai-workbench/core'); }
|
|
5
|
-
catch { try { core = require('../core'); } catch {} }
|
|
6
|
-
}
|
|
7
|
-
const exported = factory(core);
|
|
8
|
-
if (typeof module !== 'undefined' && module.exports) module.exports = exported;
|
|
9
|
-
global.ClaudeWorkbenchPane = exported.ClaudeWorkbenchPane;
|
|
10
|
-
})(typeof window !== 'undefined' ? window : globalThis, function (Core) {
|
|
11
|
-
'use strict';
|
|
12
|
-
|
|
13
|
-
const COPY = {
|
|
14
|
-
'zh-CN': {
|
|
15
|
-
title: 'Claude Code', connecting: '正在连接', connected: '已连接', reconnecting: '正在重连', offline: '未连接',
|
|
16
|
-
emptyTitle: '开始与 Claude Code 对话', emptyBody: '消息和工具调用只属于当前项目。', noContext: '请先选择项目',
|
|
17
|
-
placeholder: '给 Claude 发送消息…', send: '发送', stop: '停止', default: '默认模式', acceptEdits: '自动接受编辑',
|
|
18
|
-
plan: '计划模式', bypassPermissions: '跳过权限确认', token: '上下文', permission: '需要你的确认', allow: '允许', deny: '拒绝',
|
|
19
|
-
running: '正在执行', done: '已完成', failed: '失败', thinking: '正在思考', unavailable: '终端组件未连接 Core。', attach: '附加图片', commands: '显示斜杠命令',
|
|
20
|
-
commandTitle: 'Claude Code 命令', commandLoading: '正在读取当前环境支持的命令…', commandEmpty: '没有匹配的命令', removeImage: '移除图片', imageOnly: '图片',
|
|
21
|
-
invalidImage: '仅支持 JPEG、PNG、GIF、WebP 图片。', imageTooLarge: '单张图片不能超过 10 MB。', imageTotalTooLarge: '本次图片总大小不能超过 20 MB。', tooManyImages: '一次最多发送 20 张图片。',
|
|
22
|
-
},
|
|
23
|
-
en: {
|
|
24
|
-
title: 'Claude Code', connecting: 'Connecting', connected: 'Connected', reconnecting: 'Reconnecting', offline: 'Offline',
|
|
25
|
-
emptyTitle: 'Start a Claude Code conversation', emptyBody: 'Messages and tool calls stay with this project.', noContext: 'Select a project first',
|
|
26
|
-
placeholder: 'Message Claude…', send: 'Send', stop: 'Stop', default: 'Default Mode', acceptEdits: 'Accept edits',
|
|
27
|
-
plan: 'Plan mode', bypassPermissions: 'Bypass permissions', token: 'Context', permission: 'Your approval is required', allow: 'Allow', deny: 'Deny',
|
|
28
|
-
running: 'Running', done: 'Done', failed: 'Failed', thinking: 'Thinking', unavailable: 'Terminal Core is not connected.', attach: 'Attach image', commands: 'Show slash commands',
|
|
29
|
-
commandTitle: 'Claude Code commands', commandLoading: 'Loading commands supported by this environment…', commandEmpty: 'No matching commands', removeImage: 'Remove image', imageOnly: 'Image',
|
|
30
|
-
invalidImage: 'Only JPEG, PNG, GIF, and WebP images are supported.', imageTooLarge: 'Each image must be 10 MB or smaller.', imageTotalTooLarge: 'Images in one message must total 20 MB or less.', tooManyImages: 'A message can contain at most 20 images.',
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const CSS = `
|
|
35
|
-
:host{display:block;min-width:280px;height:100%;color:var(--caw-text,#172231);font:13px/1.55 Inter,"Segoe UI",system-ui,sans-serif}
|
|
36
|
-
*{box-sizing:border-box}button,textarea,select{font:inherit;color:inherit}button{cursor:pointer}
|
|
37
|
-
.terminal{height:100%;min-height:420px;display:grid;grid-template-rows:auto minmax(0,1fr) auto;background:var(--caw-panel,#fff);border:1px solid var(--caw-line,#d7dfe8);border-radius:12px;overflow:hidden;box-shadow:0 10px 34px rgba(24,34,48,.08)}
|
|
38
|
-
.bar{height:46px;padding:0 13px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid var(--caw-line,#d7dfe8);background:var(--caw-panel,#fff)}
|
|
39
|
-
.identity,.status{display:flex;align-items:center;min-width:0}.identity{gap:9px;font-weight:650}.status{gap:6px;color:var(--caw-muted,#687789);font-size:11px;white-space:nowrap}
|
|
40
|
-
.dot{width:8px;height:8px;border-radius:50%;background:var(--caw-muted,#687789);box-shadow:0 0 0 3px color-mix(in srgb,var(--caw-muted,#687789) 14%,transparent)}
|
|
41
|
-
.dot.connected{background:var(--caw-good,#2f7d64)}.dot.connecting,.dot.reconnecting,.dot.running{background:var(--caw-warn,#9a7428)}.dot.error{background:var(--caw-bad,#af4b4b)}
|
|
42
|
-
.profile{max-width:156px;overflow:hidden;text-overflow:ellipsis;padding:2px 7px;border:1px solid var(--caw-line,#d7dfe8);border-radius:999px;background:var(--caw-panel-2,#f6f8fb);color:var(--caw-muted,#687789)}
|
|
43
|
-
.messages{min-height:0;overflow:auto;padding:15px 14px 22px;background:var(--caw-panel-2,#f6f8fb);scrollbar-width:thin;scrollbar-color:var(--caw-line-strong,#bcc8d5) transparent}
|
|
44
|
-
.empty{height:100%;min-height:210px;display:grid;place-items:center;text-align:center;color:var(--caw-muted,#687789)}.empty strong{display:block;margin-bottom:3px;color:var(--caw-text,#172231);font-size:14px}
|
|
45
|
-
.line{margin:0 0 12px}.line.user{display:flex;justify-content:flex-end}.bubble{max-width:92%;border-radius:10px;padding:8px 10px;overflow-wrap:anywhere}
|
|
46
|
-
.user .bubble{max-width:86%;background:var(--caw-accent,#4f6f8f);color:#fff;border-bottom-right-radius:3px}.assistant .bubble{padding:0 2px;color:var(--caw-text,#172231)}
|
|
47
|
-
.markdown p{margin:0 0 8px}.markdown p:last-child{margin-bottom:0}.markdown h1,.markdown h2,.markdown h3{margin:12px 0 6px;line-height:1.3}.markdown h1{font-size:17px}.markdown h2{font-size:15px}.markdown h3{font-size:14px}.markdown ul{margin:5px 0;padding-left:20px}
|
|
48
|
-
code{font-family:"Cascadia Code",Consolas,monospace;font-size:.92em;background:var(--caw-panel-3,#e8edf3);border-radius:4px;padding:1px 4px}.code{margin:8px 0;padding:9px 10px;overflow:auto;border:1px solid var(--caw-line,#d7dfe8);border-radius:7px;background:var(--caw-panel,#fff);font:12px/1.55 "Cascadia Code",Consolas,monospace;white-space:pre}
|
|
49
|
-
.thinking,.system,.status-line{color:var(--caw-muted,#687789);font-size:12px}.thinking{font-style:italic}.error{padding:8px 10px;border:1px solid color-mix(in srgb,var(--caw-bad,#af4b4b) 28%,transparent);border-radius:8px;background:var(--caw-bad-soft,rgba(175,75,75,.11));color:var(--caw-bad,#af4b4b)}
|
|
50
|
-
details.tool{border:1px solid var(--caw-line,#d7dfe8);border-radius:8px;background:var(--caw-panel,#fff);overflow:hidden}details.tool summary{list-style:none;display:flex;align-items:center;gap:7px;padding:7px 9px;cursor:pointer}details.tool summary::-webkit-details-marker{display:none}.tool-name{font-weight:600}.tool-state{margin-left:auto;color:var(--caw-muted,#687789);font-size:11px}.tool-body{padding:0 9px 9px}.tool-body pre{max-height:220px;margin:5px 0 0;padding:8px;overflow:auto;border-radius:6px;background:var(--caw-panel-2,#f6f8fb);white-space:pre-wrap;font:11px/1.5 "Cascadia Code",Consolas,monospace}
|
|
51
|
-
.permission{padding:10px;border:1px solid color-mix(in srgb,var(--caw-warn,#9a7428) 35%,transparent);border-radius:9px;background:var(--caw-warn-soft,rgba(154,116,40,.12))}.permission-head{display:flex;gap:8px;align-items:baseline}.permission-head strong{font-size:12px}.permission-head span{color:var(--caw-muted,#687789);font:11px "Cascadia Code",Consolas,monospace}.permission-detail{margin-top:5px;color:var(--caw-muted,#687789);font-size:12px;overflow-wrap:anywhere}.permission-actions{display:flex;justify-content:flex-end;gap:7px;margin-top:9px}
|
|
52
|
-
.small{min-width:60px;padding:5px 9px;border:1px solid var(--caw-line-strong,#bcc8d5);border-radius:6px;background:var(--caw-panel,#fff)}.small.primary{border-color:var(--caw-accent,#4f6f8f);background:var(--caw-accent,#4f6f8f);color:#fff}.small:hover{filter:brightness(.97)}
|
|
53
|
-
.composer{position:relative;padding:8px 8px 7px;border-top:1px solid var(--caw-line,#d7dfe8);background:var(--caw-panel-2,#f6f8fb)}.composer.dragging{outline:2px solid var(--caw-accent,#4f6f8f);outline-offset:-3px}.input{display:flex;align-items:flex-end;gap:8px}.input:focus-within textarea{border-color:var(--caw-accent,#4f6f8f);box-shadow:0 0 0 2px var(--caw-accent-soft,rgba(79,111,143,.12))}
|
|
54
|
-
.attachment-input{display:none}.attachment-tray{display:flex;gap:7px;overflow-x:auto;padding:0 0 7px}.attachment{position:relative;flex:0 0 62px;height:52px;border:1px solid var(--caw-line,#d7dfe8);border-radius:7px;overflow:hidden;background:var(--caw-panel,#fff)}.attachment img{width:100%;height:100%;display:block;object-fit:cover}.attachment-remove{position:absolute;right:2px;top:2px;width:18px;height:18px;display:grid;place-items:center;padding:0;border:0;border-radius:50%;background:rgba(20,28,38,.78);color:#fff;font-size:13px;line-height:1}.attachment-name{position:absolute;left:0;right:0;bottom:0;padding:2px 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;background:linear-gradient(transparent,rgba(20,28,38,.8));color:#fff;font-size:8px}.image-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(112px,220px));gap:7px;margin-bottom:7px}.image-card{display:block;overflow:hidden;border:1px solid var(--caw-line,#d7dfe8);border-radius:8px;background:var(--caw-panel,#fff)}.image-card img{display:block;width:100%;max-height:260px;object-fit:contain;background:var(--caw-panel-3,#e8edf3)}.image-card span{display:block;padding:4px 7px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--caw-muted,#687789);font-size:9px}.user .image-grid{justify-content:end}.user .image-card{border-color:color-mix(in srgb,var(--caw-accent,#4f6f8f) 55%,white)}
|
|
55
|
-
textarea{width:100%;min-height:34px;max-height:130px;padding:7px 10px;border:1px solid var(--caw-line-strong,#bcc8d5);border-radius:5px;outline:0;resize:none;overflow-y:hidden;background:var(--caw-panel,#fff);font:12px/1.55 "Cascadia Code",Consolas,monospace;transition:border-color .15s,box-shadow .15s}textarea::placeholder{color:var(--caw-muted,#687789)}textarea:disabled{cursor:not-allowed}
|
|
56
|
-
.send{flex:0 0 34px;width:34px;height:34px;display:grid;place-items:center;border:0;border-radius:7px;background:var(--caw-accent,#4f6f8f);color:#fff;font-weight:700}.send.stop{background:var(--caw-bad,#af4b4b)}.send:disabled{opacity:.45;cursor:not-allowed}
|
|
57
|
-
.footer{min-height:28px;margin-top:6px;display:flex;align-items:center;gap:6px;color:var(--caw-muted,#687789);font-size:11px}.tool-button,.pill{height:28px;display:flex;align-items:center;justify-content:center;border:1px solid transparent;border-radius:5px;background:transparent;color:var(--caw-muted,#687789)}.tool-button{position:relative;width:28px;padding:0}.tool-button:hover{background:var(--caw-panel-3,#e8edf3);color:var(--caw-text,#172231)}.pill{gap:6px;padding:0 8px;border-color:var(--caw-line,#d7dfe8);background:var(--caw-panel,#fff);white-space:nowrap}.pill:hover{border-color:var(--caw-line-strong,#bcc8d5)}.mode-dot{width:8px;height:8px;border-radius:50%}.chevron{width:9px;height:9px}.usage{font-family:"Cascadia Code",Consolas,monospace;font-variant-numeric:tabular-nums}.ring{--p:0;width:15px;height:15px;border-radius:50%;background:conic-gradient(var(--caw-good,#2f7d64) calc(var(--p)*1%),color-mix(in srgb,var(--caw-good,#2f7d64) 22%,transparent) 0);position:relative}.ring:after{content:"";position:absolute;inset:3px;border-radius:50%;background:var(--caw-panel,#fff)}.count{position:absolute;right:-4px;top:-5px;min-width:16px;height:16px;padding:0 4px;display:grid;place-items:center;border-radius:999px;background:var(--caw-accent,#4f6f8f);color:#fff;font-size:9px;font-weight:650}
|
|
58
|
-
.popup{position:absolute;left:8px;bottom:42px;z-index:20;width:min(360px,calc(100% - 16px));max-height:min(330px,55vh);overflow:auto;padding:5px;border:1px solid var(--caw-line,#d7dfe8);border-radius:8px;background:var(--caw-panel,#fff);box-shadow:0 14px 38px rgba(24,34,48,.16)}.popup-title,.popup-empty{padding:5px 7px;color:var(--caw-muted,#687789);font-size:10px}.popup-title{text-transform:uppercase;letter-spacing:.06em}.popup-row{width:100%;display:flex;align-items:flex-start;gap:8px;padding:7px;border:0;border-radius:6px;background:transparent;text-align:left}.popup-row:hover,.popup-row.active{background:var(--caw-panel-2,#f6f8fb)}.popup-row .mode-dot{margin-top:5px;flex:none}.popup-copy{display:grid;min-width:0}.popup-copy strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.popup-copy small{color:var(--caw-muted,#687789);font-size:10px}.command-mark{width:20px;height:20px;display:grid;place-items:center;flex:none;border-radius:5px;background:var(--caw-accent,#4f6f8f);color:#fff;font:10px "Cascadia Code",Consolas,monospace}
|
|
59
|
-
@media(max-width:430px){.terminal{border-radius:0;border-left:0;border-right:0}.profile{max-width:105px}.messages{padding-inline:11px}}
|
|
60
|
-
`;
|
|
61
|
-
|
|
62
|
-
const MODES = [
|
|
63
|
-
{ id: 'default', color: '#94a3b8' }, { id: 'acceptEdits', color: '#22c55e' },
|
|
64
|
-
{ id: 'bypassPermissions', color: '#f59e0b' }, { id: 'plan', color: '#a855f7' },
|
|
65
|
-
];
|
|
66
|
-
const IMAGE_TYPES = new Set(['image/jpeg', 'image/png', 'image/gif', 'image/webp']);
|
|
67
|
-
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
68
|
-
const MAX_IMAGE_TOTAL_BYTES = 20 * 1024 * 1024;
|
|
69
|
-
const MAX_IMAGES = 20;
|
|
70
|
-
|
|
71
|
-
function escapeHtml(value) {
|
|
72
|
-
return String(value == null ? '' : value).replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char]);
|
|
73
|
-
}
|
|
74
|
-
function json(value) {
|
|
75
|
-
if (value == null || value === '') return '';
|
|
76
|
-
if (typeof value === 'string') return value;
|
|
77
|
-
try { return JSON.stringify(value, null, 2); } catch { return String(value); }
|
|
78
|
-
}
|
|
79
|
-
function markdown(value) {
|
|
80
|
-
const escaped = escapeHtml(value);
|
|
81
|
-
const blocks = [];
|
|
82
|
-
let body = escaped.replace(/```(?:[\w-]+)?\n([\s\S]*?)```/g, (_, code) => {
|
|
83
|
-
blocks.push(`<pre class="code">${code.replace(/\n$/, '')}</pre>`);
|
|
84
|
-
return `\u0000${blocks.length - 1}\u0000`;
|
|
85
|
-
});
|
|
86
|
-
body = body.replace(/^### (.+)$/gm, '<h3>$1</h3>').replace(/^## (.+)$/gm, '<h2>$1</h2>').replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
|
87
|
-
body = body.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>').replace(/`([^`\n]+)`/g, '<code>$1</code>');
|
|
88
|
-
const sections = body.split(/\n{2,}/).map(part => {
|
|
89
|
-
if (/^\u0000\d+\u0000$/.test(part) || /^<h[1-3]>/.test(part)) return part;
|
|
90
|
-
if (/^(?:- .+(?:\n|$))+/.test(part)) return `<ul>${part.split('\n').map(line => `<li>${line.replace(/^- /, '')}</li>`).join('')}</ul>`;
|
|
91
|
-
return `<p>${part.replace(/\n/g, '<br>')}</p>`;
|
|
92
|
-
}).join('');
|
|
93
|
-
return sections.replace(/\u0000(\d+)\u0000/g, (_, index) => blocks[Number(index)] || '');
|
|
94
|
-
}
|
|
95
|
-
function normalizeContext(input) {
|
|
96
|
-
if (!input) return null;
|
|
97
|
-
return { ...input, contextId: input.contextId || input.projectId || '' };
|
|
98
|
-
}
|
|
99
|
-
function safeImageSource(value, mediaType) {
|
|
100
|
-
const source = String(value || '');
|
|
101
|
-
if (/^\/api\//.test(source) || /^https?:\/\//i.test(source)) return source;
|
|
102
|
-
if (/^data:image\/(?:jpeg|png|gif|webp);base64,/i.test(source)) return source;
|
|
103
|
-
if (source && IMAGE_TYPES.has(mediaType)) return `data:${mediaType};base64,${source}`;
|
|
104
|
-
return '';
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
class ClaudeWorkbenchPane extends HTMLElement {
|
|
108
|
-
constructor() {
|
|
109
|
-
super();
|
|
110
|
-
this.attachShadow({ mode: 'open' });
|
|
111
|
-
this._client = null;
|
|
112
|
-
this._context = null;
|
|
113
|
-
this._locale = 'zh-CN';
|
|
114
|
-
this._draft = '';
|
|
115
|
-
this._attachments = [];
|
|
116
|
-
this._modeMenu = false;
|
|
117
|
-
this._commandMenu = false;
|
|
118
|
-
this._commandIndex = 0;
|
|
119
|
-
this._dragDepth = 0;
|
|
120
|
-
this._renderQueued = false;
|
|
121
|
-
this._controller = null;
|
|
122
|
-
this._ensureController();
|
|
123
|
-
}
|
|
124
|
-
connectedCallback() {
|
|
125
|
-
this._ensureController();
|
|
126
|
-
this.render();
|
|
127
|
-
if (this._controller && this._client) this._controller.setAdapter(this._client);
|
|
128
|
-
if (this._controller && this._context) this._controller.setContext(this._context);
|
|
129
|
-
}
|
|
130
|
-
disconnectedCallback() { if (!this.hasAttribute('preserve-controller')) this.destroy(); }
|
|
131
|
-
set client(value) { this._client = value || null; this._ensureController(); if (this._controller) this._controller.setAdapter(this._client); }
|
|
132
|
-
get client() { return this._client; }
|
|
133
|
-
set adapter(value) { this.client = value; }
|
|
134
|
-
get adapter() { return this._client; }
|
|
135
|
-
set context(value) {
|
|
136
|
-
this._context = normalizeContext(value);
|
|
137
|
-
this._ensureController();
|
|
138
|
-
if (this._controller && this.isConnected) this._controller.setContext(this._context);
|
|
139
|
-
else this.render();
|
|
140
|
-
}
|
|
141
|
-
get context() { return this._context; }
|
|
142
|
-
set locale(value) { this._locale = COPY[value] ? value : (String(value).toLowerCase().startsWith('en') ? 'en' : 'zh-CN'); this.render(); }
|
|
143
|
-
get locale() { return this._locale; }
|
|
144
|
-
get controller() { return this._controller; }
|
|
145
|
-
_ensureController() {
|
|
146
|
-
if (!this._controller && Core) this._controller = Core.createController({ adapter: this._client, onChange: state => this._onState(state) });
|
|
147
|
-
return this._controller;
|
|
148
|
-
}
|
|
149
|
-
async newSession() { return this._run(() => this._controller.newSession()); }
|
|
150
|
-
async restoreSession(id) { return this._run(() => this._controller.restoreSession(id)); }
|
|
151
|
-
focusInput() { const input = this.shadowRoot.querySelector('textarea'); if (input) { input.focus(); input.setSelectionRange(input.value.length, input.value.length); } }
|
|
152
|
-
destroy() { if (this._controller) this._controller.destroy(); this._controller = null; }
|
|
153
|
-
_copy() { return COPY[this._locale] || COPY['zh-CN']; }
|
|
154
|
-
_onState(state) {
|
|
155
|
-
this.dispatchEvent(new CustomEvent('terminal-state-change', { detail: state }));
|
|
156
|
-
this.dispatchEvent(new CustomEvent('workbench-state-change', { detail: state }));
|
|
157
|
-
this.dispatchEvent(new CustomEvent('running-change', { detail: { running: Boolean(state.turnRunning) } }));
|
|
158
|
-
if (!this._renderQueued) {
|
|
159
|
-
this._renderQueued = true;
|
|
160
|
-
queueMicrotask(() => { this._renderQueued = false; if (this.isConnected) this.render(); });
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
async _run(action) {
|
|
164
|
-
try { return await action(); }
|
|
165
|
-
catch (error) {
|
|
166
|
-
this.dispatchEvent(new CustomEvent('terminal-error', { detail: error }));
|
|
167
|
-
this.dispatchEvent(new CustomEvent('workbench-error', { detail: error }));
|
|
168
|
-
throw error;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
_status(state) {
|
|
172
|
-
if (state.error) return ['error', this._copy().failed];
|
|
173
|
-
if (state.turnRunning) return ['running', this._copy().running];
|
|
174
|
-
const connection = state.session.connectionState || 'offline';
|
|
175
|
-
return [connection, this._copy()[connection] || connection];
|
|
176
|
-
}
|
|
177
|
-
_modeLabel(mode) { return this._copy()[mode] || mode; }
|
|
178
|
-
_modeColor(mode) { return (MODES.find(item => item.id === mode) || MODES[0]).color; }
|
|
179
|
-
_appendLocalInfo(text, kind = 'status') {
|
|
180
|
-
if (!this._controller) return;
|
|
181
|
-
this._controller.state.lines.push({ id: `local-${Date.now()}`, at: new Date().toISOString(), kind, text });
|
|
182
|
-
this.render();
|
|
183
|
-
}
|
|
184
|
-
_commands(state) {
|
|
185
|
-
const query = this._draft.startsWith('/') ? this._draft.slice(1).trimStart().toLowerCase() : '';
|
|
186
|
-
const commands = Array.isArray(state && state.commands) ? state.commands : [];
|
|
187
|
-
if (!query || /\s/.test(query)) return /\s/.test(query) ? [] : commands;
|
|
188
|
-
return commands.filter(item => {
|
|
189
|
-
const haystack = [item.name, ...(item.aliases || [])].join(' ').toLowerCase();
|
|
190
|
-
return haystack.includes(query);
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
_selectCommand(command) {
|
|
194
|
-
this._commandMenu = false;
|
|
195
|
-
const name = command && command.name || String(command || '').replace(/^\//, '');
|
|
196
|
-
const event = new CustomEvent('terminal-command', { bubbles: true, composed: true, cancelable: true, detail: { command: name, commandInfo: command, context: this._context, state: this._controller && this._controller.state } });
|
|
197
|
-
if (!this.dispatchEvent(event)) { this.render(); return; }
|
|
198
|
-
this._draft = `/${name}${command && command.argumentHint ? ' ' : ''}`;
|
|
199
|
-
this.render();
|
|
200
|
-
this.focusInput();
|
|
201
|
-
}
|
|
202
|
-
_reportAttachmentError(message) {
|
|
203
|
-
const error = new Error(message);
|
|
204
|
-
this.dispatchEvent(new CustomEvent('terminal-error', { detail: error }));
|
|
205
|
-
this.dispatchEvent(new CustomEvent('workbench-error', { detail: error }));
|
|
206
|
-
this._appendLocalInfo(message, 'error');
|
|
207
|
-
}
|
|
208
|
-
async _addFiles(fileList) {
|
|
209
|
-
const files = Array.from(fileList || []).filter(Boolean);
|
|
210
|
-
if (!files.length) return;
|
|
211
|
-
if (this._attachments.length + files.length > MAX_IMAGES) return this._reportAttachmentError(this._copy().tooManyImages);
|
|
212
|
-
let total = this._attachments.reduce((sum, item) => sum + Number(item.size || 0), 0);
|
|
213
|
-
for (const file of files) {
|
|
214
|
-
if (!IMAGE_TYPES.has(file.type)) { this._reportAttachmentError(this._copy().invalidImage); continue; }
|
|
215
|
-
if (file.size > MAX_IMAGE_BYTES) { this._reportAttachmentError(this._copy().imageTooLarge); continue; }
|
|
216
|
-
if (total + file.size > MAX_IMAGE_TOTAL_BYTES) { this._reportAttachmentError(this._copy().imageTotalTooLarge); break; }
|
|
217
|
-
const dataUrl = await new Promise((resolve, reject) => {
|
|
218
|
-
const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || '')); reader.onerror = () => reject(reader.error || new Error('Unable to read image')); reader.readAsDataURL(file);
|
|
219
|
-
});
|
|
220
|
-
const data = dataUrl.slice(dataUrl.indexOf(',') + 1);
|
|
221
|
-
this._attachments.push({ id: `image-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`, name: file.name || this._copy().imageOnly, mediaType: file.type, size: file.size, data, previewUrl: dataUrl });
|
|
222
|
-
total += file.size;
|
|
223
|
-
}
|
|
224
|
-
this.dispatchEvent(new CustomEvent('terminal-attachments-change', { detail: { attachments: this._attachments.map(({ previewUrl, ...item }) => item) } }));
|
|
225
|
-
this.render();
|
|
226
|
-
this.focusInput();
|
|
227
|
-
}
|
|
228
|
-
_removeAttachment(id) {
|
|
229
|
-
this._attachments = this._attachments.filter(item => item.id !== id);
|
|
230
|
-
this.dispatchEvent(new CustomEvent('terminal-attachments-change', { detail: { attachments: this._attachments.map(({ previewUrl, ...item }) => item) } }));
|
|
231
|
-
this.render();
|
|
232
|
-
this.focusInput();
|
|
233
|
-
}
|
|
234
|
-
_openAttachmentPicker() {
|
|
235
|
-
const event = new CustomEvent('terminal-attach-request', { bubbles: true, composed: true, cancelable: true, detail: { context: this._context, addFiles: files => this._addFiles(files) } });
|
|
236
|
-
if (this.dispatchEvent(event)) this.shadowRoot.querySelector('.attachment-input')?.click();
|
|
237
|
-
}
|
|
238
|
-
_line(item, state) {
|
|
239
|
-
const copy = this._copy();
|
|
240
|
-
const imageCards = attachments => `<div class="image-grid">${(attachments || []).map(image => { const source = safeImageSource(image.url || image.data, image.mediaType); return source ? `<a class="image-card" href="${escapeHtml(source)}" target="_blank" rel="noreferrer"><img src="${escapeHtml(source)}" alt="${escapeHtml(image.name || copy.imageOnly)}"><span>${escapeHtml(image.name || copy.imageOnly)}</span></a>` : ''; }).join('')}</div>`;
|
|
241
|
-
if (item.kind === 'user') return `<div class="line user"><div class="bubble">${imageCards(item.attachments)}${item.text ? escapeHtml(item.text) : ''}</div></div>`;
|
|
242
|
-
if (item.kind === 'image') return `<div class="line assistant">${imageCards([item])}</div>`;
|
|
243
|
-
if (item.kind === 'assistant') return item.text ? `<div class="line assistant"><div class="bubble markdown">${markdown(item.text)}</div></div>` : '';
|
|
244
|
-
if (item.kind === 'thinking') return `<div class="line thinking">${escapeHtml(item.text || copy.thinking)}</div>`;
|
|
245
|
-
if (item.kind === 'error') return `<div class="line error">${escapeHtml(item.text)}</div>`;
|
|
246
|
-
if (item.kind === 'system' || item.kind === 'status') return `<div class="line ${item.kind === 'system' ? 'system' : 'status-line'}">${escapeHtml(item.text)}</div>`;
|
|
247
|
-
if (item.kind === 'permission') {
|
|
248
|
-
const active = state.permission && (state.permission.requestId === item.id || state.permission.requestId === item.requestId);
|
|
249
|
-
const detail = item.text || json(item.input);
|
|
250
|
-
return `<div class="line permission"><div class="permission-head"><strong>${escapeHtml(copy.permission)}</strong><span>${escapeHtml(item.name || '')}</span></div>${detail ? `<div class="permission-detail">${escapeHtml(detail)}</div>` : ''}${active ? `<div class="permission-actions"><button class="small" data-action="deny">${copy.deny}</button><button class="small primary" data-action="allow">${copy.allow}</button></div>` : ''}</div>`;
|
|
251
|
-
}
|
|
252
|
-
if (item.kind === 'tool' || item.kind === 'tool-result') {
|
|
253
|
-
const status = item.status === 'error' ? copy.failed : item.status === 'running' ? copy.running : copy.done;
|
|
254
|
-
const input = json(item.input); const result = item.result || item.text || '';
|
|
255
|
-
return `<details class="line tool"><summary><span>›</span><span class="tool-name">${escapeHtml(item.name || item.toolUseId || 'Tool')}</span><span class="tool-state">${escapeHtml(status)}</span></summary><div class="tool-body">${input ? `<pre>${escapeHtml(input)}</pre>` : ''}${result ? `<pre>${escapeHtml(result)}</pre>` : ''}</div></details>`;
|
|
256
|
-
}
|
|
257
|
-
return item.text ? `<div class="line status-line">${escapeHtml(item.text)}</div>` : '';
|
|
258
|
-
}
|
|
259
|
-
render() {
|
|
260
|
-
const copy = this._copy();
|
|
261
|
-
const state = this._controller ? this._controller.state : {
|
|
262
|
-
session: { connectionState: 'offline' }, lines: [], tokenUsage: { used: 0, total: 200000, hasUsage: false },
|
|
263
|
-
permissionMode: 'default', requestPending: false, turnRunning: false, error: copy.unavailable,
|
|
264
|
-
};
|
|
265
|
-
const [statusClass, statusText] = this._status(state);
|
|
266
|
-
const context = this._context || state.context || {};
|
|
267
|
-
const profile = state.session.aiProfileId || context.aiProfileId || '';
|
|
268
|
-
const usage = state.tokenUsage || {};
|
|
269
|
-
const percent = usage.hasUsage && usage.total ? Math.min(100, (usage.used / usage.total) * 100) : 0;
|
|
270
|
-
const mode = state.permissionMode || 'default';
|
|
271
|
-
const commands = this._commands(state);
|
|
272
|
-
if (this._commandIndex >= commands.length) this._commandIndex = 0;
|
|
273
|
-
const imageIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>';
|
|
274
|
-
const commandIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
|
|
275
|
-
const sendIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>';
|
|
276
|
-
const chevron = '<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>';
|
|
277
|
-
const modePopup = this._modeMenu ? `<div class="popup mode-popup"><div class="popup-title">Permission mode</div>${MODES.map(item => `<button class="popup-row ${item.id === mode ? 'active' : ''}" data-mode="${item.id}"><i class="mode-dot" style="background:${item.color}"></i><span class="popup-copy"><strong>${escapeHtml(this._modeLabel(item.id))}</strong></span></button>`).join('')}</div>` : '';
|
|
278
|
-
const commandRows = commands.map((item, index) => `<button class="popup-row ${index === this._commandIndex ? 'active' : ''}" data-command-index="${index}"><i class="command-mark">›_</i><span class="popup-copy"><strong>/${escapeHtml(item.name)}${item.argumentHint ? ` ${escapeHtml(item.argumentHint)}` : ''}</strong>${item.description ? `<small>${escapeHtml(item.description)}</small>` : ''}</span></button>`).join('');
|
|
279
|
-
const commandEmpty = state.commandError ? escapeHtml(state.commandError) : (Array.isArray(state.commands) && state.commands.length ? copy.commandEmpty : copy.commandLoading);
|
|
280
|
-
const commandPopup = this._commandMenu ? `<div class="popup command-popup"><div class="popup-title">${escapeHtml(copy.commandTitle)}</div>${commandRows || `<div class="popup-empty">${commandEmpty}</div>`}</div>` : '';
|
|
281
|
-
const disabled = !context.contextId || state.requestPending || Boolean(state.permission);
|
|
282
|
-
const messageHtml = state.lines.length ? state.lines.map(line => this._line(line, state)).join('') : `<div class="empty"><div><strong>${escapeHtml(context.contextId ? copy.emptyTitle : copy.noContext)}</strong>${context.contextId ? `<span>${escapeHtml(copy.emptyBody)}</span>` : ''}</div></div>`;
|
|
283
|
-
const attachmentTray = this._attachments.length ? `<div class="attachment-tray">${this._attachments.map(item => `<div class="attachment" title="${escapeHtml(item.name)}"><img src="${escapeHtml(item.previewUrl)}" alt="${escapeHtml(item.name)}"><span class="attachment-name">${escapeHtml(item.name)}</span><button class="attachment-remove" data-remove-attachment="${escapeHtml(item.id)}" title="${escapeHtml(copy.removeImage)}" aria-label="${escapeHtml(copy.removeImage)}">×</button></div>`).join('')}</div>` : '';
|
|
284
|
-
const commandCount = Array.isArray(state.commands) ? state.commands.length : 0;
|
|
285
|
-
this.shadowRoot.innerHTML = `<style>${CSS}</style><section class="terminal" aria-label="Claude Code terminal"><header class="bar"><div class="identity"><i class="dot ${statusClass}"></i><span>${copy.title}</span></div><div class="status"><span>${escapeHtml(statusText)}</span>${profile ? `<span class="profile" title="${escapeHtml(profile)}">${escapeHtml(profile)}</span>` : ''}</div></header><main class="messages" aria-live="polite">${messageHtml}</main><footer class="composer">${modePopup}${commandPopup}${attachmentTray}<input class="attachment-input" type="file" accept="image/jpeg,image/png,image/gif,image/webp" multiple><div class="input"><textarea rows="1" aria-label="${copy.placeholder}" placeholder="${copy.placeholder}" ${disabled ? 'disabled' : ''}>${escapeHtml(this._draft)}</textarea><button class="send ${state.turnRunning ? 'stop' : ''}" data-action="${state.turnRunning ? 'stop' : 'send'}" aria-label="${state.turnRunning ? copy.stop : copy.send}" ${!state.turnRunning && disabled ? 'disabled' : ''}>${state.turnRunning ? '■' : sendIcon}</button></div><div class="footer"><button class="tool-button" data-action="attach" title="${copy.attach}" aria-label="${copy.attach}">${imageIcon}</button><button class="pill" data-action="mode-menu" title="Permission mode"><i class="mode-dot" style="background:${this._modeColor(mode)}"></i><strong>${escapeHtml(this._modeLabel(mode))}</strong>${chevron}</button><span class="pill usage" title="${usage.used || 0} / ${usage.total || 0} tokens"><i class="ring" style="--p:${percent}"></i>${usage.hasUsage ? `${percent.toFixed(1)}%` : '—'}</span><button class="tool-button" data-action="commands" title="${copy.commands}" aria-label="${copy.commands}">${commandIcon}${commandCount ? `<span class="count">${commandCount}</span>` : ''}</button></div></footer></section>`;
|
|
286
|
-
const messages = this.shadowRoot.querySelector('.messages'); if (messages) messages.scrollTop = messages.scrollHeight;
|
|
287
|
-
const textarea = this.shadowRoot.querySelector('textarea');
|
|
288
|
-
if (textarea) {
|
|
289
|
-
const size = () => { textarea.style.height = 'auto'; const fullHeight = textarea.scrollHeight; textarea.style.height = `${Math.min(130, fullHeight)}px`; textarea.style.overflowY = fullHeight > 130 ? 'auto' : 'hidden'; };
|
|
290
|
-
textarea.addEventListener('input', () => {
|
|
291
|
-
this._draft = textarea.value; size();
|
|
292
|
-
const shouldOpen = this._draft.startsWith('/') && !/\s/.test(this._draft.slice(1));
|
|
293
|
-
if (shouldOpen !== this._commandMenu || shouldOpen) { this._commandMenu = shouldOpen; this._commandIndex = 0; this.render(); this.focusInput(); }
|
|
294
|
-
}); size();
|
|
295
|
-
textarea.addEventListener('paste', event => {
|
|
296
|
-
const images = Array.from(event.clipboardData && event.clipboardData.files || []).filter(file => file.type.startsWith('image/'));
|
|
297
|
-
if (images.length) { event.preventDefault(); this._addFiles(images); }
|
|
298
|
-
});
|
|
299
|
-
textarea.addEventListener('keydown', event => {
|
|
300
|
-
if (this._commandMenu && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) { event.preventDefault(); const count = this._commands(state).length; if (count) { this._commandIndex = (this._commandIndex + (event.key === 'ArrowDown' ? 1 : -1) + count) % count; this.render(); this.focusInput(); } return; }
|
|
301
|
-
if (event.key === 'Escape' && this._commandMenu) { event.preventDefault(); this._commandMenu = false; this.render(); this.focusInput(); return; }
|
|
302
|
-
if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) { event.preventDefault(); if (this._commandMenu && commands.length) this._selectCommand(commands[this._commandIndex]); else this._send(); }
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
const fileInput = this.shadowRoot.querySelector('.attachment-input');
|
|
306
|
-
if (fileInput) fileInput.addEventListener('change', () => { this._addFiles(fileInput.files); fileInput.value = ''; });
|
|
307
|
-
const composer = this.shadowRoot.querySelector('.composer');
|
|
308
|
-
if (composer) {
|
|
309
|
-
composer.addEventListener('dragenter', event => { if (event.dataTransfer && Array.from(event.dataTransfer.items || []).some(item => item.kind === 'file')) { event.preventDefault(); this._dragDepth += 1; composer.classList.add('dragging'); } });
|
|
310
|
-
composer.addEventListener('dragover', event => { event.preventDefault(); if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy'; });
|
|
311
|
-
composer.addEventListener('dragleave', () => { this._dragDepth = Math.max(0, this._dragDepth - 1); if (!this._dragDepth) composer.classList.remove('dragging'); });
|
|
312
|
-
composer.addEventListener('drop', event => { event.preventDefault(); this._dragDepth = 0; composer.classList.remove('dragging'); this._addFiles(Array.from(event.dataTransfer && event.dataTransfer.files || []).filter(file => file.type.startsWith('image/'))); });
|
|
313
|
-
}
|
|
314
|
-
this.shadowRoot.querySelectorAll('[data-action]').forEach(element => element.addEventListener('click', event => {
|
|
315
|
-
const action = event.currentTarget.dataset.action;
|
|
316
|
-
if (action === 'send') this._send();
|
|
317
|
-
else if (action === 'stop') this._run(() => this._controller.abort());
|
|
318
|
-
else if (action === 'allow') this._run(() => this._controller.resolvePermission(true));
|
|
319
|
-
else if (action === 'deny') this._run(() => this._controller.resolvePermission(false));
|
|
320
|
-
else if (action === 'attach') this._openAttachmentPicker();
|
|
321
|
-
else if (action === 'mode-menu') { this._modeMenu = !this._modeMenu; this._commandMenu = false; this.render(); }
|
|
322
|
-
else if (action === 'commands') { this._commandMenu = !this._commandMenu; this._modeMenu = false; this.render(); }
|
|
323
|
-
}));
|
|
324
|
-
this.shadowRoot.querySelectorAll('[data-mode]').forEach(element => element.addEventListener('click', () => { this._modeMenu = false; this._run(() => this._controller.setPermissionMode(element.dataset.mode)); }));
|
|
325
|
-
this.shadowRoot.querySelectorAll('[data-command-index]').forEach(element => element.addEventListener('click', () => this._selectCommand(commands[Number(element.dataset.commandIndex)])));
|
|
326
|
-
this.shadowRoot.querySelectorAll('[data-remove-attachment]').forEach(element => element.addEventListener('click', () => this._removeAttachment(element.dataset.removeAttachment)));
|
|
327
|
-
}
|
|
328
|
-
async _send() {
|
|
329
|
-
const message = this._draft.trim();
|
|
330
|
-
if ((!message && !this._attachments.length) || !this._controller) return;
|
|
331
|
-
const attachments = this._attachments.map(({ previewUrl, ...item }) => item);
|
|
332
|
-
this._draft = '';
|
|
333
|
-
this._attachments = [];
|
|
334
|
-
this._commandMenu = false;
|
|
335
|
-
this.render();
|
|
336
|
-
try { await this._run(() => this._controller.send(message, { attachments })); }
|
|
337
|
-
catch { this._draft = message; this._attachments = attachments.map(item => ({ ...item, previewUrl: `data:${item.mediaType};base64,${item.data}` })); this.render(); }
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (globalThis.customElements && !customElements.get('claude-workbench-pane')) customElements.define('claude-workbench-pane', ClaudeWorkbenchPane);
|
|
342
|
-
return { ClaudeWorkbenchPane };
|
|
343
|
-
});
|
|
1
|
+
(function (global, factory) {
|
|
2
|
+
let core = global.ClaudeWorkbenchCore;
|
|
3
|
+
if (!core && typeof require === 'function') {
|
|
4
|
+
try { core = require('@claude-ai-workbench/core'); }
|
|
5
|
+
catch { try { core = require('../core'); } catch {} }
|
|
6
|
+
}
|
|
7
|
+
const exported = factory(core);
|
|
8
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = exported;
|
|
9
|
+
global.ClaudeWorkbenchPane = exported.ClaudeWorkbenchPane;
|
|
10
|
+
})(typeof window !== 'undefined' ? window : globalThis, function (Core) {
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const COPY = {
|
|
14
|
+
'zh-CN': {
|
|
15
|
+
title: 'Claude Code', connecting: '正在连接', connected: '已连接', reconnecting: '正在重连', offline: '未连接',
|
|
16
|
+
emptyTitle: '开始与 Claude Code 对话', emptyBody: '消息和工具调用只属于当前项目。', noContext: '请先选择项目',
|
|
17
|
+
placeholder: '给 Claude 发送消息…', send: '发送', stop: '停止', default: '默认模式', acceptEdits: '自动接受编辑',
|
|
18
|
+
plan: '计划模式', bypassPermissions: '跳过权限确认', token: '上下文', permission: '需要你的确认', allow: '允许', deny: '拒绝',
|
|
19
|
+
running: '正在执行', done: '已完成', failed: '失败', thinking: '正在思考', unavailable: '终端组件未连接 Core。', attach: '附加图片', commands: '显示斜杠命令',
|
|
20
|
+
commandTitle: 'Claude Code 命令', commandLoading: '正在读取当前环境支持的命令…', commandEmpty: '没有匹配的命令', removeImage: '移除图片', imageOnly: '图片',
|
|
21
|
+
invalidImage: '仅支持 JPEG、PNG、GIF、WebP 图片。', imageTooLarge: '单张图片不能超过 10 MB。', imageTotalTooLarge: '本次图片总大小不能超过 20 MB。', tooManyImages: '一次最多发送 20 张图片。',
|
|
22
|
+
},
|
|
23
|
+
en: {
|
|
24
|
+
title: 'Claude Code', connecting: 'Connecting', connected: 'Connected', reconnecting: 'Reconnecting', offline: 'Offline',
|
|
25
|
+
emptyTitle: 'Start a Claude Code conversation', emptyBody: 'Messages and tool calls stay with this project.', noContext: 'Select a project first',
|
|
26
|
+
placeholder: 'Message Claude…', send: 'Send', stop: 'Stop', default: 'Default Mode', acceptEdits: 'Accept edits',
|
|
27
|
+
plan: 'Plan mode', bypassPermissions: 'Bypass permissions', token: 'Context', permission: 'Your approval is required', allow: 'Allow', deny: 'Deny',
|
|
28
|
+
running: 'Running', done: 'Done', failed: 'Failed', thinking: 'Thinking', unavailable: 'Terminal Core is not connected.', attach: 'Attach image', commands: 'Show slash commands',
|
|
29
|
+
commandTitle: 'Claude Code commands', commandLoading: 'Loading commands supported by this environment…', commandEmpty: 'No matching commands', removeImage: 'Remove image', imageOnly: 'Image',
|
|
30
|
+
invalidImage: 'Only JPEG, PNG, GIF, and WebP images are supported.', imageTooLarge: 'Each image must be 10 MB or smaller.', imageTotalTooLarge: 'Images in one message must total 20 MB or less.', tooManyImages: 'A message can contain at most 20 images.',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const CSS = `
|
|
35
|
+
:host{display:block;min-width:280px;height:100%;color:var(--caw-text,#172231);font:13px/1.55 Inter,"Segoe UI",system-ui,sans-serif}
|
|
36
|
+
*{box-sizing:border-box}button,textarea,select{font:inherit;color:inherit}button{cursor:pointer}
|
|
37
|
+
.terminal{height:100%;min-height:420px;display:grid;grid-template-rows:auto minmax(0,1fr) auto;background:var(--caw-panel,#fff);border:1px solid var(--caw-line,#d7dfe8);border-radius:12px;overflow:hidden;box-shadow:0 10px 34px rgba(24,34,48,.08)}
|
|
38
|
+
.bar{height:46px;padding:0 13px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid var(--caw-line,#d7dfe8);background:var(--caw-panel,#fff)}
|
|
39
|
+
.identity,.status{display:flex;align-items:center;min-width:0}.identity{gap:9px;font-weight:650}.status{gap:6px;color:var(--caw-muted,#687789);font-size:11px;white-space:nowrap}
|
|
40
|
+
.dot{width:8px;height:8px;border-radius:50%;background:var(--caw-muted,#687789);box-shadow:0 0 0 3px color-mix(in srgb,var(--caw-muted,#687789) 14%,transparent)}
|
|
41
|
+
.dot.connected{background:var(--caw-good,#2f7d64)}.dot.connecting,.dot.reconnecting,.dot.running{background:var(--caw-warn,#9a7428)}.dot.error{background:var(--caw-bad,#af4b4b)}
|
|
42
|
+
.profile{max-width:156px;overflow:hidden;text-overflow:ellipsis;padding:2px 7px;border:1px solid var(--caw-line,#d7dfe8);border-radius:999px;background:var(--caw-panel-2,#f6f8fb);color:var(--caw-muted,#687789)}
|
|
43
|
+
.messages{min-height:0;overflow:auto;padding:15px 14px 22px;background:var(--caw-panel-2,#f6f8fb);scrollbar-width:thin;scrollbar-color:var(--caw-line-strong,#bcc8d5) transparent}
|
|
44
|
+
.empty{height:100%;min-height:210px;display:grid;place-items:center;text-align:center;color:var(--caw-muted,#687789)}.empty strong{display:block;margin-bottom:3px;color:var(--caw-text,#172231);font-size:14px}
|
|
45
|
+
.line{margin:0 0 12px}.line.user{display:flex;justify-content:flex-end}.bubble{max-width:92%;border-radius:10px;padding:8px 10px;overflow-wrap:anywhere}
|
|
46
|
+
.user .bubble{max-width:86%;background:var(--caw-accent,#4f6f8f);color:#fff;border-bottom-right-radius:3px}.assistant .bubble{padding:0 2px;color:var(--caw-text,#172231)}
|
|
47
|
+
.markdown p{margin:0 0 8px}.markdown p:last-child{margin-bottom:0}.markdown h1,.markdown h2,.markdown h3{margin:12px 0 6px;line-height:1.3}.markdown h1{font-size:17px}.markdown h2{font-size:15px}.markdown h3{font-size:14px}.markdown ul{margin:5px 0;padding-left:20px}
|
|
48
|
+
code{font-family:"Cascadia Code",Consolas,monospace;font-size:.92em;background:var(--caw-panel-3,#e8edf3);border-radius:4px;padding:1px 4px}.code{margin:8px 0;padding:9px 10px;overflow:auto;border:1px solid var(--caw-line,#d7dfe8);border-radius:7px;background:var(--caw-panel,#fff);font:12px/1.55 "Cascadia Code",Consolas,monospace;white-space:pre}
|
|
49
|
+
.thinking,.system,.status-line{color:var(--caw-muted,#687789);font-size:12px}.thinking{font-style:italic}.error{padding:8px 10px;border:1px solid color-mix(in srgb,var(--caw-bad,#af4b4b) 28%,transparent);border-radius:8px;background:var(--caw-bad-soft,rgba(175,75,75,.11));color:var(--caw-bad,#af4b4b)}
|
|
50
|
+
details.tool{border:1px solid var(--caw-line,#d7dfe8);border-radius:8px;background:var(--caw-panel,#fff);overflow:hidden}details.tool summary{list-style:none;display:flex;align-items:center;gap:7px;padding:7px 9px;cursor:pointer}details.tool summary::-webkit-details-marker{display:none}.tool-name{font-weight:600}.tool-state{margin-left:auto;color:var(--caw-muted,#687789);font-size:11px}.tool-body{padding:0 9px 9px}.tool-body pre{max-height:220px;margin:5px 0 0;padding:8px;overflow:auto;border-radius:6px;background:var(--caw-panel-2,#f6f8fb);white-space:pre-wrap;font:11px/1.5 "Cascadia Code",Consolas,monospace}
|
|
51
|
+
.permission{padding:10px;border:1px solid color-mix(in srgb,var(--caw-warn,#9a7428) 35%,transparent);border-radius:9px;background:var(--caw-warn-soft,rgba(154,116,40,.12))}.permission-head{display:flex;gap:8px;align-items:baseline}.permission-head strong{font-size:12px}.permission-head span{color:var(--caw-muted,#687789);font:11px "Cascadia Code",Consolas,monospace}.permission-detail{margin-top:5px;color:var(--caw-muted,#687789);font-size:12px;overflow-wrap:anywhere}.permission-actions{display:flex;justify-content:flex-end;gap:7px;margin-top:9px}
|
|
52
|
+
.small{min-width:60px;padding:5px 9px;border:1px solid var(--caw-line-strong,#bcc8d5);border-radius:6px;background:var(--caw-panel,#fff)}.small.primary{border-color:var(--caw-accent,#4f6f8f);background:var(--caw-accent,#4f6f8f);color:#fff}.small:hover{filter:brightness(.97)}
|
|
53
|
+
.composer{position:relative;padding:8px 8px 7px;border-top:1px solid var(--caw-line,#d7dfe8);background:var(--caw-panel-2,#f6f8fb)}.composer.dragging{outline:2px solid var(--caw-accent,#4f6f8f);outline-offset:-3px}.input{display:flex;align-items:flex-end;gap:8px}.input:focus-within textarea{border-color:var(--caw-accent,#4f6f8f);box-shadow:0 0 0 2px var(--caw-accent-soft,rgba(79,111,143,.12))}
|
|
54
|
+
.attachment-input{display:none}.attachment-tray{display:flex;gap:7px;overflow-x:auto;padding:0 0 7px}.attachment{position:relative;flex:0 0 62px;height:52px;border:1px solid var(--caw-line,#d7dfe8);border-radius:7px;overflow:hidden;background:var(--caw-panel,#fff)}.attachment img{width:100%;height:100%;display:block;object-fit:cover}.attachment-remove{position:absolute;right:2px;top:2px;width:18px;height:18px;display:grid;place-items:center;padding:0;border:0;border-radius:50%;background:rgba(20,28,38,.78);color:#fff;font-size:13px;line-height:1}.attachment-name{position:absolute;left:0;right:0;bottom:0;padding:2px 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;background:linear-gradient(transparent,rgba(20,28,38,.8));color:#fff;font-size:8px}.image-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(112px,220px));gap:7px;margin-bottom:7px}.image-card{display:block;overflow:hidden;border:1px solid var(--caw-line,#d7dfe8);border-radius:8px;background:var(--caw-panel,#fff)}.image-card img{display:block;width:100%;max-height:260px;object-fit:contain;background:var(--caw-panel-3,#e8edf3)}.image-card span{display:block;padding:4px 7px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--caw-muted,#687789);font-size:9px}.user .image-grid{justify-content:end}.user .image-card{border-color:color-mix(in srgb,var(--caw-accent,#4f6f8f) 55%,white)}
|
|
55
|
+
textarea{width:100%;min-height:34px;max-height:130px;padding:7px 10px;border:1px solid var(--caw-line-strong,#bcc8d5);border-radius:5px;outline:0;resize:none;overflow-y:hidden;background:var(--caw-panel,#fff);font:12px/1.55 "Cascadia Code",Consolas,monospace;transition:border-color .15s,box-shadow .15s}textarea::placeholder{color:var(--caw-muted,#687789)}textarea:disabled{cursor:not-allowed}
|
|
56
|
+
.send{flex:0 0 34px;width:34px;height:34px;display:grid;place-items:center;border:0;border-radius:7px;background:var(--caw-accent,#4f6f8f);color:#fff;font-weight:700}.send.stop{background:var(--caw-bad,#af4b4b)}.send:disabled{opacity:.45;cursor:not-allowed}
|
|
57
|
+
.footer{min-height:28px;margin-top:6px;display:flex;align-items:center;gap:6px;color:var(--caw-muted,#687789);font-size:11px}.tool-button,.pill{height:28px;display:flex;align-items:center;justify-content:center;border:1px solid transparent;border-radius:5px;background:transparent;color:var(--caw-muted,#687789)}.tool-button{position:relative;width:28px;padding:0}.tool-button:hover{background:var(--caw-panel-3,#e8edf3);color:var(--caw-text,#172231)}.pill{gap:6px;padding:0 8px;border-color:var(--caw-line,#d7dfe8);background:var(--caw-panel,#fff);white-space:nowrap}.pill:hover{border-color:var(--caw-line-strong,#bcc8d5)}.mode-dot{width:8px;height:8px;border-radius:50%}.chevron{width:9px;height:9px}.usage{font-family:"Cascadia Code",Consolas,monospace;font-variant-numeric:tabular-nums}.ring{--p:0;width:15px;height:15px;border-radius:50%;background:conic-gradient(var(--caw-good,#2f7d64) calc(var(--p)*1%),color-mix(in srgb,var(--caw-good,#2f7d64) 22%,transparent) 0);position:relative}.ring:after{content:"";position:absolute;inset:3px;border-radius:50%;background:var(--caw-panel,#fff)}.count{position:absolute;right:-4px;top:-5px;min-width:16px;height:16px;padding:0 4px;display:grid;place-items:center;border-radius:999px;background:var(--caw-accent,#4f6f8f);color:#fff;font-size:9px;font-weight:650}
|
|
58
|
+
.popup{position:absolute;left:8px;bottom:42px;z-index:20;width:min(360px,calc(100% - 16px));max-height:min(330px,55vh);overflow:auto;padding:5px;border:1px solid var(--caw-line,#d7dfe8);border-radius:8px;background:var(--caw-panel,#fff);box-shadow:0 14px 38px rgba(24,34,48,.16)}.popup-title,.popup-empty{padding:5px 7px;color:var(--caw-muted,#687789);font-size:10px}.popup-title{text-transform:uppercase;letter-spacing:.06em}.popup-row{width:100%;display:flex;align-items:flex-start;gap:8px;padding:7px;border:0;border-radius:6px;background:transparent;text-align:left}.popup-row:hover,.popup-row.active{background:var(--caw-panel-2,#f6f8fb)}.popup-row .mode-dot{margin-top:5px;flex:none}.popup-copy{display:grid;min-width:0}.popup-copy strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.popup-copy small{color:var(--caw-muted,#687789);font-size:10px}.command-mark{width:20px;height:20px;display:grid;place-items:center;flex:none;border-radius:5px;background:var(--caw-accent,#4f6f8f);color:#fff;font:10px "Cascadia Code",Consolas,monospace}
|
|
59
|
+
@media(max-width:430px){.terminal{border-radius:0;border-left:0;border-right:0}.profile{max-width:105px}.messages{padding-inline:11px}}
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
const MODES = [
|
|
63
|
+
{ id: 'default', color: '#94a3b8' }, { id: 'acceptEdits', color: '#22c55e' },
|
|
64
|
+
{ id: 'bypassPermissions', color: '#f59e0b' }, { id: 'plan', color: '#a855f7' },
|
|
65
|
+
];
|
|
66
|
+
const IMAGE_TYPES = new Set(['image/jpeg', 'image/png', 'image/gif', 'image/webp']);
|
|
67
|
+
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
68
|
+
const MAX_IMAGE_TOTAL_BYTES = 20 * 1024 * 1024;
|
|
69
|
+
const MAX_IMAGES = 20;
|
|
70
|
+
|
|
71
|
+
function escapeHtml(value) {
|
|
72
|
+
return String(value == null ? '' : value).replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char]);
|
|
73
|
+
}
|
|
74
|
+
function json(value) {
|
|
75
|
+
if (value == null || value === '') return '';
|
|
76
|
+
if (typeof value === 'string') return value;
|
|
77
|
+
try { return JSON.stringify(value, null, 2); } catch { return String(value); }
|
|
78
|
+
}
|
|
79
|
+
function markdown(value) {
|
|
80
|
+
const escaped = escapeHtml(value);
|
|
81
|
+
const blocks = [];
|
|
82
|
+
let body = escaped.replace(/```(?:[\w-]+)?\n([\s\S]*?)```/g, (_, code) => {
|
|
83
|
+
blocks.push(`<pre class="code">${code.replace(/\n$/, '')}</pre>`);
|
|
84
|
+
return `\u0000${blocks.length - 1}\u0000`;
|
|
85
|
+
});
|
|
86
|
+
body = body.replace(/^### (.+)$/gm, '<h3>$1</h3>').replace(/^## (.+)$/gm, '<h2>$1</h2>').replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
|
87
|
+
body = body.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>').replace(/`([^`\n]+)`/g, '<code>$1</code>');
|
|
88
|
+
const sections = body.split(/\n{2,}/).map(part => {
|
|
89
|
+
if (/^\u0000\d+\u0000$/.test(part) || /^<h[1-3]>/.test(part)) return part;
|
|
90
|
+
if (/^(?:- .+(?:\n|$))+/.test(part)) return `<ul>${part.split('\n').map(line => `<li>${line.replace(/^- /, '')}</li>`).join('')}</ul>`;
|
|
91
|
+
return `<p>${part.replace(/\n/g, '<br>')}</p>`;
|
|
92
|
+
}).join('');
|
|
93
|
+
return sections.replace(/\u0000(\d+)\u0000/g, (_, index) => blocks[Number(index)] || '');
|
|
94
|
+
}
|
|
95
|
+
function normalizeContext(input) {
|
|
96
|
+
if (!input) return null;
|
|
97
|
+
return { ...input, contextId: input.contextId || input.projectId || '' };
|
|
98
|
+
}
|
|
99
|
+
function safeImageSource(value, mediaType) {
|
|
100
|
+
const source = String(value || '');
|
|
101
|
+
if (/^\/api\//.test(source) || /^https?:\/\//i.test(source)) return source;
|
|
102
|
+
if (/^data:image\/(?:jpeg|png|gif|webp);base64,/i.test(source)) return source;
|
|
103
|
+
if (source && IMAGE_TYPES.has(mediaType)) return `data:${mediaType};base64,${source}`;
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
class ClaudeWorkbenchPane extends HTMLElement {
|
|
108
|
+
constructor() {
|
|
109
|
+
super();
|
|
110
|
+
this.attachShadow({ mode: 'open' });
|
|
111
|
+
this._client = null;
|
|
112
|
+
this._context = null;
|
|
113
|
+
this._locale = 'zh-CN';
|
|
114
|
+
this._draft = '';
|
|
115
|
+
this._attachments = [];
|
|
116
|
+
this._modeMenu = false;
|
|
117
|
+
this._commandMenu = false;
|
|
118
|
+
this._commandIndex = 0;
|
|
119
|
+
this._dragDepth = 0;
|
|
120
|
+
this._renderQueued = false;
|
|
121
|
+
this._controller = null;
|
|
122
|
+
this._ensureController();
|
|
123
|
+
}
|
|
124
|
+
connectedCallback() {
|
|
125
|
+
this._ensureController();
|
|
126
|
+
this.render();
|
|
127
|
+
if (this._controller && this._client) this._controller.setAdapter(this._client);
|
|
128
|
+
if (this._controller && this._context) this._controller.setContext(this._context);
|
|
129
|
+
}
|
|
130
|
+
disconnectedCallback() { if (!this.hasAttribute('preserve-controller')) this.destroy(); }
|
|
131
|
+
set client(value) { this._client = value || null; this._ensureController(); if (this._controller) this._controller.setAdapter(this._client); }
|
|
132
|
+
get client() { return this._client; }
|
|
133
|
+
set adapter(value) { this.client = value; }
|
|
134
|
+
get adapter() { return this._client; }
|
|
135
|
+
set context(value) {
|
|
136
|
+
this._context = normalizeContext(value);
|
|
137
|
+
this._ensureController();
|
|
138
|
+
if (this._controller && this.isConnected) this._controller.setContext(this._context);
|
|
139
|
+
else this.render();
|
|
140
|
+
}
|
|
141
|
+
get context() { return this._context; }
|
|
142
|
+
set locale(value) { this._locale = COPY[value] ? value : (String(value).toLowerCase().startsWith('en') ? 'en' : 'zh-CN'); this.render(); }
|
|
143
|
+
get locale() { return this._locale; }
|
|
144
|
+
get controller() { return this._controller; }
|
|
145
|
+
_ensureController() {
|
|
146
|
+
if (!this._controller && Core) this._controller = Core.createController({ adapter: this._client, onChange: state => this._onState(state) });
|
|
147
|
+
return this._controller;
|
|
148
|
+
}
|
|
149
|
+
async newSession() { return this._run(() => this._controller.newSession()); }
|
|
150
|
+
async restoreSession(id) { return this._run(() => this._controller.restoreSession(id)); }
|
|
151
|
+
focusInput() { const input = this.shadowRoot.querySelector('textarea'); if (input) { input.focus(); input.setSelectionRange(input.value.length, input.value.length); } }
|
|
152
|
+
destroy() { if (this._controller) this._controller.destroy(); this._controller = null; }
|
|
153
|
+
_copy() { return COPY[this._locale] || COPY['zh-CN']; }
|
|
154
|
+
_onState(state) {
|
|
155
|
+
this.dispatchEvent(new CustomEvent('terminal-state-change', { detail: state }));
|
|
156
|
+
this.dispatchEvent(new CustomEvent('workbench-state-change', { detail: state }));
|
|
157
|
+
this.dispatchEvent(new CustomEvent('running-change', { detail: { running: Boolean(state.turnRunning) } }));
|
|
158
|
+
if (!this._renderQueued) {
|
|
159
|
+
this._renderQueued = true;
|
|
160
|
+
queueMicrotask(() => { this._renderQueued = false; if (this.isConnected) this.render(); });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async _run(action) {
|
|
164
|
+
try { return await action(); }
|
|
165
|
+
catch (error) {
|
|
166
|
+
this.dispatchEvent(new CustomEvent('terminal-error', { detail: error }));
|
|
167
|
+
this.dispatchEvent(new CustomEvent('workbench-error', { detail: error }));
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
_status(state) {
|
|
172
|
+
if (state.error) return ['error', this._copy().failed];
|
|
173
|
+
if (state.turnRunning) return ['running', this._copy().running];
|
|
174
|
+
const connection = state.session.connectionState || 'offline';
|
|
175
|
+
return [connection, this._copy()[connection] || connection];
|
|
176
|
+
}
|
|
177
|
+
_modeLabel(mode) { return this._copy()[mode] || mode; }
|
|
178
|
+
_modeColor(mode) { return (MODES.find(item => item.id === mode) || MODES[0]).color; }
|
|
179
|
+
_appendLocalInfo(text, kind = 'status') {
|
|
180
|
+
if (!this._controller) return;
|
|
181
|
+
this._controller.state.lines.push({ id: `local-${Date.now()}`, at: new Date().toISOString(), kind, text });
|
|
182
|
+
this.render();
|
|
183
|
+
}
|
|
184
|
+
_commands(state) {
|
|
185
|
+
const query = this._draft.startsWith('/') ? this._draft.slice(1).trimStart().toLowerCase() : '';
|
|
186
|
+
const commands = Array.isArray(state && state.commands) ? state.commands : [];
|
|
187
|
+
if (!query || /\s/.test(query)) return /\s/.test(query) ? [] : commands;
|
|
188
|
+
return commands.filter(item => {
|
|
189
|
+
const haystack = [item.name, ...(item.aliases || [])].join(' ').toLowerCase();
|
|
190
|
+
return haystack.includes(query);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
_selectCommand(command) {
|
|
194
|
+
this._commandMenu = false;
|
|
195
|
+
const name = command && command.name || String(command || '').replace(/^\//, '');
|
|
196
|
+
const event = new CustomEvent('terminal-command', { bubbles: true, composed: true, cancelable: true, detail: { command: name, commandInfo: command, context: this._context, state: this._controller && this._controller.state } });
|
|
197
|
+
if (!this.dispatchEvent(event)) { this.render(); return; }
|
|
198
|
+
this._draft = `/${name}${command && command.argumentHint ? ' ' : ''}`;
|
|
199
|
+
this.render();
|
|
200
|
+
this.focusInput();
|
|
201
|
+
}
|
|
202
|
+
_reportAttachmentError(message) {
|
|
203
|
+
const error = new Error(message);
|
|
204
|
+
this.dispatchEvent(new CustomEvent('terminal-error', { detail: error }));
|
|
205
|
+
this.dispatchEvent(new CustomEvent('workbench-error', { detail: error }));
|
|
206
|
+
this._appendLocalInfo(message, 'error');
|
|
207
|
+
}
|
|
208
|
+
async _addFiles(fileList) {
|
|
209
|
+
const files = Array.from(fileList || []).filter(Boolean);
|
|
210
|
+
if (!files.length) return;
|
|
211
|
+
if (this._attachments.length + files.length > MAX_IMAGES) return this._reportAttachmentError(this._copy().tooManyImages);
|
|
212
|
+
let total = this._attachments.reduce((sum, item) => sum + Number(item.size || 0), 0);
|
|
213
|
+
for (const file of files) {
|
|
214
|
+
if (!IMAGE_TYPES.has(file.type)) { this._reportAttachmentError(this._copy().invalidImage); continue; }
|
|
215
|
+
if (file.size > MAX_IMAGE_BYTES) { this._reportAttachmentError(this._copy().imageTooLarge); continue; }
|
|
216
|
+
if (total + file.size > MAX_IMAGE_TOTAL_BYTES) { this._reportAttachmentError(this._copy().imageTotalTooLarge); break; }
|
|
217
|
+
const dataUrl = await new Promise((resolve, reject) => {
|
|
218
|
+
const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || '')); reader.onerror = () => reject(reader.error || new Error('Unable to read image')); reader.readAsDataURL(file);
|
|
219
|
+
});
|
|
220
|
+
const data = dataUrl.slice(dataUrl.indexOf(',') + 1);
|
|
221
|
+
this._attachments.push({ id: `image-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`, name: file.name || this._copy().imageOnly, mediaType: file.type, size: file.size, data, previewUrl: dataUrl });
|
|
222
|
+
total += file.size;
|
|
223
|
+
}
|
|
224
|
+
this.dispatchEvent(new CustomEvent('terminal-attachments-change', { detail: { attachments: this._attachments.map(({ previewUrl, ...item }) => item) } }));
|
|
225
|
+
this.render();
|
|
226
|
+
this.focusInput();
|
|
227
|
+
}
|
|
228
|
+
_removeAttachment(id) {
|
|
229
|
+
this._attachments = this._attachments.filter(item => item.id !== id);
|
|
230
|
+
this.dispatchEvent(new CustomEvent('terminal-attachments-change', { detail: { attachments: this._attachments.map(({ previewUrl, ...item }) => item) } }));
|
|
231
|
+
this.render();
|
|
232
|
+
this.focusInput();
|
|
233
|
+
}
|
|
234
|
+
_openAttachmentPicker() {
|
|
235
|
+
const event = new CustomEvent('terminal-attach-request', { bubbles: true, composed: true, cancelable: true, detail: { context: this._context, addFiles: files => this._addFiles(files) } });
|
|
236
|
+
if (this.dispatchEvent(event)) this.shadowRoot.querySelector('.attachment-input')?.click();
|
|
237
|
+
}
|
|
238
|
+
_line(item, state) {
|
|
239
|
+
const copy = this._copy();
|
|
240
|
+
const imageCards = attachments => `<div class="image-grid">${(attachments || []).map(image => { const source = safeImageSource(image.url || image.data, image.mediaType); return source ? `<a class="image-card" href="${escapeHtml(source)}" target="_blank" rel="noreferrer"><img src="${escapeHtml(source)}" alt="${escapeHtml(image.name || copy.imageOnly)}"><span>${escapeHtml(image.name || copy.imageOnly)}</span></a>` : ''; }).join('')}</div>`;
|
|
241
|
+
if (item.kind === 'user') return `<div class="line user"><div class="bubble">${imageCards(item.attachments)}${item.text ? escapeHtml(item.text) : ''}</div></div>`;
|
|
242
|
+
if (item.kind === 'image') return `<div class="line assistant">${imageCards([item])}</div>`;
|
|
243
|
+
if (item.kind === 'assistant') return item.text ? `<div class="line assistant"><div class="bubble markdown">${markdown(item.text)}</div></div>` : '';
|
|
244
|
+
if (item.kind === 'thinking') return `<div class="line thinking">${escapeHtml(item.text || copy.thinking)}</div>`;
|
|
245
|
+
if (item.kind === 'error') return `<div class="line error">${escapeHtml(item.text)}</div>`;
|
|
246
|
+
if (item.kind === 'system' || item.kind === 'status') return `<div class="line ${item.kind === 'system' ? 'system' : 'status-line'}">${escapeHtml(item.text)}</div>`;
|
|
247
|
+
if (item.kind === 'permission') {
|
|
248
|
+
const active = state.permission && (state.permission.requestId === item.id || state.permission.requestId === item.requestId);
|
|
249
|
+
const detail = item.text || json(item.input);
|
|
250
|
+
return `<div class="line permission"><div class="permission-head"><strong>${escapeHtml(copy.permission)}</strong><span>${escapeHtml(item.name || '')}</span></div>${detail ? `<div class="permission-detail">${escapeHtml(detail)}</div>` : ''}${active ? `<div class="permission-actions"><button class="small" data-action="deny">${copy.deny}</button><button class="small primary" data-action="allow">${copy.allow}</button></div>` : ''}</div>`;
|
|
251
|
+
}
|
|
252
|
+
if (item.kind === 'tool' || item.kind === 'tool-result') {
|
|
253
|
+
const status = item.status === 'error' ? copy.failed : item.status === 'running' ? copy.running : copy.done;
|
|
254
|
+
const input = json(item.input); const result = item.result || item.text || '';
|
|
255
|
+
return `<details class="line tool"><summary><span>›</span><span class="tool-name">${escapeHtml(item.name || item.toolUseId || 'Tool')}</span><span class="tool-state">${escapeHtml(status)}</span></summary><div class="tool-body">${input ? `<pre>${escapeHtml(input)}</pre>` : ''}${result ? `<pre>${escapeHtml(result)}</pre>` : ''}</div></details>`;
|
|
256
|
+
}
|
|
257
|
+
return item.text ? `<div class="line status-line">${escapeHtml(item.text)}</div>` : '';
|
|
258
|
+
}
|
|
259
|
+
render() {
|
|
260
|
+
const copy = this._copy();
|
|
261
|
+
const state = this._controller ? this._controller.state : {
|
|
262
|
+
session: { connectionState: 'offline' }, lines: [], tokenUsage: { used: 0, total: 200000, hasUsage: false },
|
|
263
|
+
permissionMode: 'default', requestPending: false, turnRunning: false, error: copy.unavailable,
|
|
264
|
+
};
|
|
265
|
+
const [statusClass, statusText] = this._status(state);
|
|
266
|
+
const context = this._context || state.context || {};
|
|
267
|
+
const profile = state.session.aiProfileId || context.aiProfileId || '';
|
|
268
|
+
const usage = state.tokenUsage || {};
|
|
269
|
+
const percent = usage.hasUsage && usage.total ? Math.min(100, (usage.used / usage.total) * 100) : 0;
|
|
270
|
+
const mode = state.permissionMode || 'default';
|
|
271
|
+
const commands = this._commands(state);
|
|
272
|
+
if (this._commandIndex >= commands.length) this._commandIndex = 0;
|
|
273
|
+
const imageIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>';
|
|
274
|
+
const commandIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
|
|
275
|
+
const sendIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>';
|
|
276
|
+
const chevron = '<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>';
|
|
277
|
+
const modePopup = this._modeMenu ? `<div class="popup mode-popup"><div class="popup-title">Permission mode</div>${MODES.map(item => `<button class="popup-row ${item.id === mode ? 'active' : ''}" data-mode="${item.id}"><i class="mode-dot" style="background:${item.color}"></i><span class="popup-copy"><strong>${escapeHtml(this._modeLabel(item.id))}</strong></span></button>`).join('')}</div>` : '';
|
|
278
|
+
const commandRows = commands.map((item, index) => `<button class="popup-row ${index === this._commandIndex ? 'active' : ''}" data-command-index="${index}"><i class="command-mark">›_</i><span class="popup-copy"><strong>/${escapeHtml(item.name)}${item.argumentHint ? ` ${escapeHtml(item.argumentHint)}` : ''}</strong>${item.description ? `<small>${escapeHtml(item.description)}</small>` : ''}</span></button>`).join('');
|
|
279
|
+
const commandEmpty = state.commandError ? escapeHtml(state.commandError) : (Array.isArray(state.commands) && state.commands.length ? copy.commandEmpty : copy.commandLoading);
|
|
280
|
+
const commandPopup = this._commandMenu ? `<div class="popup command-popup"><div class="popup-title">${escapeHtml(copy.commandTitle)}</div>${commandRows || `<div class="popup-empty">${commandEmpty}</div>`}</div>` : '';
|
|
281
|
+
const disabled = !context.contextId || state.requestPending || Boolean(state.permission);
|
|
282
|
+
const messageHtml = state.lines.length ? state.lines.map(line => this._line(line, state)).join('') : `<div class="empty"><div><strong>${escapeHtml(context.contextId ? copy.emptyTitle : copy.noContext)}</strong>${context.contextId ? `<span>${escapeHtml(copy.emptyBody)}</span>` : ''}</div></div>`;
|
|
283
|
+
const attachmentTray = this._attachments.length ? `<div class="attachment-tray">${this._attachments.map(item => `<div class="attachment" title="${escapeHtml(item.name)}"><img src="${escapeHtml(item.previewUrl)}" alt="${escapeHtml(item.name)}"><span class="attachment-name">${escapeHtml(item.name)}</span><button class="attachment-remove" data-remove-attachment="${escapeHtml(item.id)}" title="${escapeHtml(copy.removeImage)}" aria-label="${escapeHtml(copy.removeImage)}">×</button></div>`).join('')}</div>` : '';
|
|
284
|
+
const commandCount = Array.isArray(state.commands) ? state.commands.length : 0;
|
|
285
|
+
this.shadowRoot.innerHTML = `<style>${CSS}</style><section class="terminal" aria-label="Claude Code terminal"><header class="bar"><div class="identity"><i class="dot ${statusClass}"></i><span>${copy.title}</span></div><div class="status"><span>${escapeHtml(statusText)}</span>${profile ? `<span class="profile" title="${escapeHtml(profile)}">${escapeHtml(profile)}</span>` : ''}</div></header><main class="messages" aria-live="polite">${messageHtml}</main><footer class="composer">${modePopup}${commandPopup}${attachmentTray}<input class="attachment-input" type="file" accept="image/jpeg,image/png,image/gif,image/webp" multiple><div class="input"><textarea rows="1" aria-label="${copy.placeholder}" placeholder="${copy.placeholder}" ${disabled ? 'disabled' : ''}>${escapeHtml(this._draft)}</textarea><button class="send ${state.turnRunning ? 'stop' : ''}" data-action="${state.turnRunning ? 'stop' : 'send'}" aria-label="${state.turnRunning ? copy.stop : copy.send}" ${!state.turnRunning && disabled ? 'disabled' : ''}>${state.turnRunning ? '■' : sendIcon}</button></div><div class="footer"><button class="tool-button" data-action="attach" title="${copy.attach}" aria-label="${copy.attach}">${imageIcon}</button><button class="pill" data-action="mode-menu" title="Permission mode"><i class="mode-dot" style="background:${this._modeColor(mode)}"></i><strong>${escapeHtml(this._modeLabel(mode))}</strong>${chevron}</button><span class="pill usage" title="${usage.used || 0} / ${usage.total || 0} tokens"><i class="ring" style="--p:${percent}"></i>${usage.hasUsage ? `${percent.toFixed(1)}%` : '—'}</span><button class="tool-button" data-action="commands" title="${copy.commands}" aria-label="${copy.commands}">${commandIcon}${commandCount ? `<span class="count">${commandCount}</span>` : ''}</button></div></footer></section>`;
|
|
286
|
+
const messages = this.shadowRoot.querySelector('.messages'); if (messages) messages.scrollTop = messages.scrollHeight;
|
|
287
|
+
const textarea = this.shadowRoot.querySelector('textarea');
|
|
288
|
+
if (textarea) {
|
|
289
|
+
const size = () => { textarea.style.height = 'auto'; const fullHeight = textarea.scrollHeight; textarea.style.height = `${Math.min(130, fullHeight)}px`; textarea.style.overflowY = fullHeight > 130 ? 'auto' : 'hidden'; };
|
|
290
|
+
textarea.addEventListener('input', () => {
|
|
291
|
+
this._draft = textarea.value; size();
|
|
292
|
+
const shouldOpen = this._draft.startsWith('/') && !/\s/.test(this._draft.slice(1));
|
|
293
|
+
if (shouldOpen !== this._commandMenu || shouldOpen) { this._commandMenu = shouldOpen; this._commandIndex = 0; this.render(); this.focusInput(); }
|
|
294
|
+
}); size();
|
|
295
|
+
textarea.addEventListener('paste', event => {
|
|
296
|
+
const images = Array.from(event.clipboardData && event.clipboardData.files || []).filter(file => file.type.startsWith('image/'));
|
|
297
|
+
if (images.length) { event.preventDefault(); this._addFiles(images); }
|
|
298
|
+
});
|
|
299
|
+
textarea.addEventListener('keydown', event => {
|
|
300
|
+
if (this._commandMenu && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) { event.preventDefault(); const count = this._commands(state).length; if (count) { this._commandIndex = (this._commandIndex + (event.key === 'ArrowDown' ? 1 : -1) + count) % count; this.render(); this.focusInput(); } return; }
|
|
301
|
+
if (event.key === 'Escape' && this._commandMenu) { event.preventDefault(); this._commandMenu = false; this.render(); this.focusInput(); return; }
|
|
302
|
+
if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) { event.preventDefault(); if (this._commandMenu && commands.length) this._selectCommand(commands[this._commandIndex]); else this._send(); }
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
const fileInput = this.shadowRoot.querySelector('.attachment-input');
|
|
306
|
+
if (fileInput) fileInput.addEventListener('change', () => { this._addFiles(fileInput.files); fileInput.value = ''; });
|
|
307
|
+
const composer = this.shadowRoot.querySelector('.composer');
|
|
308
|
+
if (composer) {
|
|
309
|
+
composer.addEventListener('dragenter', event => { if (event.dataTransfer && Array.from(event.dataTransfer.items || []).some(item => item.kind === 'file')) { event.preventDefault(); this._dragDepth += 1; composer.classList.add('dragging'); } });
|
|
310
|
+
composer.addEventListener('dragover', event => { event.preventDefault(); if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy'; });
|
|
311
|
+
composer.addEventListener('dragleave', () => { this._dragDepth = Math.max(0, this._dragDepth - 1); if (!this._dragDepth) composer.classList.remove('dragging'); });
|
|
312
|
+
composer.addEventListener('drop', event => { event.preventDefault(); this._dragDepth = 0; composer.classList.remove('dragging'); this._addFiles(Array.from(event.dataTransfer && event.dataTransfer.files || []).filter(file => file.type.startsWith('image/'))); });
|
|
313
|
+
}
|
|
314
|
+
this.shadowRoot.querySelectorAll('[data-action]').forEach(element => element.addEventListener('click', event => {
|
|
315
|
+
const action = event.currentTarget.dataset.action;
|
|
316
|
+
if (action === 'send') this._send();
|
|
317
|
+
else if (action === 'stop') this._run(() => this._controller.abort());
|
|
318
|
+
else if (action === 'allow') this._run(() => this._controller.resolvePermission(true));
|
|
319
|
+
else if (action === 'deny') this._run(() => this._controller.resolvePermission(false));
|
|
320
|
+
else if (action === 'attach') this._openAttachmentPicker();
|
|
321
|
+
else if (action === 'mode-menu') { this._modeMenu = !this._modeMenu; this._commandMenu = false; this.render(); }
|
|
322
|
+
else if (action === 'commands') { this._commandMenu = !this._commandMenu; this._modeMenu = false; this.render(); }
|
|
323
|
+
}));
|
|
324
|
+
this.shadowRoot.querySelectorAll('[data-mode]').forEach(element => element.addEventListener('click', () => { this._modeMenu = false; this._run(() => this._controller.setPermissionMode(element.dataset.mode)); }));
|
|
325
|
+
this.shadowRoot.querySelectorAll('[data-command-index]').forEach(element => element.addEventListener('click', () => this._selectCommand(commands[Number(element.dataset.commandIndex)])));
|
|
326
|
+
this.shadowRoot.querySelectorAll('[data-remove-attachment]').forEach(element => element.addEventListener('click', () => this._removeAttachment(element.dataset.removeAttachment)));
|
|
327
|
+
}
|
|
328
|
+
async _send() {
|
|
329
|
+
const message = this._draft.trim();
|
|
330
|
+
if ((!message && !this._attachments.length) || !this._controller) return;
|
|
331
|
+
const attachments = this._attachments.map(({ previewUrl, ...item }) => item);
|
|
332
|
+
this._draft = '';
|
|
333
|
+
this._attachments = [];
|
|
334
|
+
this._commandMenu = false;
|
|
335
|
+
this.render();
|
|
336
|
+
try { await this._run(() => this._controller.send(message, { attachments })); }
|
|
337
|
+
catch { this._draft = message; this._attachments = attachments.map(item => ({ ...item, previewUrl: `data:${item.mediaType};base64,${item.data}` })); this.render(); }
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (globalThis.customElements && !customElements.get('claude-workbench-pane')) customElements.define('claude-workbench-pane', ClaudeWorkbenchPane);
|
|
342
|
+
return { ClaudeWorkbenchPane };
|
|
343
|
+
});
|