dsh-fps-workbench 0.0.0-stage → 0.1.77

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/client.js ADDED
@@ -0,0 +1,4450 @@
1
+ window.__ModuleLoader__.load({
2
+ id: 'dsh-fps-workbench',
3
+ factory(require) {
4
+ try {
5
+ const React = require('react');
6
+ const ReactDOM = require('react-dom');
7
+ const h = React.createElement;
8
+ const NS = 'claudeWorkbench';
9
+ const CONTEXT_TAB = '@local/claude-code-workbench/context';
10
+ const RIGHT_SIDEBAR_MIN_WIDTH = 300;
11
+ const NATIVE_INPUT_ACTIVITY = 'conversation.input.activity';
12
+ const WORKBENCH_VOICE_SLOT = 'workbench.composer.voice';
13
+ const BRAND_VIEW_BOX = '8.5 8.5 47 47';
14
+ const BRAND_RAY = 'M31.1 6Q32 4.3 32.9 6L39.1 18.1Q40.5 21 37.7 23L32 26.3 26.3 23Q23.5 21 24.9 18.1Z';
15
+ const BRAND_RAYS = [135, 225, 315, 45].map((angle) => ({
16
+ d: BRAND_RAY,
17
+ transform: `rotate(${angle} 32 32)`,
18
+ }));
19
+ const BRAND_COLORS = {
20
+ light: ['#bb6349', '#9f452e'],
21
+ dark: ['#f5b59a', '#e98e70'],
22
+ };
23
+ const THEME_TOKENS = {
24
+ '--dsw-alias-bg-base': { light: '#ffffff', dark: '#1f1f1f' },
25
+ '--dsw-alias-bg-layer-1': { light: '#ffffff', dark: '#232323' },
26
+ '--dsw-alias-bg-layer-2': { light: '#f4f4f4', dark: '#2a2a2a' },
27
+ '--dsw-alias-bg-overlay': { light: '#ffffff', dark: '#242424' },
28
+ '--dsw-alias-markdown-code-block': { light: '#f6f6f6', dark: '#1b1b1c' },
29
+ '--dsw-alias-border-l1': { light: '#e5e5e5', dark: 'rgba(255, 255, 255, 0.08)' },
30
+ '--dsw-alias-border-l2': { light: '#dddddd', dark: 'rgba(255, 255, 255, 0.14)' },
31
+ '--dsw-alias-brand-primary': { light: '#a4482e', dark: '#d97757' },
32
+ '--dsw-alias-label-primary': { light: '#191919', dark: '#f0eee9' },
33
+ '--dsw-alias-label-secondary': { light: '#5e5e5e', dark: '#b8b5b0' },
34
+ '--dsw-alias-label-tertiary': { light: '#666666', dark: '#adb2b8' },
35
+ '--dsw-alias-label-caption': { light: '#6e6e6e', dark: '#81858c' },
36
+ '--dsw-alias-state-error-primary': { light: '#ac463e', dark: '#e68575' },
37
+ '--dsw-alias-state-idle-primary': { light: '#6e6e6e', dark: '#8c8983' },
38
+ '--dsw-alias-state-success-primary': { light: '#3c704d', dark: '#86b38e' },
39
+ '--dsw-alias-state-warn-primary': { light: '#8b5b15', dark: '#d6a35c' },
40
+ '--dsw-specific-bubble': { light: '#f3f3f3', dark: '#2c2c2e' },
41
+ '--dsw-specific-sidebar-fill': { light: '#ececec', dark: '#1b1b1b' },
42
+ };
43
+ const ignoreVoiceActivity = () => {};
44
+ const runtime = {
45
+ ctx: undefined, workspace: undefined, sessions: undefined,
46
+ permissionPresets: undefined, shortcuts: undefined, workspaces: undefined, fileUploads: undefined,
47
+ commandsRemote: undefined, modelDirectories: undefined, t: undefined,
48
+ fileReferences: undefined, sessionReferences: undefined, composerSettings: undefined,
49
+ contextAutoOpened: false,
50
+ };
51
+
52
+ function reportDiagnostic(message) {
53
+ console.error(`[claude-workbench] ${message}`);
54
+ }
55
+
56
+ const en = {
57
+ greetingMorning: 'Good morning',
58
+ greetingAfternoon: 'Good afternoon',
59
+ greetingEvening: 'Good evening',
60
+ greetingWithName: '{greeting}, {name}',
61
+ conversations: 'Conversations',
62
+ sessionHistory: 'Conversation history',
63
+ sessionBack: 'Go back to the previous conversation',
64
+ sessionForward: 'Go forward to the next conversation',
65
+ newChat: 'New chat',
66
+ searchChats: 'Search conversations',
67
+ expandSearch: 'Expand the sidebar to search',
68
+ noChats: 'No conversations yet',
69
+ noResults: 'No matching conversations',
70
+ untitled: 'Untitled',
71
+ you: 'You',
72
+ assistant: 'Assistant',
73
+ reasoning: 'Reasoning',
74
+ attachment: 'Attachment',
75
+ image: 'Image',
76
+ inputPlaceholder: 'Write a message…',
77
+ inputDisabled: 'Select a conversation to start writing',
78
+ send: 'Send',
79
+ queue: 'Queue message',
80
+ stop: 'Stop',
81
+ sending: 'Sending…',
82
+ chooseWorkspace: 'Choose a workspace',
83
+ sendFailed: 'Message was not sent. Your draft has been kept.',
84
+ stopFailed: 'Could not stop this turn.',
85
+ sessionNotReady: 'Session is not ready',
86
+ noText: 'No text content',
87
+ otherContent: 'Unsupported content block',
88
+ stopped: 'Stopped',
89
+ attachImage: 'Attach image',
90
+ removeImage: 'Remove image',
91
+ imageTooLarge: 'Each image must be 8 MB or smaller.',
92
+ imageReadFailed: 'The image could not be read.',
93
+ imageRecognizing: 'Reading the images…',
94
+ imageRecognitionFailed: 'Could not read the images.',
95
+ imageRecognitionContext: 'Image observations for reference only',
96
+ imageOnlyRequest: 'Please describe the attached images.',
97
+ attachFiles: 'Attach files',
98
+ removeFile: 'Remove file',
99
+ fileTooLarge: 'Each file must be 20 MB or smaller.',
100
+ fileTooMany: 'At most 6 files can be attached.',
101
+ fileReadFailed: 'A file could not be read.',
102
+ fileUploadFailed: 'A file could not be uploaded.',
103
+ composerHint: 'Enter to send · Shift+Enter newline · / commands · @ references',
104
+ busySendHint: 'Primary modifier + Enter uses the other delivery mode',
105
+ selectModel: 'Select model',
106
+ modelLoading: 'Loading models…',
107
+ modelSearch: 'Search models',
108
+ modelMore: 'More models',
109
+ modelBack: 'Back',
110
+ noModels: 'No models available',
111
+ modelLoadFailed: 'Could not load models',
112
+ modelSelectFailed: 'Could not switch model',
113
+ effortLabel: 'Reasoning effort',
114
+ effortDefault: 'Default',
115
+ effortSelectFailed: 'Could not switch reasoning effort',
116
+ commandNotFound: 'This command is not available in the custom composer.',
117
+ commandLoading: 'Loading commands…',
118
+ commandNoMatch: 'No matching commands',
119
+ commandLoadFailed: 'Could not load commands',
120
+ commandsUnavailable: 'Command catalog is unavailable',
121
+ contextTitle: 'Context',
122
+ contextEnvironment: 'Environment',
123
+ contextModel: 'Model',
124
+ contextProvider: 'Provider',
125
+ contextUsage: 'Context usage',
126
+ contextTokens: 'Tokens',
127
+ contextTurns: 'Turns',
128
+ contextSteps: 'Steps',
129
+ contextSystemPrompt: 'System prompt',
130
+ contextTools: 'Tools',
131
+ contextMessages: 'Messages',
132
+ contextFree: 'Free space',
133
+ contextUsed: 'Used',
134
+ contextEstimate: 'Context composition is estimated.',
135
+ contextEmpty: 'Context data appears after the first model request.',
136
+ usageTitle: 'Usage',
137
+ usageDetails: 'View context',
138
+ usageBalance: 'API balance',
139
+ usageDreaminaName: 'Dreamina',
140
+ usageDreaminaBalance: 'Credit balance',
141
+ usageKeySpent: 'Key spend',
142
+ usageKeyRemaining: 'Key spending limit remaining',
143
+ usageDreaminaReset: 'Estimated subscription credit reset',
144
+ usageExtraBalance: 'Extra usage balance',
145
+ usageFiveHours: '5-hour limit',
146
+ usageSevenDays: '7-day limit',
147
+ usageMonthTotal: 'Monthly shared quota',
148
+ usageCreditsRemaining: '{amount} credits left',
149
+ usageReset: 'Resets',
150
+ usageTotal: 'Total tokens',
151
+ usageInput: 'Uncached input',
152
+ usageOutput: 'Output',
153
+ usageCacheRead: 'Cache read',
154
+ usageCacheWrite: 'Cache write',
155
+ usageEmpty: 'Usage appears when the provider reports tokens.',
156
+ contextSession: 'Session',
157
+ contextWorkspace: 'Workspace',
158
+ contextStatus: 'Status',
159
+ contextRunning: 'Running',
160
+ contextIdle: 'Idle',
161
+ contextQueued: 'Queued',
162
+ contextSessionId: 'Session ID',
163
+ contextOutputs: 'Outputs',
164
+ contextOutputsLoading: 'Checking output files…',
165
+ contextOutputsEmpty: 'No files produced in this session yet.',
166
+ contextOutputOpenFailed: 'Could not open the file preview.',
167
+ contextOpen: 'Open session context',
168
+ headerFiles: 'Files',
169
+ headerTerminal: 'Terminal',
170
+ headerBrowser: 'Browser',
171
+ addMenuTitle: 'Add',
172
+ addImage: 'Image',
173
+ addFiles: 'Files',
174
+ addCommands: 'Commands',
175
+ addContext: 'Session context',
176
+ addPlugins: 'Plugins',
177
+ workspaceLabel: 'Workspace',
178
+ workspaceChange: 'Choose folder…',
179
+ workspaceOpenFailed: 'Could not open that folder',
180
+ mentionTitle: 'Files and sessions',
181
+ mentionLoading: 'Looking up references…',
182
+ mentionNoMatch: 'No matching references',
183
+ mentionUnavailable: 'References are unavailable',
184
+ steer: 'Send to the current turn',
185
+ archivedChats: 'Archived sessions',
186
+ actionUnarchive: 'Restore session',
187
+ collapseSidebar: 'Collapse sidebar',
188
+ expandSidebar: 'Expand sidebar',
189
+ moreMenu: 'More',
190
+ menuPlugins: 'Plugins',
191
+ menuAutomations: 'Automation tasks',
192
+ menuFiles: 'Workspace files',
193
+ menuSettings: 'Settings',
194
+ localWorkspace: 'Local workspace',
195
+ sessionActions: 'Session actions',
196
+ actionRename: 'Rename',
197
+ actionFork: 'Fork',
198
+ actionPin: 'Pin',
199
+ actionUnpin: 'Unpin',
200
+ actionArchive: 'Archive',
201
+ renameTitle: 'Rename session',
202
+ renameSave: 'Save',
203
+ archiveConfirm: 'Archive this session?',
204
+ sessionActionFailed: 'Session action failed',
205
+ toolPreparing: 'Preparing…',
206
+ toolRunning: 'Running',
207
+ toolDone: 'Done',
208
+ toolFailed: 'Failed',
209
+ toolDetails: 'View details',
210
+ workReadOne: 'Read 1 file',
211
+ workReadMany: 'Read {count} files',
212
+ workCommandOne: 'Ran 1 command',
213
+ workCommandMany: 'Ran {count} commands',
214
+ workChanged: 'Changed {files}',
215
+ workFailed: '{count} failed',
216
+ toolApplyPatch: 'Edit files',
217
+ toolBash: 'Run command',
218
+ toolRead: 'Read file',
219
+ toolWrite: 'Write file',
220
+ toolGrep: 'Search',
221
+ toolGlob: 'Find files',
222
+ toolWebSearch: 'Search the web',
223
+ toolWebFetch: 'Read page',
224
+ permissionTitle: 'Permissions',
225
+ permissionReadOnly: 'Read only',
226
+ permissionWorkspaceWrite: 'Workspace write',
227
+ permissionFullAccess: 'Full access',
228
+ permissionAuto: 'Auto review',
229
+ permissionCustom: 'Custom',
230
+ permissionLoadFailed: 'Could not load permission presets',
231
+ permissionSwitchFailed: 'Could not switch permission mode',
232
+ permissionConfirmFull: 'Enable full access? This reduces approval safeguards for this session.',
233
+ permissionConfirmAuto: 'Enable Auto review? A separate reviewer will decide some tool approvals.',
234
+ permissionCancel: 'Cancel',
235
+ permissionEnable: 'Enable',
236
+ };
237
+ const zh = {
238
+ greetingMorning: '早上好',
239
+ greetingAfternoon: '下午好',
240
+ greetingEvening: '晚上好',
241
+ greetingWithName: '{greeting},{name}',
242
+ conversations: '会话',
243
+ sessionHistory: '会话访问历史',
244
+ sessionBack: '后退到上一个会话',
245
+ sessionForward: '前进到下一个会话',
246
+ newChat: '新会话',
247
+ searchChats: '搜索会话',
248
+ expandSearch: '展开侧栏以搜索',
249
+ noChats: '还没有会话',
250
+ noResults: '没有匹配的会话',
251
+ untitled: '未命名',
252
+ you: '你',
253
+ assistant: '助手',
254
+ reasoning: '思考过程',
255
+ attachment: '附件',
256
+ image: '图片',
257
+ inputPlaceholder: '输入消息…',
258
+ inputDisabled: '选择一个会话后开始输入',
259
+ send: '发送',
260
+ queue: '排队发送',
261
+ stop: '停止',
262
+ sending: '发送中…',
263
+ chooseWorkspace: '选择工作区',
264
+ sendFailed: '消息未发送,草稿已保留。',
265
+ stopFailed: '无法停止当前回合。',
266
+ sessionNotReady: '会话尚未就绪',
267
+ noText: '没有文本内容',
268
+ otherContent: '不支持的内容块',
269
+ stopped: '已停止',
270
+ attachImage: '添加图片',
271
+ removeImage: '移除图片',
272
+ imageTooLarge: '每张图片不能超过 8 MB。',
273
+ imageReadFailed: '无法读取这张图片。',
274
+ imageRecognizing: '正在识图…',
275
+ imageRecognitionFailed: '未能识别图片。',
276
+ imageRecognitionContext: '图片观察结果,仅供参考',
277
+ imageOnlyRequest: '请描述附件中的图片。',
278
+ attachFiles: '添加文件',
279
+ removeFile: '移除文件',
280
+ fileTooLarge: '每个文件不能超过 20 MB。',
281
+ fileTooMany: '最多附加 6 个文件。',
282
+ fileReadFailed: '文件读取失败。',
283
+ fileUploadFailed: '文件上传失败。',
284
+ composerHint: 'Enter 发送 · Shift+Enter 换行 · / 命令 · @ 引用',
285
+ busySendHint: '⌘/Ctrl + Enter 使用另一种发送方式',
286
+ selectModel: '选择模型',
287
+ modelLoading: '正在加载模型…',
288
+ modelSearch: '搜索模型',
289
+ modelMore: '更多模型',
290
+ modelBack: '返回',
291
+ noModels: '没有可用模型',
292
+ modelLoadFailed: '无法加载模型列表',
293
+ modelSelectFailed: '无法切换模型',
294
+ effortLabel: '思考强度',
295
+ effortDefault: '默认',
296
+ effortSelectFailed: '无法切换思考强度',
297
+ commandNotFound: '当前输入区无法执行这个命令。',
298
+ commandLoading: '正在加载命令…',
299
+ commandNoMatch: '没有匹配的命令',
300
+ commandLoadFailed: '无法加载命令目录',
301
+ commandsUnavailable: '命令目录不可用',
302
+ contextTitle: '上下文',
303
+ contextEnvironment: '环境',
304
+ contextModel: '模型',
305
+ contextProvider: '供应商',
306
+ contextUsage: '上下文占用',
307
+ contextTokens: 'Token',
308
+ contextTurns: '回合',
309
+ contextSteps: '步骤',
310
+ contextSystemPrompt: '系统提示',
311
+ contextTools: '工具',
312
+ contextMessages: '消息',
313
+ contextFree: '可用空间',
314
+ contextUsed: '已使用',
315
+ contextEstimate: '上下文构成为估算值。',
316
+ contextEmpty: '首次请求模型后显示上下文数据。',
317
+ usageTitle: '用量',
318
+ usageDetails: '查看上下文',
319
+ usageBalance: 'API 余额',
320
+ usageDreaminaName: '即梦',
321
+ usageDreaminaBalance: '积分余额',
322
+ usageKeySpent: 'Key 已用金额',
323
+ usageKeyRemaining: 'Key 剩余支出额度',
324
+ usageDreaminaReset: '订阅积分重置(推算)',
325
+ usageExtraBalance: '加量余额',
326
+ usageFiveHours: '5 小时限额',
327
+ usageSevenDays: '7 天限额',
328
+ usageMonthTotal: '每月共享额度',
329
+ usageCreditsRemaining: '剩余 {amount} 积分',
330
+ usageReset: '重置',
331
+ usageTotal: '累计 Token',
332
+ usageInput: '未缓存输入',
333
+ usageOutput: '输出',
334
+ usageCacheRead: '缓存读取',
335
+ usageCacheWrite: '缓存写入',
336
+ usageEmpty: '供应商回报 Token 后显示用量。',
337
+ contextSession: '会话',
338
+ contextWorkspace: '工作区',
339
+ contextStatus: '状态',
340
+ contextRunning: '运行中',
341
+ contextIdle: '空闲',
342
+ contextQueued: '排队中',
343
+ contextSessionId: '会话 ID',
344
+ contextOutputs: '输出内容',
345
+ contextOutputsLoading: '正在检查输出文件…',
346
+ contextOutputsEmpty: '当前会话暂无输出文件。',
347
+ contextOutputOpenFailed: '无法打开文件预览。',
348
+ contextOpen: '打开会话上下文',
349
+ headerFiles: '文件',
350
+ headerTerminal: '终端',
351
+ headerBrowser: '浏览器',
352
+ addMenuTitle: '添加',
353
+ addImage: '图片',
354
+ addFiles: '文件',
355
+ addCommands: '命令',
356
+ addContext: '会话上下文',
357
+ addPlugins: '插件',
358
+ workspaceLabel: '工作区',
359
+ workspaceChange: '选择文件夹…',
360
+ workspaceOpenFailed: '无法打开该文件夹',
361
+ mentionTitle: '文件与会话',
362
+ mentionLoading: '正在查找引用…',
363
+ mentionNoMatch: '没有匹配的引用',
364
+ mentionUnavailable: '引用暂不可用',
365
+ steer: '追加到当前轮',
366
+ archivedChats: '已归档会话',
367
+ actionUnarchive: '恢复会话',
368
+ collapseSidebar: '收起侧栏',
369
+ expandSidebar: '展开侧栏',
370
+ moreMenu: '更多',
371
+ menuPlugins: '插件',
372
+ menuAutomations: '自动化任务',
373
+ menuFiles: '工作区文件',
374
+ menuSettings: '设置',
375
+ localWorkspace: '本地工作区',
376
+ sessionActions: '会话操作',
377
+ actionRename: '重命名',
378
+ actionFork: '分叉',
379
+ actionPin: '置顶',
380
+ actionUnpin: '取消置顶',
381
+ actionArchive: '归档',
382
+ renameTitle: '重命名会话',
383
+ renameSave: '保存',
384
+ archiveConfirm: '归档这个会话?',
385
+ sessionActionFailed: '会话操作失败',
386
+ toolPreparing: '准备中…',
387
+ toolRunning: '运行中',
388
+ toolDone: '已完成',
389
+ toolFailed: '失败',
390
+ toolDetails: '查看详情',
391
+ workReadOne: '读取了 1 个文件',
392
+ workReadMany: '读取了 {count} 个文件',
393
+ workCommandOne: '运行了 1 条命令',
394
+ workCommandMany: '运行了 {count} 条命令',
395
+ workChanged: '改动了 {files}',
396
+ workFailed: '{count} 次失败',
397
+ toolApplyPatch: '编辑文件',
398
+ toolBash: '运行命令',
399
+ toolRead: '读取文件',
400
+ toolWrite: '写入文件',
401
+ toolGrep: '搜索',
402
+ toolGlob: '查找文件',
403
+ toolWebSearch: '搜索网页',
404
+ toolWebFetch: '读取网页',
405
+ permissionTitle: '权限',
406
+ permissionReadOnly: '仅可查看',
407
+ permissionWorkspaceWrite: '工作区内修改',
408
+ permissionFullAccess: '完全权限',
409
+ permissionAuto: '自动审查',
410
+ permissionCustom: '自定义',
411
+ permissionLoadFailed: '无法加载权限预设',
412
+ permissionSwitchFailed: '无法切换权限模式',
413
+ permissionConfirmFull: '启用完全权限?本会话的审批保护会减少。',
414
+ permissionConfirmAuto: '启用自动审查?部分工具审批将由独立审查器决定。',
415
+ permissionCancel: '取消',
416
+ permissionEnable: '启用',
417
+ };
418
+
419
+ Object.assign(en, {
420
+ imageSettings: 'Image generation',
421
+ imageSettingsIntro: 'Optional text-to-image tools for this DSH installation. Cloud providers receive the image prompt. Turn on the ones you want to use.',
422
+ imageSettingsLoad: 'Loading image providers…', imageSettingsError: 'Could not update image settings.',
423
+ imageSettingsReadError: 'Could not load image settings. If you just updated FPS, restart DSH.',
424
+ imageSettingsUnavailable: 'Image tools are not available on this DSH host.',
425
+ imageZ: 'No key required. Good for quick concepts and illustration. Shared Hugging Face capacity (typically 2 minutes of GPU time per day for anonymous users); waits and availability vary.',
426
+ imageCog: 'Requires a Zhipu API key with access to the free CogView-3-Flash model. Best for drafts and Chinese prompts. Rate limits depend on the account.',
427
+ imagePoll: 'Requires a Pollinations key and positive Pollen balance. FPS only uses a verified zero-price community model (SDXL Lightning). Good for quick drafts; the service may log prompts and images.',
428
+ imageComfy: 'Runs on this computer through official Comfy MCP. The bundled starter workflow makes 512-class SD 1.5 images for drafts. A modern GPU is recommended; CPU generation can take several minutes.',
429
+ imageComfyRequirements: 'Apple Silicon macOS 14+, Windows x64 or Linux x64 (glibc 2.28+). Requires Python 3.11–3.13 with pip and venv, Git, 8 GB RAM, 16 GiB free disk, and internet for the initial download.',
430
+ imageKey: 'API key', imageSaveKey: 'Save key', imageRemoveKey: 'Remove saved key', imageKeyConfigured: 'Key configured',
431
+ imageKeyMissing: 'Key required', imageKeyUnavailable: 'Configure the key on the DSH host.',
432
+ imageInstall: 'Download and install', imageCancel: 'Cancel download', imageRetry: 'Retry installation',
433
+ imageReady: 'Ready to generate', imageMissing: 'Setup required', imageUnsupported: 'Automatic setup is unavailable on this computer.',
434
+ imageInstallError: 'Setup did not finish.', imageInstalling: 'Installing…',
435
+ imageStage_checking: 'Checking requirements…', imageStage_environment: 'Preparing Python…',
436
+ imageStage_comfyui: 'Downloading ComfyUI…', imageStage_dependencies: 'Installing ComfyUI and official Comfy MCP…',
437
+ imageStage_model: 'Downloading the image model…', imageStage_verifying: 'Verifying the installation…',
438
+ });
439
+ Object.assign(zh, {
440
+ imageSettings: '图片生成',
441
+ imageSettingsIntro: '可为此 DSH 安装启用文生图工具。云端服务会收到生图提示词,请按需开启。',
442
+ imageSettingsLoad: '正在读取生图设置…', imageSettingsError: '无法更新生图设置。',
443
+ imageSettingsReadError: '无法读取图片生成设置。如果刚更新 FPS,请重启 DSH。',
444
+ imageSettingsUnavailable: '此 DSH 主机暂不支持生图工具。',
445
+ imageZ: '无需 Key,适合快速做概念图和插画。使用 Hugging Face 共享算力,匿名用户通常每天有 2 分钟 GPU 时间,可能排队或暂时不可用。',
446
+ imageCog: '需要可访问免费 CogView-3-Flash 模型的智谱 API Key,适合初稿和中文提示词。速率限制以账号为准。',
447
+ imagePoll: '需要 Pollinations Key 和正的 Pollen 余额。FPS 只调用经核验为零价的社区模型 SDXL Lightning,适合快速出草图。服务可能记录提示词和图片。',
448
+ imageComfy: '通过官方 Comfy MCP 在本机生图。初始工作流使用 SD 1.5,输出约 512 像素级别的草图。建议使用现代 GPU,仅用 CPU 可能需要数分钟。',
449
+ imageComfyRequirements: '支持 Apple 芯片 Mac(macOS 14+)、Windows x64 或 Linux x64(glibc 2.28+)。需要 Python 3.11–3.13(含 pip 和 venv)、Git、8 GB 内存、16 GiB 可用磁盘,以及首次下载所需的网络。',
450
+ imageKey: 'API Key', imageSaveKey: '保存 Key', imageRemoveKey: '移除已保存的 Key', imageKeyConfigured: '已配置 Key',
451
+ imageKeyMissing: '需要配置 Key', imageKeyUnavailable: '请在 DSH 主机上配置 Key。',
452
+ imageInstall: '下载并安装', imageCancel: '取消下载', imageRetry: '重试安装',
453
+ imageReady: '可以生图', imageMissing: '待安装', imageUnsupported: '此电脑不支持自动安装。',
454
+ imageInstallError: '安装未完成。', imageInstalling: '正在安装…',
455
+ imageStage_checking: '正在检查运行要求…', imageStage_environment: '正在准备 Python…',
456
+ imageStage_comfyui: '正在下载 ComfyUI…', imageStage_dependencies: '正在安装 ComfyUI 和官方 Comfy MCP…',
457
+ imageStage_model: '正在下载生图模型…', imageStage_verifying: '正在验证安装…',
458
+ });
459
+
460
+ function brandImage(animated, colors) {
461
+ const motion = animated ? `<style>
462
+ path { animation: pulse 1.6s ease-in-out infinite both; }
463
+ path:nth-child(2) { animation-delay: -1.2s; }
464
+ path:nth-child(3) { animation-delay: -0.8s; }
465
+ path:nth-child(4) { animation-delay: -0.4s; }
466
+ @keyframes pulse { 0%, 100% { opacity: 0.34; } 36% { opacity: 1; } }
467
+ </style>` : '';
468
+ const rays = BRAND_RAYS.map(({ d, transform }, index) =>
469
+ `<path d="${d}" fill="${colors[index % 2]}" transform="${transform}"/>`).join('');
470
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${BRAND_VIEW_BOX}">${motion}${rays}</svg>`;
471
+ return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
472
+ }
473
+
474
+ const CSS = `
475
+ .cw-image-settings { padding: 4px 0 16px; color: var(--dsw-alias-label-primary); }
476
+ .cw-image-settings h2 { margin: 0 0 5px; font-size: 15px; font-weight: 600; }
477
+ .cw-image-settings p { margin: 0; color: var(--dsw-alias-label-secondary); font-size: 13px; line-height: 1.55; }
478
+ .cw-image-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: 8px 20px;
479
+ padding: 14px 0; border-bottom: 1px solid var(--dsw-alias-border-l1); }
480
+ .cw-image-row strong { display: block; margin-bottom: 3px; font-size: 14px; font-weight: 600; }
481
+ .cw-image-switch { width: 36px; height: 21px; margin-top: 1px; padding: 2px; border: 0; border-radius: 20px;
482
+ background: var(--dsw-alias-border-l2); cursor: pointer; }
483
+ .cw-image-switch::before { content: ''; display: block; width: 17px; height: 17px; border-radius: 100%;
484
+ background: var(--dsw-alias-label-caption); }
485
+ .cw-image-switch[aria-checked='true'] { background: var(--dsw-alias-brand-primary); }
486
+ .cw-image-switch[aria-checked='true']::before { margin-left: auto; background: white; }
487
+ .cw-image-actions { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 9px; }
488
+ .cw-image-actions p { flex-basis: 100%; }
489
+ .cw-image-actions button, .cw-image-actions input { min-height: 30px; padding: 4px 9px;
490
+ border: 1px solid var(--dsw-alias-border-l2); border-radius: 7px; color: inherit;
491
+ background: var(--dsw-alias-bg-layer-2); font: inherit; font-size: 13px; }
492
+ .cw-image-actions button:not(:disabled) { cursor: pointer; }
493
+ .cw-image-actions input { width: min(100%, 235px); }
494
+ .cw-image-actions button:disabled, .cw-image-switch:disabled { opacity: .55; }
495
+ .cw-image-settings .cw-image-error { margin: 6px 0 0; color: var(--dsw-alias-state-error-primary); }
496
+ html[data-workbench-theme] body {
497
+ --cw-bg: var(--dsw-alias-bg-base);
498
+ --cw-surface: var(--dsw-alias-bg-layer-1);
499
+ --cw-text: var(--dsw-alias-label-primary);
500
+ --cw-text-secondary: var(--dsw-alias-label-secondary);
501
+ --cw-text-muted: var(--dsw-alias-state-idle-primary);
502
+ --cw-border: var(--dsw-alias-border-l1);
503
+ --cw-border-strong: var(--dsw-alias-border-l2);
504
+ --cw-accent: var(--dsw-alias-brand-primary);
505
+ --cw-danger: var(--dsw-alias-state-error-primary);
506
+ --cw-surface-hover: #f0f0f0;
507
+ --cw-user: var(--dsw-specific-bubble);
508
+ --cw-code: #f6f6f6;
509
+ --cw-accent-hover: #8c3924;
510
+ --cw-on-accent: #fffaf6;
511
+ --cw-sidebar-fill: #ececec;
512
+ --cw-sidebar-hover: #e1e1e1;
513
+ --cw-sidebar-selected: #d4d4d4;
514
+ --cw-new-session: transparent;
515
+ --cw-new-session-hover: var(--cw-sidebar-hover);
516
+ --cw-sidebar-muted: #5e5e5e;
517
+ --cw-panel: #ffffff;
518
+ --cw-panel-layer: #fafafa;
519
+ --cw-panel-hover: #f2f2f2;
520
+ --cw-panel-selected: #e5e5e5;
521
+ --cw-context-active: var(--cw-panel-selected);
522
+ --cw-tabs-bg: #f1f1f1;
523
+ --cw-tabs-hover: #e9e9e9;
524
+ --cw-tabs-active: #ffffff;
525
+ --cw-tabs-text: #5e5e5e;
526
+ --cw-popover: #ffffff;
527
+ --cw-shadow: rgba(0, 0, 0, 0.13);
528
+ --cw-metric-track: #ededed;
529
+ --cw-metric-ring: #858585;
530
+ --cw-metric-blue: #2875bc;
531
+ --cw-metric-orange: #b65d35;
532
+ --cw-metric-gold: #8b5b15;
533
+ --cw-metric-teal: #25816c;
534
+ --cw-metric-free: #a2a2a2;
535
+ --cw-brand-ray: ${BRAND_COLORS.light[0]};
536
+ --cw-brand-ray-shade: ${BRAND_COLORS.light[1]};
537
+ --cw-thinking-icon: ${brandImage(false, BRAND_COLORS.light)};
538
+ --cw-thinking-icon-animated: ${brandImage(true, BRAND_COLORS.light)};
539
+ }
540
+ html[data-workbench-theme] body[data-ds-dark-theme] {
541
+ --cw-surface-hover: #2d2d2d;
542
+ --cw-code: #161616;
543
+ --cw-accent-hover: #e48868;
544
+ --cw-on-accent: #2b1b15;
545
+ --cw-sidebar-fill: #272423;
546
+ --cw-sidebar-hover: #383432;
547
+ --cw-sidebar-selected: #423d3a;
548
+ --cw-new-session: var(--cw-sidebar-selected);
549
+ --cw-new-session-hover: #504946;
550
+ --cw-sidebar-muted: #aaa39e;
551
+ --cw-panel: #1b1b1b;
552
+ --cw-panel-layer: #202020;
553
+ --cw-panel-hover: #2d2c2b;
554
+ --cw-panel-selected: #323130;
555
+ --cw-context-active: var(--cw-tabs-active);
556
+ --cw-tabs-bg: #292725;
557
+ --cw-tabs-hover: #34312f;
558
+ --cw-tabs-active: #403b38;
559
+ --cw-tabs-text: #b4aca5;
560
+ --cw-popover: #2b2a29;
561
+ --cw-shadow: rgba(0, 0, 0, 0.42);
562
+ --cw-metric-track: #343331;
563
+ --cw-metric-ring: #55524f;
564
+ --cw-metric-blue: #5c9ddd;
565
+ --cw-metric-orange: #dc865e;
566
+ --cw-metric-gold: #cfa24f;
567
+ --cw-metric-teal: #5eac97;
568
+ --cw-metric-free: #464441;
569
+ --cw-brand-ray: ${BRAND_COLORS.dark[0]};
570
+ --cw-brand-ray-shade: ${BRAND_COLORS.dark[1]};
571
+ --cw-thinking-icon: ${brandImage(false, BRAND_COLORS.dark)};
572
+ --cw-thinking-icon-animated: ${brandImage(true, BRAND_COLORS.dark)};
573
+ }
574
+
575
+ /* Native settings uses its own control and navigation surface tokens. */
576
+ html[data-workbench-theme] body:not([data-ds-dark-theme]) [data-shortcut-modal="settings"] {
577
+ --dsw-alias-bg-layer-2: var(--cw-popover);
578
+ --dsw-alias-bg-module-platform: #f4f4f4;
579
+ --dsw-specific-sidebar-nav-item-active: #e9e9e9;
580
+ --dsw-alias-interactive-bg-hover: #f0f0f0;
581
+ --dsw-alias-interactive-bg-hover-solid: #f0f0f0;
582
+ --dsw-alias-interactive-bg-active: #e5e5e5;
583
+ --dsw-alias-border-l4: var(--cw-border-strong);
584
+ }
585
+ html[data-workbench-theme] body:not([data-ds-dark-theme]) [data-shortcut-modal="settings"]
586
+ button[class*="_themeCube"][aria-pressed="true"] {
587
+ background: #eeeeee;
588
+ border-color: #bcbcbc;
589
+ }
590
+ html[data-workbench-theme] body:not([data-ds-dark-theme]):has([data-shortcut-modal="settings"])
591
+ > [role="menu"][data-menu-material] {
592
+ --dsw-menu-surface-fill: var(--cw-popover);
593
+ --dsw-menu-backdrop-filter: none;
594
+ --dsw-alias-interactive-bg-hover: #f0f0f0;
595
+ }
596
+
597
+ .cw-root {
598
+ color: var(--cw-text);
599
+ font-family: var(--dsw-font-family, "Avenir Next", "Segoe UI", sans-serif);
600
+ font-size: var(--dsh-content-font-size, 14px);
601
+ box-sizing: border-box;
602
+ }
603
+ .cw-root *, .cw-root *::before, .cw-root *::after { box-sizing: border-box; }
604
+ .cw-root button, .cw-root input, .cw-root textarea { font: inherit; }
605
+ .cw-root button { color: inherit; }
606
+ .cw-root button:focus-visible, .cw-root input:focus-visible,
607
+ .cw-root textarea:focus-visible, .cw-root summary:focus-visible {
608
+ outline: 2px solid var(--cw-accent);
609
+ outline-offset: 2px;
610
+ }
611
+ .cw-icon { width: 16px; height: 16px; flex: 0 0 auto; display: block; }
612
+ .cw-icon-button {
613
+ width: 30px; height: 30px; padding: 0; border: 0; border-radius: 8px;
614
+ background: transparent; display: inline-flex; align-items: center;
615
+ justify-content: center; cursor: pointer;
616
+ }
617
+ .cw-icon-button:hover { background: var(--cw-surface-hover); }
618
+ .cw-icon-button:disabled { opacity: 0.4; cursor: default; }
619
+
620
+ /* DSH's sidebar, workspace and account component classes. Keep their layout and controls mounted. */
621
+ html[data-workbench-theme] .kB3qhq_root {
622
+ --cw-sidebar-font: 13px;
623
+ --dsw-alias-interactive-bg-hover: var(--cw-sidebar-hover);
624
+ --dsh-sidebar-inline-padding: 8px;
625
+ --dsh-sidebar-row-padding: 10px;
626
+ --dsh-sidebar-label-gap: 8px;
627
+ --dsh-sidebar-paint-inset: 1px;
628
+ background: var(--cw-sidebar-fill);
629
+ font-size: var(--cw-sidebar-font);
630
+ }
631
+ html[data-workbench-theme] .kB3qhq_logoRow > .kB3qhq_brand,
632
+ html[data-workbench-theme] .kB3qhq_topStrip + .kB3qhq_logoRow,
633
+ html[data-workbench-theme] .kB3qhq_root .kB3qhq_railMark {
634
+ display: none;
635
+ }
636
+ html[data-workbench-theme] .kB3qhq_collapsed .kB3qhq_toggle .kB3qhq_panelIcon {
637
+ display: inline;
638
+ }
639
+ html[data-workbench-theme] .kB3qhq_root > .kB3qhq_topStrip {
640
+ height: 44px;
641
+ }
642
+ .cw-session-history {
643
+ display: inline-flex; align-items: center; gap: 2px; flex: none;
644
+ margin-inline-start: 4px; -webkit-app-region: no-drag;
645
+ }
646
+ .kB3qhq_collapsed .cw-session-history { display: none; }
647
+ .cw-session-history .cw-icon-button {
648
+ width: 28px; height: 28px; color: var(--dsw-alias-label-primary);
649
+ }
650
+ .cw-session-history .cw-icon-button:not(:disabled):hover {
651
+ background: var(--dsw-alias-interactive-bg-hover);
652
+ }
653
+ .cw-session-history .cw-icon-button:disabled { background: transparent; }
654
+ .cw-session-history .cw-icon-button:focus-visible {
655
+ outline: 2px solid var(--cw-accent); outline-offset: -2px;
656
+ }
657
+ /* The native hero mark slot lives beside the built-in headline. */
658
+ html[data-workbench-theme] ._8tPydW_headline:has(.cw-hero-greeting) > ._8tPydW_titleGroup {
659
+ display: none;
660
+ }
661
+ html[data-workbench-theme] ._8tPydW_fishHitbox:has(.cw-hero-greeting) {
662
+ max-inline-size: 100%;
663
+ }
664
+ .cw-hero-greeting {
665
+ display: inline-flex;
666
+ align-items: center;
667
+ gap: 10px;
668
+ min-inline-size: 0;
669
+ padding-block: 0 10px;
670
+ color: var(--dsw-alias-label-primary);
671
+ font-family: Georgia, "Songti SC", "Noto Serif CJK SC", serif;
672
+ font-size: clamp(25px, 4vw, 36px);
673
+ font-weight: 400;
674
+ line-height: 1.25;
675
+ text-align: center;
676
+ overflow-wrap: anywhere;
677
+ }
678
+ .cw-hero-greeting svg { inline-size: 1.08em; block-size: 1.08em; flex: none; }
679
+
680
+ /* Keep the native running-status row, elapsed time and announcements. */
681
+ @media (forced-colors: none) {
682
+ html[data-workbench-theme] [data-chat-running] {
683
+ --dsw-alias-label-deep-diving: var(--cw-sidebar-muted);
684
+ --dsw-alias-label-deep-diving-shimmer: var(--cw-text);
685
+ }
686
+ html[data-workbench-theme] [data-chat-running] .tnXGgG_runningIcon {
687
+ width: calc(17px + var(--dsh-content-font-delta, 0px));
688
+ height: calc(17px + var(--dsh-content-font-delta, 0px));
689
+ background: var(--cw-thinking-icon) center / contain no-repeat;
690
+ }
691
+ html[data-workbench-theme] [data-chat-running] .tnXGgG_runningIcon > * { display: none; }
692
+ }
693
+ @media (forced-colors: none) and (prefers-reduced-motion: no-preference) {
694
+ html[data-workbench-theme] [data-chat-running]:not([data-holding]) .tnXGgG_runningIcon {
695
+ background-image: var(--cw-thinking-icon-animated);
696
+ }
697
+ }
698
+ html[data-workbench-theme] .kB3qhq_root:not(.kB3qhq_collapsed) .kB3qhq_newSession {
699
+ height: 34px;
700
+ margin: 0 1px 3px;
701
+ padding: 5px 10px;
702
+ border: 0;
703
+ border-radius: 8px;
704
+ background: var(--cw-new-session);
705
+ justify-content: flex-start;
706
+ font-size: var(--cw-sidebar-font);
707
+ }
708
+ html[data-workbench-theme] .kB3qhq_root:not(.kB3qhq_collapsed) .kB3qhq_newSession:hover {
709
+ background: var(--cw-new-session-hover);
710
+ }
711
+ html[data-workbench-theme] .kB3qhq_root .kB3qhq_newSessionContent {
712
+ width: auto;
713
+ gap: 8px;
714
+ }
715
+ html[data-workbench-theme] .kB3qhq_root:not(.kB3qhq_collapsed) .kB3qhq_newSessionContent {
716
+ justify-content: flex-start;
717
+ }
718
+ html[data-workbench-theme] .kB3qhq_root:not(.kB3qhq_collapsed) .kB3qhq_panelList {
719
+ gap: 1px;
720
+ margin: 2px 0 2px;
721
+ }
722
+ html[data-workbench-theme] .kB3qhq_root:not(.kB3qhq_collapsed) .kB3qhq_panelRow {
723
+ min-height: 31px;
724
+ border-radius: 7px;
725
+ padding-block: 4px;
726
+ }
727
+ html[data-workbench-theme] .kB3qhq_root .Kjh-qa_root:not(.Kjh-qa_rail) .Kjh-qa_sectionHeader {
728
+ display: grid;
729
+ grid-template-columns: minmax(0, 1fr) auto;
730
+ grid-template-areas: "search search" "label actions";
731
+ grid-template-rows: 31px 27px;
732
+ align-items: center;
733
+ height: auto;
734
+ margin: 0 0 3px;
735
+ padding: 0 1px;
736
+ gap: 12px 6px;
737
+ }
738
+ html[data-workbench-theme] .kB3qhq_root .Kjh-qa_root:not(.Kjh-qa_rail) .Kjh-qa_sectionHeader:has(.Kjh-qa_searchExpanded) {
739
+ grid-template-rows: 31px 0;
740
+ row-gap: 0;
741
+ margin-bottom: 7px;
742
+ }
743
+ html[data-workbench-theme] .kB3qhq_root .Kjh-qa_sectionLabel {
744
+ grid-area: label;
745
+ margin-left: 9px;
746
+ max-width: none;
747
+ color: var(--cw-sidebar-muted);
748
+ font-size: 11px;
749
+ font-weight: 500;
750
+ }
751
+ html[data-workbench-theme] .kB3qhq_root .Kjh-qa_headerActions { grid-area: actions; }
752
+ html[data-workbench-theme] .kB3qhq_root .Kjh-qa_searchSlot {
753
+ grid-area: search;
754
+ max-width: none;
755
+ width: 100%;
756
+ margin: 0;
757
+ padding: 0;
758
+ }
759
+ html[data-workbench-theme] .kB3qhq_root .Kjh-qa_searchSlot .Kjh-qa_search {
760
+ height: 31px;
761
+ border-radius: 7px;
762
+ }
763
+ html[data-workbench-theme] .kB3qhq_root .Kjh-qa_searchSlot .Kjh-qa_search:not(.Kjh-qa_searchExpanded) .Kjh-qa_searchButton {
764
+ width: 100%; height: 100%; justify-content: flex-start; gap: 8px;
765
+ padding-inline: 9px;
766
+ color: var(--dsw-alias-label-primary);
767
+ }
768
+ html[data-workbench-theme] .kB3qhq_root .Kjh-qa_searchButton[aria-expanded="false"]::after {
769
+ content: attr(aria-label);
770
+ font-size: var(--cw-sidebar-font);
771
+ }
772
+ html[data-workbench-theme] .kB3qhq_root .Kjh-qa_searchExpanded {
773
+ background: var(--cw-surface);
774
+ border-color: var(--cw-border-strong);
775
+ }
776
+ html[data-workbench-theme] .kB3qhq_root [data-row-key^="workspace:"] {
777
+ height: 32px;
778
+ border-radius: 7px;
779
+ color: var(--cw-sidebar-muted);
780
+ font-size: var(--cw-sidebar-font);
781
+ }
782
+ html[data-workbench-theme] .kB3qhq_root :is(.kB3qhq_panelRow, .JoiKIa_title, .JoiKIa_searchResultTitle, .Kjh-qa_searchInput) {
783
+ font-size: var(--cw-sidebar-font);
784
+ line-height: 20px;
785
+ }
786
+ html[data-workbench-theme] .kB3qhq_root [data-row-key^="workspace:"] .JoiKIa_folder { display: none; }
787
+ html[data-workbench-theme] .kB3qhq_root [data-row-key^="workspace:"] .JoiKIa_projectText {
788
+ order: -2;
789
+ flex: 0 1 auto;
790
+ }
791
+ html[data-workbench-theme] .kB3qhq_root [data-row-key^="workspace:"] .JoiKIa_chevron {
792
+ order: -1;
793
+ width: 11px;
794
+ display: inline-flex;
795
+ }
796
+ html[data-workbench-theme] .kB3qhq_root .JoiKIa_projectRow .JoiKIa_rowActions { margin-left: auto; }
797
+ html[data-workbench-theme] .kB3qhq_root .JoiKIa_sessionRow {
798
+ border-radius: 7px;
799
+ }
800
+ html[data-workbench-theme] .kB3qhq_root .JoiKIa_sessionRow.JoiKIa_selected {
801
+ background-color: var(--cw-sidebar-selected);
802
+ }
803
+ html[data-workbench-theme] .kB3qhq_root:not(.kB3qhq_collapsed) .kB3qhq_footArea {
804
+ flex-direction: row;
805
+ align-items: center;
806
+ gap: 4px;
807
+ min-height: 43px;
808
+ margin: 0 calc(-1 * var(--dsh-sidebar-inline-padding)) -6px;
809
+ padding: 3px 9px;
810
+ border-top: 1px solid var(--cw-border);
811
+ }
812
+ html[data-workbench-theme] .kB3qhq_root:not(.kB3qhq_collapsed) .kB3qhq_settingsArea {
813
+ order: -1;
814
+ flex: 1;
815
+ width: auto;
816
+ }
817
+ html[data-workbench-theme] .kB3qhq_root:not(.kB3qhq_collapsed) .kB3qhq_footerActions {
818
+ width: auto;
819
+ flex: 0 0 auto;
820
+ align-items: center;
821
+ }
822
+ html[data-workbench-theme] .kB3qhq_root .hjANda_trigger:not([data-collapsed="true"]) {
823
+ height: 34px;
824
+ padding-inline: 5px;
825
+ font-size: var(--cw-sidebar-font);
826
+ }
827
+ html[data-workbench-theme] .kB3qhq_root .hjANda_trigger:not([data-collapsed="true"])::after {
828
+ content: "";
829
+ width: 5px; height: 5px;
830
+ margin: -3px 3px 0 0;
831
+ flex: 0 0 auto;
832
+ border-right: 1px solid var(--cw-sidebar-muted);
833
+ border-bottom: 1px solid var(--cw-sidebar-muted);
834
+ transform: rotate(45deg);
835
+ }
836
+ html[data-workbench-theme] .kB3qhq_footArea .hjANda_avatar {
837
+ width: 20px;
838
+ height: 20px;
839
+ background: #5f4ba6;
840
+ color: #f5f1ff;
841
+ font-size: 11px;
842
+ font-weight: 500;
843
+ line-height: 1;
844
+ text-transform: uppercase;
845
+ }
846
+ html[data-workbench-theme] .kB3qhq_footArea .hjANda_avatar > * { display: none; }
847
+ html[data-workbench-theme] .kB3qhq_footArea .hjANda_avatar::before {
848
+ content: var(--cw-account-initial, "•") / "";
849
+ }
850
+ .cw-sidebar-settings {
851
+ width: 32px;
852
+ height: 32px;
853
+ color: var(--dsw-alias-label-primary);
854
+ border-radius: 7px;
855
+ }
856
+ .cw-sidebar-settings:hover { background: var(--dsw-alias-interactive-bg-hover); }
857
+ .cw-sidebar-settings:focus-visible { outline: 2px solid var(--cw-accent); outline-offset: -2px; }
858
+ .cw-sidebar-settings .cw-icon { width: 17px; height: 17px; }
859
+
860
+ .cw-message { width: 100%; padding: 9px 28px; background: transparent; }
861
+ .cw-message-inner { width: min(100%, 760px); margin: 0 auto; }
862
+ .cw-user-inner { display: flex; justify-content: flex-end; }
863
+ .cw-user-bubble {
864
+ max-width: min(82%, 620px); padding: 10px 15px; border-radius: 14px;
865
+ background: var(--cw-user); color: var(--cw-text);
866
+ line-height: 1.6; overflow-wrap: anywhere;
867
+ }
868
+ .cw-assistant-inner {
869
+ color: var(--cw-text); line-height: 1.75;
870
+ font-size: calc(var(--dsh-content-font-size, 14px) + 1px);
871
+ }
872
+ .cw-text { white-space: pre-wrap; overflow-wrap: anywhere; }
873
+ .cw-text + .cw-text { margin-top: 12px; }
874
+ .cw-heading {
875
+ margin: 18px 0 7px; color: var(--cw-text);
876
+ font-size: 15px; font-weight: 600; line-height: 1.45;
877
+ }
878
+ .cw-heading:first-child { margin-top: 0; }
879
+ .cw-list {
880
+ margin: 8px 0; padding-left: 22px; color: var(--cw-text);
881
+ line-height: 1.7;
882
+ }
883
+ .cw-list li + li { margin-top: 3px; }
884
+ .cw-quote {
885
+ margin: 10px 0; padding: 2px 0 2px 12px;
886
+ border-left: 2px solid var(--cw-border-strong);
887
+ color: var(--cw-text-secondary); white-space: pre-wrap;
888
+ }
889
+ .cw-code {
890
+ margin: 12px 0; padding: 13px 15px; overflow: auto;
891
+ border: 1px solid var(--cw-border); border-radius: 10px;
892
+ background: var(--cw-code); color: var(--cw-text);
893
+ font-family: var(--ds-font-family-code, monospace);
894
+ font-size: var(--dsh-code-font-size, 12px); line-height: 1.55;
895
+ white-space: pre; tab-size: 2;
896
+ }
897
+ .cw-reasoning {
898
+ margin: 0 0 12px; color: var(--cw-text-secondary);
899
+ border-left: 2px solid var(--cw-border-strong); padding-left: 12px;
900
+ }
901
+ .cw-reasoning summary { cursor: pointer; color: var(--cw-text-muted); }
902
+ .cw-reasoning-content { margin-top: 8px; white-space: pre-wrap; }
903
+ .cw-interrupted { margin-top: 10px; color: var(--cw-text-muted); font-size: 12px; }
904
+ .cw-composer-files { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
905
+ .cw-composer-file {
906
+ display: inline-flex; align-items: center; gap: 6px;
907
+ max-width: 220px; padding: 4px 8px; border: 1px solid var(--cw-border);
908
+ border-radius: 8px; background: var(--cw-surface);
909
+ color: var(--cw-text-secondary); font-size: 11.5px;
910
+ }
911
+ .cw-composer-file .cw-icon { width: 13px; height: 13px; color: var(--cw-text-muted); flex: 0 0 auto; }
912
+ .cw-composer-file-name {
913
+ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
914
+ }
915
+ .cw-composer-file-size { color: var(--cw-text-muted); font-size: 10.5px; flex: 0 0 auto; }
916
+ .cw-composer-file button {
917
+ width: 18px; height: 18px; padding: 0; border: 0; border-radius: 5px;
918
+ display: inline-flex; align-items: center; justify-content: center;
919
+ background: transparent; color: var(--cw-text-muted) !important; cursor: pointer;
920
+ flex: 0 0 auto;
921
+ }
922
+ .cw-composer-file button:hover { background: var(--cw-border); color: var(--cw-text) !important; }
923
+ .cw-composer-file button .cw-icon { width: 11px; height: 11px; }
924
+ .cw-composer-file-pending { border-style: dashed; color: var(--cw-text-muted); }
925
+ .cw-tool-row { width: 100%; padding: 2px 28px; }
926
+ .cw-tool-row-inner { width: min(100%, 760px); margin: 0 auto; }
927
+ .cw-tool-summary {
928
+ width: 100%; min-height: 30px; display: flex; align-items: center; gap: 8px;
929
+ padding: 5px 8px; border: 0; border-radius: 7px;
930
+ background: transparent; color: var(--cw-text-secondary) !important;
931
+ text-align: left; cursor: pointer; font-size: 12.5px;
932
+ }
933
+ .cw-tool-summary:hover { background: var(--cw-surface-hover); color: var(--cw-text) !important; }
934
+ .cw-tool-summary:disabled { cursor: default; }
935
+ .cw-tool-leading { color: var(--cw-text-muted); display: inline-flex; }
936
+ .cw-tool-leading .cw-icon { width: 14px; height: 14px; }
937
+ .cw-tool-label { flex: 0 0 auto; color: var(--cw-text); }
938
+ .cw-tool-detail {
939
+ min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis;
940
+ white-space: nowrap; color: var(--cw-text-muted); font-size: 11px;
941
+ }
942
+ .cw-tool-count { color: var(--cw-text-muted); font-size: 10.5px; }
943
+ .cw-tool-state {
944
+ flex: 0 0 auto; color: var(--cw-text-muted); font-size: 10.5px;
945
+ }
946
+ .cw-tool-state[data-state="error"] { color: var(--cw-danger); }
947
+ .cw-tool-state[data-state="running"],
948
+ .cw-tool-state[data-state="preparing"] { color: var(--cw-accent); }
949
+ .cw-tool-chevron { flex: none; color: var(--cw-text-muted); }
950
+ .cw-tool-chevron .cw-icon { width: 13px; height: 13px; transform: rotate(-90deg); }
951
+ [data-workbench-theme] [data-turn-process][aria-expanded][data-turn-process-tool-calls]:not([data-turn-process-tool-calls="0"], [data-cw-work-summary])::after {
952
+ content: "· " attr(data-turn-process-tool-calls) " tool calls";
953
+ order: 1; flex: none; margin-inline-start: 7px;
954
+ font-size: calc(var(--dsh-content-font-size, 14px) - 1px);
955
+ }
956
+ [data-workbench-theme] [data-turn-process][aria-expanded][data-turn-process-tool-calls="1"]:not([data-cw-work-summary])::after {
957
+ content: "· 1 tool call";
958
+ }
959
+ html[data-workbench-theme][lang|="zh"] [data-turn-process][aria-expanded][data-turn-process-tool-calls]:not([data-turn-process-tool-calls="0"], [data-cw-work-summary])::after {
960
+ content: "· " attr(data-turn-process-tool-calls) " 次工具调用";
961
+ }
962
+ [data-workbench-theme] [data-turn-process] > .gTO55a_chevron {
963
+ order: 2;
964
+ margin-inline-start: 6px;
965
+ }
966
+ [data-workbench-theme] [data-turn-process][aria-expanded="false"] > .gTO55a_chevron {
967
+ transform: rotate(-90deg);
968
+ }
969
+ [data-workbench-theme] [data-turn-process][aria-expanded="false"][data-cw-work-summary]::after {
970
+ content: "· " attr(data-cw-work-summary);
971
+ order: 1; min-width: 0; margin-inline-start: 7px;
972
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
973
+ font-size: calc(var(--dsh-content-font-size, 14px) - 1px);
974
+ }
975
+ [data-workbench-theme] [data-turn-process][aria-expanded="false"][data-cw-work-summary] > .gTO55a_label {
976
+ flex: none;
977
+ max-width: 45%;
978
+ }
979
+ [data-workbench-theme] [data-step-process] .r52wTW_leading { order: 1; }
980
+ [data-workbench-theme] [data-step-process] [data-step-process-icon] { display: none; }
981
+ [data-workbench-theme] [data-step-process] [data-step-process-chevron] { opacity: 1; }
982
+ [data-workbench-theme] [data-step-process] button[aria-expanded="false"] [data-step-process-chevron] svg {
983
+ transform: rotate(-90deg);
984
+ }
985
+ [data-workbench-theme] [data-step-process]:not([data-group-expanded-mode]) button[data-cw-work-summary] > :not(.r52wTW_leading) {
986
+ display: none;
987
+ }
988
+ [data-workbench-theme] [data-step-process]:not([data-group-expanded-mode]) button[data-cw-work-summary]::before {
989
+ content: attr(data-cw-work-summary);
990
+ min-width: 0;
991
+ overflow: hidden;
992
+ text-overflow: ellipsis;
993
+ white-space: nowrap;
994
+ }
995
+ .cw-attachment {
996
+ display: inline-flex; align-items: center; gap: 6px; padding: 4px 8px;
997
+ border: 1px solid var(--cw-border); border-radius: 7px;
998
+ color: var(--cw-text-secondary); font-size: 12px;
999
+ }
1000
+
1001
+ .cw-composer {
1002
+ width: min(100%, 760px); margin: 0 auto; background: transparent;
1003
+ padding: 0;
1004
+ }
1005
+ [data-composer-seat] .cw-composer-composer,
1006
+ [data-workbench-theme] [data-composer-seat] [data-testid="todo-panel"] {
1007
+ /* Match the native chat column, including its adjustable width. */
1008
+ width: min(var(--dsh-chat-content-width, 760px),
1009
+ calc(100% - 2 * (var(--dsh-composer-side-clearance, 16px) + 16px)));
1010
+ }
1011
+ [data-workbench-theme] [data-composer-seat] [data-testid="todo-panel"] {
1012
+ position: relative; max-width: none; margin: 0 auto 8px;
1013
+ border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px;
1014
+ background: var(--dsw-alias-bg-base); box-shadow: none; backdrop-filter: none;
1015
+ }
1016
+ [data-workbench-theme] [data-testid="todo-panel"] > div { padding: 6px 13px; }
1017
+ [data-workbench-theme] [data-testid="todo-panel"]:has(button[aria-expanded="true"]) > div {
1018
+ padding: 12px 14px;
1019
+ }
1020
+ [data-workbench-theme] [data-testid="todo-panel"] button[aria-expanded="true"] {
1021
+ position: absolute; inset-block-start: 7px; inset-inline-end: 7px; z-index: 1;
1022
+ display: grid; place-items: center; width: 26px; height: 26px; border-radius: 6px;
1023
+ }
1024
+ [data-workbench-theme] [data-testid="todo-panel"] button[aria-expanded="true"]:hover {
1025
+ background: var(--dsw-alias-interactive-bg-hover);
1026
+ }
1027
+ [data-workbench-theme] [data-testid="todo-panel"] button[aria-expanded="true"] > :not(:last-child) {
1028
+ position: absolute; inline-size: 1px; block-size: 1px;
1029
+ overflow: hidden; clip-path: inset(50%); white-space: nowrap;
1030
+ }
1031
+ [data-workbench-theme] [data-testid="todo-panel"] ul {
1032
+ gap: 7px; max-height: min(240px, 30vh); padding-inline-end: 25px;
1033
+ }
1034
+ [data-workbench-theme] [data-testid="todo-panel"] li[data-status] {
1035
+ align-items: flex-start; gap: 7px; color: var(--dsw-alias-label-primary);
1036
+ font-size: var(--dsh-content-font-size, 14px); line-height: 1.5;
1037
+ }
1038
+ [data-workbench-theme] [data-testid="todo-panel"] li[data-status="completed"] {
1039
+ color: var(--dsw-alias-label-secondary);
1040
+ }
1041
+ [data-workbench-theme] [data-testid="todo-panel"] li[data-status] > :last-child {
1042
+ white-space: normal; overflow-wrap: anywhere;
1043
+ }
1044
+ [data-workbench-theme] [data-testid="todo-panel"] li > [role="img"] {
1045
+ position: relative; box-sizing: border-box; margin-block-start: 3px;
1046
+ inline-size: 14px; block-size: 14px; border: 1.5px solid currentColor; border-radius: 50%;
1047
+ }
1048
+ [data-workbench-theme] [data-testid="todo-panel"] li > [role="img"] > * { display: none; }
1049
+ [data-workbench-theme] [data-testid="todo-panel"] li[data-status="pending"] > [role="img"] {
1050
+ border-style: dashed; color: var(--dsw-alias-label-secondary);
1051
+ }
1052
+ [data-workbench-theme] [data-testid="todo-panel"] li[data-status="completed"] > [role="img"] {
1053
+ background: currentColor;
1054
+ }
1055
+ [data-workbench-theme] [data-testid="todo-panel"] li[data-status="completed"] > [role="img"]::after {
1056
+ content: ""; box-sizing: border-box; width: 4px; height: 7px;
1057
+ border: solid var(--dsw-alias-bg-base); border-width: 0 1.5px 1.5px 0;
1058
+ transform: translateY(-1px) rotate(40deg);
1059
+ }
1060
+ [data-workbench-theme] [data-testid="todo-panel"] li[data-status="in_progress"] > [role="img"]::after {
1061
+ content: ""; width: 2px; height: 2px; border-radius: 50%; background: currentColor;
1062
+ box-shadow: -3px 0 currentColor, 3px 0 currentColor;
1063
+ }
1064
+ .cw-command-menu {
1065
+ max-height: 240px; overflow-y: auto; margin-bottom: 6px;
1066
+ padding: 4px; border: 1px solid var(--cw-border-strong);
1067
+ border-radius: 11px; background: var(--cw-bg);
1068
+ box-shadow: 0 12px 32px var(--cw-shadow);
1069
+ }
1070
+ .cw-command-row {
1071
+ width: 100%; min-height: 30px; display: flex; align-items: baseline;
1072
+ gap: 9px; padding: 5px 8px; border: 0; border-radius: 7px;
1073
+ background: transparent; color: var(--cw-text-secondary) !important;
1074
+ text-align: left; cursor: pointer;
1075
+ }
1076
+ .cw-command-row:hover,
1077
+ .cw-command-row[data-active="true"] {
1078
+ background: var(--cw-surface-hover); color: var(--cw-text) !important;
1079
+ }
1080
+ .cw-command-name {
1081
+ flex: 0 0 auto; color: var(--cw-text); font-size: 12.5px;
1082
+ font-family: var(--ds-font-family-code, monospace);
1083
+ }
1084
+ .cw-command-detail {
1085
+ min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis;
1086
+ white-space: nowrap; color: var(--cw-text-muted); font-size: 11px;
1087
+ }
1088
+ .cw-command-hint {
1089
+ flex: 0 0 auto; color: var(--cw-text-muted); font-size: 10.5px;
1090
+ font-family: var(--ds-font-family-code, monospace);
1091
+ }
1092
+ .cw-command-status {
1093
+ padding: 8px; color: var(--cw-text-muted); font-size: 11.5px;
1094
+ }
1095
+ .cw-command-status[data-error="true"] { color: var(--cw-danger); }
1096
+ .cw-composer-card {
1097
+ border: 1px solid var(--cw-border-strong); border-radius: 12px;
1098
+ background: var(--cw-surface); color: var(--cw-text);
1099
+ padding: 4px 8px;
1100
+ margin-bottom: 12px;
1101
+ }
1102
+ .cw-composer-footer {
1103
+ display: flex; align-items: center;
1104
+ gap: 12px; padding: 0 4px 8px 7px; min-height: 18px;
1105
+ }
1106
+ .cw-composer-hint {
1107
+ padding: 0; color: var(--cw-text-muted);
1108
+ text-align: left; font-size: 10px; line-height: 1.2;
1109
+ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; opacity: 0;
1110
+ }
1111
+ .cw-composer:has(.cw-composer-input:focus) .cw-composer-hint { opacity: 1; }
1112
+ .cw-footer-controls { display: flex; align-items: center; gap: 1px; flex: 0 1 auto; min-width: 0; margin-inline-start: auto; }
1113
+ .cw-folder-button { max-width: 150px; }
1114
+ .cw-folder-button .cw-icon { width: 12px; height: 12px; }
1115
+ .cw-mention-menu .cw-command-row .cw-icon {
1116
+ width: 13px; height: 13px; color: var(--cw-text-muted); flex: 0 0 auto;
1117
+ }
1118
+ .cw-mention-heading {
1119
+ padding: 7px 10px 4px; color: var(--cw-text-muted); font-size: 10.5px;
1120
+ letter-spacing: 0.02em;
1121
+ }
1122
+ .cw-permission-button { max-width: 150px; }
1123
+ .cw-permission-button .cw-icon { width: 12px; height: 12px; }
1124
+ .cw-permission-menu { width: 280px; }
1125
+ .cw-permission-badge {
1126
+ border: 1px solid var(--cw-border-strong); border-radius: 3px;
1127
+ padding: 0 3px; color: var(--cw-text-muted); font-size: 8px;
1128
+ }
1129
+ .cw-permission-confirm {
1130
+ margin-top: 5px; padding: 8px; border-top: 1px solid var(--cw-border);
1131
+ color: var(--cw-text-secondary); font-size: 11.5px; line-height: 1.5;
1132
+ }
1133
+ .cw-permission-confirm-actions {
1134
+ display: flex; justify-content: flex-end; gap: 6px; margin-top: 8px;
1135
+ }
1136
+ .cw-permission-confirm-actions button {
1137
+ min-width: 54px; height: 26px; padding: 0 9px;
1138
+ border: 1px solid var(--cw-border-strong); border-radius: 7px;
1139
+ background: transparent; color: var(--cw-text-secondary) !important;
1140
+ font-size: 11.5px; cursor: pointer;
1141
+ }
1142
+ .cw-permission-confirm-actions button:hover { background: var(--cw-surface-hover); }
1143
+ .cw-permission-confirm-actions button[data-primary="true"] {
1144
+ border-color: var(--cw-accent); background: var(--cw-accent); color: var(--cw-on-accent) !important;
1145
+ }
1146
+ .cw-model-wrap { position: relative; min-width: 0; flex: 0 1 auto; }
1147
+ .cw-model-button {
1148
+ max-width: 190px; height: 20px; padding: 0 5px; border: 0; border-radius: 5px;
1149
+ background: transparent; color: var(--cw-text-secondary) !important;
1150
+ display: inline-flex; align-items: center; gap: 3px; cursor: pointer;
1151
+ font-size: 11px; min-width: 0;
1152
+ }
1153
+ .cw-model-button:hover { background: var(--cw-surface-hover); color: var(--cw-text) !important; }
1154
+ .cw-model-button:disabled { cursor: default; opacity: 0.6; }
1155
+ .cw-model-button-label {
1156
+ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1157
+ }
1158
+ .cw-model-button .cw-icon { width: 13px; height: 13px; color: var(--cw-text-muted); }
1159
+ .cw-model-effort {
1160
+ flex: 0 0 auto; color: var(--cw-text-muted); font-size: 11px;
1161
+ max-width: 74px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1162
+ }
1163
+ .cw-model-menu {
1164
+ position: fixed; z-index: 2147483000;
1165
+ max-height: min(380px, 55vh);
1166
+ overflow-y: auto; padding: 5px;
1167
+ border: 1px solid var(--cw-border-strong);
1168
+ border-radius: 11px;
1169
+ background: var(--cw-popover);
1170
+ color: var(--cw-text);
1171
+ box-shadow: 0 14px 40px var(--cw-shadow);
1172
+ }
1173
+ .cw-model-search {
1174
+ width: 100%; height: 30px; margin-bottom: 4px; padding: 0 8px;
1175
+ border: 1px solid var(--cw-border); border-radius: 7px;
1176
+ background: var(--cw-surface); color: var(--cw-text); outline: none;
1177
+ }
1178
+ .cw-model-group-label {
1179
+ padding: 8px 8px 5px; color: var(--cw-text-muted);
1180
+ font-size: 11px; line-height: 1.3;
1181
+ }
1182
+ .cw-model-more {
1183
+ justify-content: space-between; margin-top: 3px;
1184
+ border-top: 1px solid var(--cw-border); border-radius: 0 0 7px 7px;
1185
+ }
1186
+ .cw-model-more .cw-icon { width: 13px; height: 13px; transform: rotate(-90deg); }
1187
+ .cw-model-back {
1188
+ width: 100%; height: 26px; margin-bottom: 4px; padding: 0 7px;
1189
+ border: 0; border-radius: 6px; background: transparent;
1190
+ color: var(--cw-text-muted) !important; text-align: left; cursor: pointer;
1191
+ font-size: 11px;
1192
+ }
1193
+ .cw-model-back:hover { background: var(--cw-surface-hover); color: var(--cw-text) !important; }
1194
+ .cw-model-option {
1195
+ width: 100%; min-height: 32px; border: 0; border-radius: 7px;
1196
+ background: transparent; color: var(--cw-text-secondary) !important;
1197
+ padding: 6px 8px; display: flex; align-items: center; gap: 8px;
1198
+ text-align: left; font-size: 13px; cursor: pointer;
1199
+ }
1200
+ .cw-model-option:hover { background: var(--cw-surface-hover); color: var(--cw-text) !important; }
1201
+ .cw-model-option > .cw-icon {
1202
+ width: 14px; height: 14px; color: var(--cw-text-muted); flex: 0 0 auto;
1203
+ }
1204
+ .cw-model-option[aria-checked="true"] { color: var(--cw-text) !important; }
1205
+ .cw-model-option-label {
1206
+ min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1207
+ }
1208
+ .cw-model-option-check { color: var(--cw-accent); font-size: 12px; }
1209
+ .cw-model-efforts {
1210
+ margin-top: 4px; padding: 4px 8px 8px;
1211
+ border-top: 1px solid var(--cw-border);
1212
+ }
1213
+ .cw-effort-options { display: flex; flex-wrap: wrap; gap: 5px; }
1214
+ .cw-effort-option {
1215
+ min-width: 48px; height: 26px; padding: 0 9px;
1216
+ border: 1px solid var(--cw-border); border-radius: 7px;
1217
+ background: transparent; color: var(--cw-text-secondary) !important;
1218
+ font-size: 12px; cursor: pointer;
1219
+ }
1220
+ .cw-effort-option:hover { background: var(--cw-surface-hover); color: var(--cw-text) !important; }
1221
+ .cw-effort-option[aria-checked="true"] {
1222
+ border-color: var(--cw-accent);
1223
+ background: color-mix(in srgb, var(--cw-accent) 12%, transparent);
1224
+ color: var(--cw-text) !important;
1225
+ }
1226
+ .cw-model-empty, .cw-model-error {
1227
+ padding: 10px 8px; color: var(--cw-text-muted); font-size: 12px; line-height: 1.5;
1228
+ }
1229
+ .cw-model-error { color: var(--cw-danger); }
1230
+ [data-workbench-theme] .kiGsTW_header:has([data-conversation-tabs]) {
1231
+ min-height: 0;
1232
+ padding-bottom: 7px;
1233
+ border-bottom-color: var(--cw-border);
1234
+ }
1235
+ [data-workbench-theme] .kiGsTW_header:has([data-conversation-tabs]) .kiGsTW_titleRow {
1236
+ min-height: 34px;
1237
+ }
1238
+ [data-workbench-theme] .kiGsTW_tabs {
1239
+ align-items: center;
1240
+ gap: 4px;
1241
+ margin-top: 3px;
1242
+ -webkit-app-region: no-drag;
1243
+ }
1244
+ [data-workbench-theme] [data-conversation-tabs] {
1245
+ align-items: center;
1246
+ gap: 2px;
1247
+ min-height: 0;
1248
+ padding: 2px;
1249
+ border: 1px solid var(--cw-border);
1250
+ border-radius: 7px;
1251
+ background: var(--cw-tabs-bg);
1252
+ }
1253
+ [data-workbench-theme] [data-conversation-tabs] [role="tab"] {
1254
+ min-height: 23px;
1255
+ margin: 0;
1256
+ padding: 3px 9px;
1257
+ border-radius: 4px;
1258
+ color: var(--cw-tabs-text);
1259
+ font-size: 12px;
1260
+ line-height: 17px;
1261
+ }
1262
+ [data-workbench-theme] [data-conversation-tabs] [role="tab"]::after { content: none; }
1263
+ [data-workbench-theme] [data-conversation-tabs] [role="tab"]:hover,
1264
+ [data-workbench-theme] .kiGsTW_tabs .kiGsTW_viewsMore:hover {
1265
+ background: var(--cw-tabs-hover);
1266
+ color: var(--cw-text);
1267
+ }
1268
+ [data-workbench-theme] [data-conversation-tabs] [role="tab"][aria-selected="true"],
1269
+ [data-workbench-theme] .kiGsTW_tabs .kiGsTW_viewsMore:is(.kiGsTW_viewsMoreActive, [aria-expanded="true"]) {
1270
+ background: var(--cw-tabs-active);
1271
+ color: var(--cw-text);
1272
+ }
1273
+ [data-workbench-theme] .kiGsTW_tabs .kiGsTW_viewsMore {
1274
+ width: 26px;
1275
+ height: 26px;
1276
+ margin: 0;
1277
+ border-radius: 6px;
1278
+ color: var(--cw-tabs-text);
1279
+ }
1280
+ [data-workbench-theme] .kiGsTW_tabs :is([role="tab"], .kiGsTW_viewsMore):focus-visible {
1281
+ outline: 1px solid var(--cw-accent);
1282
+ outline-offset: 1px;
1283
+ }
1284
+ @supports (grid-template-columns: subgrid) {
1285
+ [data-workbench-theme] .kiGsTW_root[data-phase="active"] {
1286
+ container: cw-conversation / inline-size;
1287
+ }
1288
+ @container cw-conversation (min-width: 820px) {
1289
+ /* Keep the native tab bar wide enough for its own two-tab measurement. */
1290
+ [data-workbench-theme] .kiGsTW_header:has([data-conversation-tabs]) {
1291
+ grid-template-columns: auto minmax(0, 1fr) 292px max-content max-content;
1292
+ align-items: center;
1293
+ padding-block: 5px;
1294
+ }
1295
+ [data-workbench-theme] .kiGsTW_header:has([data-conversation-tabs]) .kiGsTW_titleRow {
1296
+ grid-area: 1 / 2 / 2 / -1;
1297
+ display: grid;
1298
+ grid-template-columns: subgrid;
1299
+ container-type: normal;
1300
+ }
1301
+ [data-workbench-theme] .kiGsTW_header:has([data-conversation-tabs]) .kiGsTW_titleCluster {
1302
+ grid-area: 1 / 1;
1303
+ }
1304
+ [data-workbench-theme] .kiGsTW_header:has([data-conversation-tabs]) .kiGsTW_headerUtilities {
1305
+ grid-area: 1 / 3;
1306
+ margin-inline-start: 0;
1307
+ gap: 5px;
1308
+ }
1309
+ [data-workbench-theme] .kiGsTW_header:has([data-conversation-tabs]) .kiGsTW_headerCorner {
1310
+ grid-area: 1 / 4;
1311
+ }
1312
+ [data-workbench-theme] .kiGsTW_header .kiGsTW_tabs {
1313
+ grid-area: 1 / 3;
1314
+ justify-content: center;
1315
+ flex-wrap: nowrap;
1316
+ margin: 0;
1317
+ padding: 0;
1318
+ }
1319
+ }
1320
+ }
1321
+ [data-workbench-theme] [data-message-bubble] { border-radius: 14px; }
1322
+ .cw-header-actions { display: flex; align-items: center; gap: 1px; }
1323
+ .cw-context-button {
1324
+ width: 28px; height: 28px; padding: 0; border: 0; border-radius: 7px;
1325
+ background: transparent; color: var(--cw-text-secondary);
1326
+ display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
1327
+ opacity: 0.9;
1328
+ }
1329
+ .cw-context-button .cw-icon { width: 15px; height: 15px; }
1330
+ .cw-context-button:hover,
1331
+ .cw-context-button[aria-pressed="true"] {
1332
+ background: var(--cw-surface-hover);
1333
+ color: var(--cw-text); opacity: 1;
1334
+ }
1335
+ .cw-context-button[aria-pressed="true"] { background: var(--cw-context-active); }
1336
+ [data-workbench-theme] [data-sidebar-right-panel] {
1337
+ --dsw-alias-bg-base: var(--cw-panel);
1338
+ --dsw-alias-bg-layer-1: var(--cw-panel-layer);
1339
+ --dsw-alias-bg-layer-2: var(--cw-panel-hover);
1340
+ }
1341
+ [data-workbench-theme] [data-sidebar-right-panel] [role="tab"]:has(.Dv5QdW_tabTitle) {
1342
+ border-radius: 7px;
1343
+ }
1344
+ [data-workbench-theme] [data-sidebar-right-panel] [role="tab"][aria-selected="true"]:has(.Dv5QdW_tabTitle) {
1345
+ background-color: var(--cw-panel-selected);
1346
+ }
1347
+ .cw-context {
1348
+ height: 100%; overflow-y: auto; padding: 3px clamp(12px, 5%, 22px) 24px;
1349
+ color: var(--cw-text); background: var(--cw-panel);
1350
+ font-size: 13px; line-height: 1.45;
1351
+ }
1352
+ .cw-context-section { padding: 15px 0; }
1353
+ .cw-context-section + .cw-context-section { border-top: 1px solid var(--cw-border); }
1354
+ .cw-context-heading {
1355
+ margin: 0 0 11px; font-size: 13px; line-height: 18px; font-weight: 600;
1356
+ }
1357
+ .cw-output-list { list-style: none; margin: 0; padding: 0; }
1358
+ .cw-output-row {
1359
+ width: 100%; min-height: 30px; padding: 4px 6px;
1360
+ border: 0; border-radius: 6px; background: transparent;
1361
+ display: flex; align-items: center; gap: 9px;
1362
+ text-align: left; cursor: pointer;
1363
+ }
1364
+ .cw-output-row:hover { background: var(--cw-surface-hover); }
1365
+ .cw-output-icon {
1366
+ width: 30px; height: 30px; flex: none; display: grid; place-items: center;
1367
+ border-radius: 6px; overflow: hidden; background: var(--cw-surface);
1368
+ color: var(--cw-text-muted);
1369
+ }
1370
+ .cw-output-icon img { display: block; width: 100%; height: 100%; object-fit: cover; }
1371
+ .cw-output-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1372
+ .cw-output-type { margin-inline-start: auto; flex: none; color: var(--cw-text-muted); font-size: 10px; }
1373
+ .cw-context-environment {
1374
+ display: flex; align-items: center; gap: 10px; min-width: 0; padding: 5px 0;
1375
+ }
1376
+ .cw-context-environment .cw-icon { color: var(--cw-sidebar-muted); }
1377
+ .cw-context-environment-text {
1378
+ display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 7px; min-width: 0;
1379
+ }
1380
+ .cw-context-environment-text > span {
1381
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;
1382
+ }
1383
+ .cw-context-row {
1384
+ display: flex; align-items: baseline; justify-content: space-between;
1385
+ gap: 12px; padding: 4px 0;
1386
+ }
1387
+ .cw-context-label { flex: 0 0 auto; color: var(--cw-text-secondary); }
1388
+ .cw-context-value {
1389
+ min-width: 0; color: var(--cw-text-secondary);
1390
+ text-align: right; overflow-wrap: anywhere;
1391
+ }
1392
+ .cw-metric-head {
1393
+ display: flex; flex-wrap: wrap; justify-content: space-between;
1394
+ align-items: baseline; gap: 2px 8px; font-variant-numeric: tabular-nums;
1395
+ }
1396
+ .cw-metric-head > :last-child { color: var(--cw-text-secondary); }
1397
+ .cw-metric-track {
1398
+ display: flex; height: 4px; margin: 6px 0 9px; overflow: hidden;
1399
+ border-radius: 999px; background: var(--cw-metric-track);
1400
+ }
1401
+ .cw-metric-track > span {
1402
+ display: block; flex: 0 0 auto; height: 100%; background: var(--cw-metric, var(--cw-metric-blue));
1403
+ }
1404
+ .cw-root [data-metric="messages"] { --cw-metric: var(--cw-metric-blue); }
1405
+ .cw-root :is([data-metric="tools"], [data-metric="output"]) { --cw-metric: var(--cw-metric-orange); }
1406
+ .cw-root :is([data-metric="system"], [data-metric="write"]) { --cw-metric: var(--cw-metric-gold); }
1407
+ .cw-root [data-metric="free"] { --cw-metric: var(--cw-metric-free); }
1408
+ .cw-root [data-metric="read"] { --cw-metric: var(--cw-metric-teal); }
1409
+ .cw-context-legend {
1410
+ display: grid; grid-template-columns: minmax(0, 1fr) auto auto;
1411
+ align-items: center; gap: 6px 12px; margin: 11px 0 0;
1412
+ }
1413
+ .cw-context-legend dt {
1414
+ display: flex; align-items: center; gap: 8px; min-width: 0; color: var(--cw-text-secondary);
1415
+ }
1416
+ .cw-context-legend dt::before, .cw-usage-breakdown dt::before {
1417
+ content: ""; width: 8px; height: 8px; border-radius: 2px;
1418
+ flex: 0 0 auto; background: var(--cw-metric);
1419
+ }
1420
+ .cw-context-legend dt span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1421
+ .cw-context-legend dd {
1422
+ margin: 0; text-align: right; white-space: nowrap;
1423
+ font-variant-numeric: tabular-nums;
1424
+ }
1425
+ .cw-context-legend dd:nth-of-type(odd) { color: var(--cw-text-secondary); }
1426
+ .cw-context-subtle { color: var(--cw-text-muted); font-size: 11px; }
1427
+ .cw-context-note { margin: 9px 0 0; }
1428
+ .cw-context-details > summary {
1429
+ display: flex; align-items: center; gap: 7px; cursor: pointer; list-style: none;
1430
+ font-weight: 550; border-radius: 5px;
1431
+ }
1432
+ .cw-context-details > summary::-webkit-details-marker { display: none; }
1433
+ .cw-context-details > summary .cw-icon { width: 13px; height: 13px; transform: rotate(-90deg); }
1434
+ .cw-context-details[open] > summary { margin-bottom: 7px; }
1435
+ .cw-context-details[open] > summary .cw-icon { transform: none; }
1436
+ .cw-usage-trigger {
1437
+ flex: 0 0 auto; width: 27px; height: 25px; margin-left: 2px;
1438
+ color: var(--cw-metric-blue) !important; border-radius: 7px;
1439
+ }
1440
+ .cw-usage-trigger[aria-expanded="true"] { background: var(--cw-surface-hover); }
1441
+ .cw-usage-trigger[data-level="high"] { color: var(--cw-danger) !important; }
1442
+ .cw-usage-trigger[data-level="warn"] { color: var(--cw-metric-gold) !important; }
1443
+ .cw-usage-ring { width: 16px; height: 16px; }
1444
+ .cw-usage-popover {
1445
+ position: fixed; z-index: 1000; overflow-y: auto; padding: 10px 15px 11px;
1446
+ scrollbar-width: none; overscroll-behavior-y: contain;
1447
+ border: 1px solid var(--cw-border-strong); border-radius: 12px;
1448
+ background: var(--cw-popover); box-shadow: 0 9px 28px var(--cw-shadow);
1449
+ font-size: 12px; line-height: 1.45;
1450
+ }
1451
+ .cw-usage-popover::-webkit-scrollbar { display: none; }
1452
+ .cw-usage-popover:focus { outline: none; }
1453
+ .cw-usage-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 3px 8px; margin-bottom: 9px; }
1454
+ .cw-usage-header h2 { margin: 0; font-size: 13px; font-weight: 600; }
1455
+ .cw-usage-link {
1456
+ border: 0; padding: 2px 0; border-radius: 3px; background: none;
1457
+ color: var(--cw-text-secondary) !important; cursor: pointer;
1458
+ }
1459
+ .cw-usage-link:hover { color: var(--cw-text) !important; }
1460
+ .cw-provider-usage {
1461
+ border-bottom: 1px solid var(--cw-border-strong); padding-bottom: 8px; margin-bottom: 8px;
1462
+ }
1463
+ .cw-provider-usage h3 { margin: 0; font-size: 12px; font-weight: 600; }
1464
+ .cw-provider-usage h3 span { color: var(--cw-text-secondary); font-weight: 400; }
1465
+ .cw-provider-usage h3 strong { color: var(--cw-text); font-weight: 600; }
1466
+ .cw-provider-usage > section + section {
1467
+ border-top: 1px solid var(--cw-border-strong); margin-top: 8px; padding-top: 8px;
1468
+ }
1469
+ .cw-provider-window { margin-top: 5px; }
1470
+ .cw-provider-window .cw-metric-track { margin: 3px 0 0; }
1471
+ .cw-provider-window [data-metric="quota-high"] { --cw-metric: var(--cw-danger); }
1472
+ .cw-provider-balance { margin-top: 5px; }
1473
+ .cw-provider-balance strong { color: var(--cw-text); font-weight: 600; }
1474
+ .cw-provider-usage .cw-context-subtle { margin: 3px 0 0; }
1475
+ .cw-usage-breakdown {
1476
+ border-top: 1px solid var(--cw-border-strong); margin-top: 8px; padding-top: 7px;
1477
+ }
1478
+ .cw-usage-breakdown .cw-metric-head { margin-bottom: 4px; }
1479
+ .cw-usage-breakdown dl { margin: 4px 0 0; }
1480
+ .cw-usage-breakdown .cw-context-row { padding: 2px 0; }
1481
+ .cw-usage-breakdown dt { display: flex; align-items: center; gap: 8px; }
1482
+ .cw-usage-breakdown dd { margin: 0; font-variant-numeric: tabular-nums; }
1483
+ .cw-composer-row {
1484
+ display: flex; align-items: center; gap: 3px; min-height: 28px;
1485
+ }
1486
+ .cw-composer-input {
1487
+ flex: 1; min-width: 0; display: block; min-height: 28px; max-height: 160px;
1488
+ border: 0; outline: none !important; resize: none; overflow-y: auto;
1489
+ background: transparent; color: var(--cw-text); padding: 4px;
1490
+ line-height: max(20px, 1.4em); field-sizing: content;
1491
+ }
1492
+ .cw-composer-input:placeholder-shown { field-sizing: fixed; }
1493
+ .cw-composer-input::placeholder { color: var(--cw-text-muted); }
1494
+ .cw-composer-input:disabled { cursor: not-allowed; opacity: 0.65; }
1495
+ .cw-composer-actions {
1496
+ display: flex; align-items: center; gap: 6px;
1497
+ }
1498
+ .cw-composer-voice {
1499
+ display: flex; align-items: center; min-width: 0;
1500
+ }
1501
+ .cw-composer-voice:not(:has(button)) { display: none; }
1502
+ .cw-composer-row:has(.cw-composer-voice [data-voice-activity]) { flex-wrap: wrap; }
1503
+ .cw-composer-voice:has([data-voice-activity]) { order: 1; flex: 1 0 100%; padding: 3px 0; }
1504
+ .cw-composer-voice [data-voice-activity] { width: 100%; }
1505
+ .cw-composer-state { color: var(--cw-text-muted); font-size: 12px; margin-left: 8px; }
1506
+ .cw-attach-button { width: 26px; height: 26px; }
1507
+ .cw-composer-images {
1508
+ display: flex; flex-wrap: wrap; gap: 5px; padding: 1px 4px 4px;
1509
+ }
1510
+ .cw-composer-image {
1511
+ position: relative; width: 44px; height: 44px; border-radius: 7px;
1512
+ overflow: hidden; border: 1px solid var(--cw-border-strong);
1513
+ background: var(--cw-code);
1514
+ }
1515
+ .cw-composer-image img { width: 100%; height: 100%; object-fit: cover; display: block; }
1516
+ .cw-composer-image button {
1517
+ position: absolute; top: 3px; right: 3px; width: 20px; height: 20px;
1518
+ border: 0; border-radius: 50%; padding: 0; cursor: pointer;
1519
+ background: rgba(20, 19, 18, 0.82); color: #fffaf6;
1520
+ display: inline-flex; align-items: center; justify-content: center;
1521
+ }
1522
+ .cw-composer-image button .cw-icon { width: 12px; height: 12px; }
1523
+ .cw-send-button {
1524
+ width: 26px; min-width: 26px; height: 26px; padding: 0; border: 0;
1525
+ border-radius: 50%; background: var(--cw-accent); color: var(--cw-on-accent) !important;
1526
+ display: inline-flex; align-items: center; justify-content: center;
1527
+ cursor: pointer;
1528
+ }
1529
+ .cw-send-button:hover { background: var(--cw-accent-hover); }
1530
+ .cw-send-button:disabled { background: var(--cw-surface-hover); color: var(--cw-text-muted) !important; cursor: default; }
1531
+ .cw-stop-button {
1532
+ background: transparent; border: 1px solid var(--cw-border-strong);
1533
+ color: var(--cw-text) !important;
1534
+ }
1535
+ .cw-stop-button:hover { background: var(--cw-surface-hover); }
1536
+ .cw-composer-error { color: var(--cw-danger); font-size: 12px; padding: 6px 4px 0; }
1537
+ .cw-workspace-button {
1538
+ border: 1px solid var(--cw-border-strong); border-radius: 9px;
1539
+ background: transparent; padding: 7px 12px; cursor: pointer;
1540
+ }
1541
+ .cw-workspace-button:hover { background: var(--cw-surface-hover); }
1542
+ @media (max-width: 640px) {
1543
+ .cw-message { padding-left: 14px; padding-right: 14px; }
1544
+ .cw-user-bubble { max-width: 92%; }
1545
+ }
1546
+ `;
1547
+
1548
+ class WorkbenchBoundary extends React.Component {
1549
+ constructor(props) {
1550
+ super(props);
1551
+ this.state = { failed: false };
1552
+ }
1553
+
1554
+ static getDerivedStateFromError() {
1555
+ return { failed: true };
1556
+ }
1557
+
1558
+ componentDidCatch(error) {
1559
+ reportDiagnostic(`slot render failed: ${error?.message || error}`);
1560
+ }
1561
+
1562
+ render() {
1563
+ if (this.state.failed) return null;
1564
+ return this.props.children;
1565
+ }
1566
+ }
1567
+
1568
+ function withBoundary(Component) {
1569
+ return function ClaudeWorkbenchStyled(props) {
1570
+ return h(WorkbenchBoundary, null, h(Component, props));
1571
+ };
1572
+ }
1573
+
1574
+ function translate(props, key) {
1575
+ const t = props.t || runtime.t || ((name) => en[name] || name);
1576
+ return t(key);
1577
+ }
1578
+
1579
+ const IMAGE_ROWS = [
1580
+ ['z-image', 'Z-Image Turbo', 'imageZ'], ['cogview', 'CogView-3-Flash', 'imageCog'],
1581
+ ['pollinations', 'Pollinations', 'imagePoll'], ['comfy', 'ComfyUI + Comfy MCP', 'imageComfy'],
1582
+ ];
1583
+
1584
+ function ImageGenerationSettings(props) {
1585
+ const t = (key) => translate(props, key);
1586
+ const [state, setState] = React.useState(null);
1587
+ const [keys, setKeys] = React.useState({});
1588
+ const [pending, setPending] = React.useState('');
1589
+ const [error, setError] = React.useState(false);
1590
+ const requestRef = React.useRef(null);
1591
+ const installing = state?.providers?.comfy?.setup?.state === 'installing';
1592
+ const loading = state === null;
1593
+
1594
+ async function request(signal, input) {
1595
+ const response = await fetch(new URL('api/workbench/imageSettings', document.baseURI), {
1596
+ cache: 'no-store', signal,
1597
+ ...(input ? { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(input) } : {}),
1598
+ });
1599
+ if (!response.ok) throw new Error('Image settings request failed');
1600
+ return response.json();
1601
+ }
1602
+
1603
+ React.useEffect(() => () => requestRef.current?.abort(), []);
1604
+ React.useEffect(() => {
1605
+ if (pending || (!loading && !installing)) return;
1606
+ const controller = new AbortController();
1607
+ let timer;
1608
+ async function refresh() {
1609
+ try {
1610
+ const result = await request(controller.signal);
1611
+ if (controller.signal.aborted) return;
1612
+ setState(result);
1613
+ setError(false);
1614
+ if (result.providers?.comfy?.setup?.state === 'installing') timer = setTimeout(refresh, 1500);
1615
+ } catch {
1616
+ if (controller.signal.aborted) return;
1617
+ setError(true);
1618
+ if (installing) timer = setTimeout(refresh, 3000);
1619
+ else setState({ supported: false });
1620
+ }
1621
+ }
1622
+ if (loading) void refresh();
1623
+ else timer = setTimeout(refresh, 1500);
1624
+ return () => { controller.abort(); clearTimeout(timer); };
1625
+ }, [loading, installing, pending]);
1626
+
1627
+ async function change(input) {
1628
+ if (requestRef.current) return;
1629
+ const controller = new AbortController();
1630
+ requestRef.current = controller;
1631
+ setPending(input.provider || 'comfy');
1632
+ setError(false);
1633
+ try {
1634
+ const result = await request(controller.signal, input);
1635
+ if (controller.signal.aborted) return;
1636
+ setState(result);
1637
+ if (input.action === 'saveKey') setKeys((old) => ({ ...old, [input.provider]: '' }));
1638
+ } catch {
1639
+ if (!controller.signal.aborted) setError(true);
1640
+ } finally {
1641
+ requestRef.current = null;
1642
+ if (!controller.signal.aborted) setPending('');
1643
+ }
1644
+ }
1645
+
1646
+ function keyControls(provider, title, row) {
1647
+ const key = keys[provider] || '';
1648
+ const save = () => change({ action: 'saveKey', provider, key });
1649
+ return h('div', { className: 'cw-image-actions' },
1650
+ h('p', null, t(row.configured ? 'imageKeyConfigured' : 'imageKeyMissing')),
1651
+ row.writable ? h(React.Fragment, null,
1652
+ h('input', { type: 'password', value: key, placeholder: t('imageKey'), autoComplete: 'off',
1653
+ 'aria-label': `${title} ${t('imageKey')}`, disabled: !!pending, maxLength: 2048,
1654
+ onChange: (event) => setKeys((old) => ({ ...old, [provider]: event.target.value })),
1655
+ onKeyDown: (event) => { if (event.key === 'Enter' && key.trim()) { event.preventDefault(); void save(); } },
1656
+ }),
1657
+ h('button', { type: 'button', disabled: !!pending || !key.trim(), onClick: save }, t('imageSaveKey')),
1658
+ row.stored && h('button', { type: 'button', disabled: !!pending,
1659
+ onClick: () => change({ action: 'saveKey', provider, key: '' }) }, t('imageRemoveKey')),
1660
+ ) : !row.configured && h('p', null, t('imageKeyUnavailable')),
1661
+ );
1662
+ }
1663
+
1664
+ function comfyControls({ state: phase, stage, detail }) {
1665
+ const phaseKey = { ready: 'imageReady', missing: 'imageMissing', unsupported: 'imageUnsupported', error: 'imageInstallError' }[phase];
1666
+ const stageKey = `imageStage_${stage}`;
1667
+ const label = phase === 'installing' ? t(Object.hasOwn(en, stageKey) ? stageKey : 'imageInstalling') : phaseKey ? t(phaseKey) : '';
1668
+ return h('div', { className: 'cw-image-actions' },
1669
+ h('p', { role: 'status' }, label),
1670
+ ['unsupported', 'error', 'missing'].includes(phase) && typeof detail === 'string' && detail
1671
+ ? h('p', null, detail.slice(0, 500)) : null,
1672
+ ['missing', 'error'].includes(phase) && h('button', { type: 'button', disabled: !!pending,
1673
+ onClick: () => change({ action: 'installComfy' }) }, t(phase === 'error' ? 'imageRetry' : 'imageInstall')),
1674
+ phase === 'installing' && h('button', { type: 'button', disabled: !!pending,
1675
+ onClick: () => change({ action: 'cancelComfy' }) }, t('imageCancel')),
1676
+ );
1677
+ }
1678
+
1679
+ return h('section', { className: 'cw-image-settings', 'aria-labelledby': 'cw-image-heading' },
1680
+ h('h2', { id: 'cw-image-heading' }, t('imageSettings')),
1681
+ h('p', null, t('imageSettingsIntro')),
1682
+ loading ? h('p', { role: 'status' }, t('imageSettingsLoad'))
1683
+ : !state?.supported && !error ? h('p', null, t('imageSettingsUnavailable')) : null,
1684
+ error && h('p', { className: 'cw-image-error', role: 'alert' }, t(state?.supported ? 'imageSettingsError' : 'imageSettingsReadError')),
1685
+ state?.supported && IMAGE_ROWS.map(([provider, title, description]) => {
1686
+ const row = state.providers[provider];
1687
+ const detailsId = `cw-image-${provider}-details`;
1688
+ return h('div', { key: provider, className: 'cw-image-row' },
1689
+ h('div', { id: detailsId }, h('strong', null, title), h('p', null, t(description)),
1690
+ provider === 'comfy' && h('p', null, t('imageComfyRequirements'))),
1691
+ h('button', { type: 'button', className: 'cw-image-switch', role: 'switch',
1692
+ 'aria-label': title, 'aria-describedby': detailsId, 'aria-checked': row.enabled,
1693
+ disabled: !!pending, onClick: () => {
1694
+ if (row.enabled) setKeys((old) => ({ ...old, [provider]: '' }));
1695
+ void change({ action: 'toggle', provider, enabled: !row.enabled });
1696
+ },
1697
+ }),
1698
+ row.enabled && (provider === 'comfy' ? comfyControls(row.setup)
1699
+ : Object.hasOwn(row, 'configured') ? keyControls(provider, title, row) : null),
1700
+ );
1701
+ }),
1702
+ );
1703
+ }
1704
+
1705
+ function commandQuery(text) {
1706
+ if (typeof text !== 'string' || !text.startsWith('/')) return null;
1707
+ if (/\s/u.test(text)) return null;
1708
+ return text.slice(1);
1709
+ }
1710
+
1711
+ function filterCommandRows(rows, query) {
1712
+ if (!Array.isArray(rows)) return [];
1713
+ const needle = String(query || '').trim().toLowerCase();
1714
+ return rows.filter((row) => {
1715
+ if (!row || typeof row.name !== 'string' || row.name.trim() === '') return false;
1716
+ if (!needle) return true;
1717
+ const fields = [row.name, row.label, row.description];
1718
+ return fields.some((field) => typeof field === 'string' && field.toLowerCase().includes(needle));
1719
+ });
1720
+ }
1721
+
1722
+ function commandRowsFromDescriptors(descriptors) {
1723
+ if (!Array.isArray(descriptors)) return [];
1724
+ return descriptors.flatMap((descriptor) => {
1725
+ if (!descriptor || typeof descriptor.name !== 'string' || descriptor.name.trim() === '') return [];
1726
+ return [{
1727
+ name: descriptor.name,
1728
+ label: descriptor.name,
1729
+ description: typeof descriptor.description === 'string' ? descriptor.description : '',
1730
+ hasInput: descriptor.input !== undefined,
1731
+ hint: typeof descriptor.input?.hint === 'string' ? descriptor.input.hint : '',
1732
+ }];
1733
+ });
1734
+ }
1735
+
1736
+ function commandInsertion(row) {
1737
+ return `/${row.name}${row.hasInput ? ' ' : ''}`;
1738
+ }
1739
+
1740
+ function formatBytes(value) {
1741
+ const bytes = Number(value);
1742
+ if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
1743
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
1744
+ const exponent = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
1745
+ const scaled = bytes / (1024 ** exponent);
1746
+ const text = exponent === 0 ? String(Math.round(scaled)) : scaled.toFixed(1);
1747
+ return `${text} ${units[exponent]}`;
1748
+ }
1749
+
1750
+ function mentionQuery(text, caret) {
1751
+ const before = text.slice(0, caret);
1752
+ const match = /(?:^|\s)(@(?:"([^"\r\n]*)|([^\s@"]*)))$/u.exec(before);
1753
+ if (!match) return null;
1754
+ const quoted = match[2] !== undefined;
1755
+ const after = text.slice(caret).match(quoted ? /^[^"\r\n]*"?/u : /^[^\s@"]*/u);
1756
+ return { query: match[2] ?? match[3], quoted, start: before.length - match[1].length, end: caret + after[0].length };
1757
+ }
1758
+
1759
+ function referenceRows(files, sessions, quoted) {
1760
+ const fileRows = files.flatMap(({ path, kind }) => {
1761
+ if (typeof path !== 'string' || !path || /[\u0000-\u001f\u007f-\u009f"]/u.test(path)) return [];
1762
+ const directory = kind === 'directory';
1763
+ const value = directory ? `${path}/` : path;
1764
+ const mention = quoted || /\s/u.test(value) ? `@"${value}"` : `@${value}`;
1765
+ return [{ key: `file:${path}`, label: value, icon: directory ? 'folder' : 'file', mention }];
1766
+ });
1767
+ const sessionRows = sessions.flatMap(({ sessionId, displayTitle, label, mention }) => (
1768
+ typeof mention === 'string' && mention.startsWith('@')
1769
+ ? [{ key: `session:${sessionId}`, label: displayTitle || label || mention, icon: 'chat', mention }]
1770
+ : []
1771
+ ));
1772
+ return [...fileRows, ...sessionRows];
1773
+ }
1774
+
1775
+ function submitMode(session, preference, alternate = false) {
1776
+ if (!session?.running || (session.subagent && session.subagent.address?.mode !== 'continuable')) return 'queue';
1777
+ return (preference === 'steer') !== alternate ? 'steer' : 'queue';
1778
+ }
1779
+
1780
+ const subscribeComposerSettings = (listener) => runtime.composerSettings?.subscribe(listener) ?? (() => {});
1781
+ const readBusyEnter = () => runtime.composerSettings?.getSnapshot().value?.busyEnter ?? 'queue';
1782
+
1783
+ function isDangerousPermission(value) {
1784
+ return value === 'full-access' || value === 'danger-full-access' || value === 'auto';
1785
+ }
1786
+
1787
+ function summarizeToolCall(data) {
1788
+ const root = data?.root || data;
1789
+ if (!root || typeof root !== 'object') return null;
1790
+ const callId = root.callId || root.call?.id || '';
1791
+ const name = root.name || root.call?.name || 'tool';
1792
+ const status = root.kind === 'tool-result'
1793
+ ? (root.isError ? 'error' : 'done')
1794
+ : (root.phase === 'preparing' ? 'preparing' : 'running');
1795
+ let detail = '';
1796
+ const argsRaw = root.argsRaw ?? root.call?.argsRaw;
1797
+ if (typeof argsRaw === 'string') {
1798
+ try {
1799
+ const parsed = JSON.parse(argsRaw);
1800
+ if (parsed && typeof parsed === 'object') {
1801
+ for (const key of ['description', 'command', 'path', 'file_path', 'query', 'pattern']) {
1802
+ if (typeof parsed[key] === 'string' && parsed[key].trim()) {
1803
+ detail = parsed[key].trim();
1804
+ break;
1805
+ }
1806
+ }
1807
+ }
1808
+ } catch {
1809
+ detail = '';
1810
+ }
1811
+ }
1812
+ const subCalls = Array.isArray(root.subCalls) ? root.subCalls.length : 0;
1813
+ return { callId: String(callId), name, status, detail, subCalls };
1814
+ }
1815
+
1816
+ function changedToolPaths(tool, args) {
1817
+ if (tool.name === 'apply_patch') {
1818
+ const paths = [];
1819
+ for (const part of tool.content ?? []) {
1820
+ if (part.type !== 'text' || typeof part.text !== 'string' ||
1821
+ !part.text.startsWith('Success. Updated the following files:\n')) continue;
1822
+ for (const match of part.text.matchAll(/^[AMD] (.+)$/gmu)) paths.push(match[1]);
1823
+ }
1824
+ return paths.length ? paths : (tool.meta?.diffs ?? []).map(({ path }) => path);
1825
+ }
1826
+ const editorWrite = tool.name === 'str_replace_editor' &&
1827
+ ['create', 'insert', 'str_replace', 'undo_edit'].includes(args('command'));
1828
+ if (tool.name === 'write' || tool.name === 'edit' || editorWrite) {
1829
+ return [tool.meta?.path ?? args('file_path') ?? args('path')];
1830
+ }
1831
+ return [];
1832
+ }
1833
+
1834
+ function summarizeWorkGroup(tools, t) {
1835
+ const seen = new Set();
1836
+ const reads = new Set();
1837
+ const changes = new Set();
1838
+ let commands = 0;
1839
+ let failures = 0;
1840
+ const pathKey = (path) => path.replace(/\\/g, '/').replace(/^(?:\.\/)+/, '');
1841
+
1842
+ function visit(tool) {
1843
+ if (!tool || seen.has(tool.callId)) return;
1844
+ seen.add(tool.callId);
1845
+ if (tool.kind === 'tool-result') {
1846
+ if (tool.isError) {
1847
+ failures += 1;
1848
+ } else {
1849
+ const args = (key) => tool.args?.value?.(key);
1850
+ const editorView = tool.name === 'str_replace_editor' && args('command') === 'view';
1851
+ if (tool.name === 'read' || tool.name === 'read_image' || editorView) {
1852
+ const path = tool.meta?.path ?? args('file_path') ?? args('path');
1853
+ if (typeof path === 'string' && path) reads.add(pathKey(path));
1854
+ }
1855
+ if (['bash', 'pwsh', 'exec_command'].includes(tool.name) ||
1856
+ (tool.name?.startsWith('terminal_') && (args('command') || args('cmd')))) commands += 1;
1857
+ for (const path of changedToolPaths(tool, args)) {
1858
+ if (typeof path === 'string' && path) changes.add(pathKey(path));
1859
+ }
1860
+ }
1861
+ }
1862
+ for (const child of tool.subCalls ?? []) visit(child);
1863
+ }
1864
+
1865
+ for (const tool of tools) visit(tool);
1866
+ const parts = [];
1867
+ if (reads.size) parts.push(t(reads.size === 1 ? 'workReadOne' : 'workReadMany').replace('{count}', reads.size));
1868
+ if (commands) parts.push(t(commands === 1 ? 'workCommandOne' : 'workCommandMany').replace('{count}', commands));
1869
+ if (changes.size) {
1870
+ const files = [...changes];
1871
+ const names = files.map((path) => path.split('/').pop());
1872
+ const labels = names.map((name, index) => names.indexOf(name) === names.lastIndexOf(name) ? name : files[index]);
1873
+ parts.push(t('workChanged').replace('{files}', () => labels.join(', ')));
1874
+ }
1875
+ if (parts.length && failures) parts.push(t('workFailed').replace('{count}', failures));
1876
+ return parts.join(' · ');
1877
+ }
1878
+
1879
+ function bindWorkSummaries(root, conversation, locale) {
1880
+ const chat = conversation.target('chat');
1881
+ const attribute = 'data-cw-work-summary';
1882
+ const summaries = new WeakMap();
1883
+ let decorated = new Set();
1884
+ let frame = null;
1885
+ let languageVersion = 0;
1886
+ function clear(button) {
1887
+ if (button.title === button.getAttribute(attribute)) button.removeAttribute('title');
1888
+ button.removeAttribute(attribute);
1889
+ }
1890
+ function decorate(button, summary, current) {
1891
+ if (!summary) return;
1892
+ if (button.getAttribute(attribute) !== summary) {
1893
+ button.setAttribute(attribute, summary);
1894
+ button.title = summary;
1895
+ }
1896
+ current.add(button);
1897
+ }
1898
+ function update() {
1899
+ frame = null;
1900
+ const nodes = chat.getSnapshot()?.nodes;
1901
+ const groups = conversation.snapshot.getSnapshot().views.grouped('chat');
1902
+ const current = new Set();
1903
+ const turnGroups = new Map();
1904
+ if (nodes && groups) {
1905
+ for (const group of root.querySelectorAll('[data-chat-group-key][data-step-process]:not([data-group-expanded-mode])')) {
1906
+ const data = groups.groupSource(group.dataset.chatGroupKey).getSnapshot();
1907
+ if (!data?.data.closed) continue;
1908
+ const button = group.querySelector(':scope > :first-child > button[data-process-activity]');
1909
+ if (!button) continue;
1910
+ const tools = data.members.map(({ key }) => nodes.get(key))
1911
+ .filter((node) => node?.kind === 'tool-call').map((node) => node.data.root);
1912
+ let cached = summaries.get(data.members);
1913
+ if (cached?.languageVersion !== languageVersion || cached.tools.length !== tools.length ||
1914
+ tools.some((tool, index) => tool !== cached.tools[index])) {
1915
+ cached = { tools, languageVersion, summary: summarizeWorkGroup(tools, (key) => translate({}, key)) };
1916
+ summaries.set(data.members, cached);
1917
+ }
1918
+ decorate(button, cached.summary, current);
1919
+ if (tools.length) {
1920
+ const turn = group.dataset.chatTurn;
1921
+ const groupsInTurn = turnGroups.get(turn) ?? [];
1922
+ groupsInTurn.push(cached);
1923
+ turnGroups.set(turn, groupsInTurn);
1924
+ }
1925
+ }
1926
+ for (const button of root.querySelectorAll('button[data-turn-process][aria-expanded="false"]')) {
1927
+ const groupsInTurn = turnGroups.get(button.dataset.turnProcess);
1928
+ if (!groupsInTurn) continue;
1929
+ const summary = groupsInTurn.length === 1 ? groupsInTurn[0].summary
1930
+ : summarizeWorkGroup(groupsInTurn.flatMap(({ tools }) => tools), (key) => translate({}, key));
1931
+ decorate(button, summary, current);
1932
+ }
1933
+ }
1934
+ for (const button of decorated) if (!current.has(button)) clear(button);
1935
+ decorated = current;
1936
+ }
1937
+ function schedule() {
1938
+ if (frame === null) frame = requestAnimationFrame(update);
1939
+ }
1940
+ const unsubscribeChat = conversation.snapshot.subscribe(schedule);
1941
+ const unsubscribeLocale = locale.subscribe(() => {
1942
+ languageVersion += 1;
1943
+ schedule();
1944
+ });
1945
+ const observer = new MutationObserver(schedule);
1946
+ observer.observe(root, {
1947
+ childList: true, subtree: true, attributes: true,
1948
+ attributeFilter: ['data-group-expanded-mode', 'aria-expanded'],
1949
+ });
1950
+ schedule();
1951
+ return () => {
1952
+ if (frame !== null) cancelAnimationFrame(frame);
1953
+ unsubscribeChat();
1954
+ unsubscribeLocale();
1955
+ observer.disconnect();
1956
+ for (const button of decorated) clear(button);
1957
+ };
1958
+ }
1959
+
1960
+ function formatTokenCount(value) {
1961
+ const count = Number(value);
1962
+ if (!Number.isFinite(count)) return '0';
1963
+ if (count < 1000) return String(Math.round(count));
1964
+ const scaled = (amount) => (amount >= 100 ? Math.round(amount) : Math.round(amount * 10) / 10);
1965
+ if (count < 1_000_000) return `${scaled(count / 1000)}K`;
1966
+ return `${scaled(count / 1_000_000)}M`;
1967
+ }
1968
+
1969
+ function contextPercent(pressure) {
1970
+ const used = pressure?.projectedTokens ?? pressure?.pressureTokens;
1971
+ const capacity = pressure?.contextWindow;
1972
+ if (!Number.isFinite(used) || !Number.isFinite(capacity) || capacity <= 0) return null;
1973
+ return Math.round((used / capacity) * 100);
1974
+ }
1975
+
1976
+ function contextMeter(pressure) {
1977
+ const percent = contextPercent(pressure);
1978
+ if (percent === null) return null;
1979
+ const used = pressure.projectedTokens ?? pressure.pressureTokens;
1980
+ const limit = pressure.contextWindow;
1981
+ return {
1982
+ used, limit, percent, fill: Math.max(0, Math.min(100, 100 * used / limit)),
1983
+ approximate: Number.isFinite(pressure.projectedTokens),
1984
+ };
1985
+ }
1986
+
1987
+ function contextComposition(breakdown, meter) {
1988
+ const categories = [
1989
+ ['messages', 'contextMessages', breakdown?.messageTokens],
1990
+ ['tools', 'contextTools', breakdown?.toolsTokens],
1991
+ ['system', 'contextSystemPrompt', breakdown?.systemTokens],
1992
+ ].filter(([, , tokens]) => Number.isFinite(tokens) && tokens > 0);
1993
+ const total = categories.reduce((sum, [, , tokens]) => sum + tokens, 0);
1994
+ return {
1995
+ categories,
1996
+ segments: meter && total ? categories.map(([id, , tokens]) => ({
1997
+ id, width: meter.fill * tokens / total,
1998
+ })) : null,
1999
+ };
2000
+ }
2001
+
2002
+ async function executeHostCommand(commands, sessionId, line) {
2003
+ const result = await commands.execute(sessionId, line, []);
2004
+ if (!result?.ok) {
2005
+ return {
2006
+ ok: false,
2007
+ error: result?.error ? `${result.error.code}: ${result.error.message}` : 'command.execute failed',
2008
+ };
2009
+ }
2010
+ if (result.value === undefined) return { ok: false, error: 'unmatched' };
2011
+ if (result.value.result?.kind === 'error') {
2012
+ return { ok: false, error: result.value.result.text || 'command failed' };
2013
+ }
2014
+ return { ok: true };
2015
+ }
2016
+
2017
+ function Icon({ name }) {
2018
+ const common = {
2019
+ className: 'cw-icon', viewBox: '0 0 24 24', fill: 'none',
2020
+ stroke: 'currentColor', strokeWidth: 1.7, strokeLinecap: 'round',
2021
+ strokeLinejoin: 'round', 'aria-hidden': true,
2022
+ };
2023
+ if (name === 'plus') return h('svg', common,
2024
+ h('path', { d: 'M12 5v14M5 12h14' }));
2025
+ if (name === 'search') return h('svg', common,
2026
+ h('circle', { cx: 10.8, cy: 10.8, r: 6.3 }), h('path', { d: 'm16 16 4 4' }));
2027
+ if (name === 'chat') return h('svg', common,
2028
+ h('path', { d: 'M5 5.5h14v10H9l-4 3v-13Z' }));
2029
+ if (name === 'stop') return h('svg', common,
2030
+ h('rect', { x: 7, y: 7, width: 10, height: 10, rx: 1.5, fill: 'currentColor', stroke: 'none' }));
2031
+ if (name === 'expand') return h('svg', common,
2032
+ h('path', { d: 'M8 5h11v14H8M4 12h10m-4-4 4 4-4 4' }));
2033
+ if (name === 'attach') return h('svg', common,
2034
+ h('path', { d: 'M9 12.5 15.5 6a3.2 3.2 0 0 1 4.5 4.5l-8 8a5 5 0 0 1-7-7l8-8' }),
2035
+ h('path', { d: 'M7.5 15.5 15 8' }));
2036
+ if (name === 'close') return h('svg', common,
2037
+ h('path', { d: 'm6 6 12 12M18 6 6 18' }));
2038
+ if (name === 'up') return h('svg', common,
2039
+ h('path', { d: 'M12 19V5m-6 6 6-6 6 6' }));
2040
+ if (name === 'chevron') return h('svg', common,
2041
+ h('path', { d: 'm7 10 5 5 5-5' }));
2042
+ if (name === 'back') return h('svg', common, h('path', { d: 'm15 4-8 8 8 8' }));
2043
+ if (name === 'forward') return h('svg', common, h('path', { d: 'm9 4 8 8-8 8' }));
2044
+ if (name === 'panel') return h('svg', common,
2045
+ h('rect', { x: 4, y: 5, width: 16, height: 14, rx: 2 }),
2046
+ h('path', { d: 'M14 5v14' }));
2047
+ if (name === 'info') return h('svg', common,
2048
+ h('circle', { cx: 12, cy: 12, r: 8 }),
2049
+ h('path', { d: 'M12 11v5M12 8h.01' }));
2050
+ if (name === 'context') return h('svg', common,
2051
+ h('path', { d: 'M9 6h11M9 12h11M9 18h11M4 6h.01M4 12h.01M4 18h.01' }));
2052
+ if (name === 'model') return h('svg', common,
2053
+ h('rect', { x: 5, y: 5, width: 14, height: 14, rx: 2 }),
2054
+ h('path', { d: 'M9 9h6v6H9zM9 2.5V5m6-2.5V5M9 19v2.5m6-2.5v2.5M2.5 9H5m-2.5 6H5m14-6h2.5M19 15h2.5' }));
2055
+ if (name === 'shield') return h('svg', common,
2056
+ h('path', { d: 'M12 4 5.5 6.5v5.2c0 4 2.7 6.8 6.5 8.3 3.8-1.5 6.5-4.3 6.5-8.3V6.5L12 4Z' }));
2057
+ if (name === 'panel') return h('svg', common,
2058
+ h('rect', { x: 3.5, y: 4.5, width: 17, height: 15, rx: 2 }),
2059
+ h('path', { d: 'M9 4.5v15' }));
2060
+ if (name === 'more') return h('svg', common,
2061
+ h('circle', { cx: 6, cy: 12, r: 1.2, fill: 'currentColor', stroke: 'none' }),
2062
+ h('circle', { cx: 12, cy: 12, r: 1.2, fill: 'currentColor', stroke: 'none' }),
2063
+ h('circle', { cx: 18, cy: 12, r: 1.2, fill: 'currentColor', stroke: 'none' }));
2064
+ if (name === 'pin') return h('svg', common,
2065
+ h('path', { d: 'm9 4 6 6M8 5l3-1 8 8-1 3-5-1-5 5-1-1 5-5-1-5-3-1Z' }));
2066
+ if (name === 'archive') return h('svg', common,
2067
+ h('rect', { x: 3.5, y: 5, width: 17, height: 4, rx: 1 }),
2068
+ h('path', { d: 'M5 9v10h14V9M10 13h4' }));
2069
+ if (name === 'fork') return h('svg', common,
2070
+ h('circle', { cx: 7, cy: 5, r: 2 }), h('circle', { cx: 7, cy: 19, r: 2 }),
2071
+ h('circle', { cx: 17, cy: 8, r: 2 }), h('path', { d: 'M7 7v10M7 15c5 0 10-2 10-5' }));
2072
+ if (name === 'edit') return h('svg', common,
2073
+ h('path', { d: 'M4 20h4L20 8l-4-4L4 16v4Z' }), h('path', { d: 'm14 6 4 4' }));
2074
+ if (name === 'clock') return h('svg', common,
2075
+ h('circle', { cx: 12, cy: 12, r: 8 }), h('path', { d: 'M12 7.5V12l3 2' }));
2076
+ if (name === 'folder') return h('svg', common,
2077
+ h('path', { d: 'M3.5 7.5h17v10.5h-17V7.5Z' }),
2078
+ h('path', { d: 'M3.5 7.5V6h6l2 2h9' }));
2079
+ if (name === 'gear') return h('svg', common,
2080
+ h('path', { d: 'm9.8 3.6-.6 2.1-1.4.8-2.1-.5-2.1 3.6 1.5 1.6v1.6l-1.5 1.6 2.1 3.6 2.1-.5 1.4.8.6 2.1h4.4l.6-2.1 1.4-.8 2.1.5 2.1-3.6-1.5-1.6v-1.6l1.5-1.6-2.1-3.6-2.1.5-1.4-.8-.6-2.1Z' }),
2081
+ h('circle', { cx: 12, cy: 12, r: 3.1 }));
2082
+ if (name === 'file') return h('svg', common,
2083
+ h('path', { d: 'M6 3.5h7l5 5v12H6v-17Z' }), h('path', { d: 'M13 3.5v5h5' }));
2084
+ if (name === 'image') return h('svg', common,
2085
+ h('rect', { x: 4, y: 4, width: 16, height: 16, rx: 2 }),
2086
+ h('circle', { cx: 9, cy: 9, r: 1.5 }),
2087
+ h('path', { d: 'm4 17 5-4 3 2 4-5 4 4' }));
2088
+ if (name === 'terminal') return h('svg', common,
2089
+ h('rect', { x: 3.5, y: 5, width: 17, height: 14, rx: 2 }),
2090
+ h('path', { d: 'm7 10 3 2.5L7 15M13 15h4' }));
2091
+ if (name === 'browser') return h('svg', common,
2092
+ h('circle', { cx: 12, cy: 12, r: 8 }), h('path', { d: 'M4 12h16M12 4c2.2 2.2 3.3 5 3.3 8S14.2 17.8 12 20c-2.2-2.2-3.3-5-3.3-8S9.8 6.2 12 4Z' }));
2093
+ if (name === 'slash') return h('svg', common,
2094
+ h('path', { d: 'M14 4 9 20' }));
2095
+ if (name === 'tool') return h('svg', common,
2096
+ h('path', { d: 'M14.5 6.5a3.5 3.5 0 0 0 4.4 4.4L10 19.8a2.1 2.1 0 0 1-3-3l8.9-8.9a3.5 3.5 0 0 0-1.4-1.4Z' }),
2097
+ h('path', { d: 'm6 6 2 2' }));
2098
+ return h('svg', common, h('path', { d: 'm5 12 14-7-4 14-3-6-7-1Z' }),
2099
+ h('path', { d: 'm12 13 7-8' }));
2100
+ }
2101
+
2102
+ function createSessionHistory(workspace, sessions, workspaces, beforeOpen) {
2103
+ const visits = [];
2104
+ const listeners = new Set();
2105
+ let cursor = -1;
2106
+ let restoring = -1;
2107
+ let snapshot = { back: false, forward: false };
2108
+
2109
+ function findVisit(step) {
2110
+ const { byId } = sessions.list.getSnapshot();
2111
+ const { archivedSessionIds } = workspaces.list.getSnapshot();
2112
+ const currentId = visits[cursor]?.sessionId;
2113
+ for (let index = cursor + step; index >= 0 && index < visits.length; index += step) {
2114
+ const entry = visits[index];
2115
+ if (!entry || entry.sessionId === currentId) continue;
2116
+ const parentId = entry.subagentAddress?.parentSessionId;
2117
+ if ((byId[entry.sessionId] || (parentId && byId[parentId])) &&
2118
+ !archivedSessionIds.includes(entry.sessionId) && !archivedSessionIds.includes(parentId)) return index;
2119
+ }
2120
+ return -1;
2121
+ }
2122
+
2123
+ function refresh() {
2124
+ const back = findVisit(-1) !== -1;
2125
+ const forward = findVisit(1) !== -1;
2126
+ if (back === snapshot.back && forward === snapshot.forward) return;
2127
+ snapshot = { back, forward };
2128
+ for (const listener of listeners) listener();
2129
+ }
2130
+
2131
+ function recordSelection() {
2132
+ const selection = workspace.selection.getSnapshot();
2133
+ if (restoring !== -1 && visits[restoring].sessionId === selection.sessionId) {
2134
+ cursor = restoring;
2135
+ } else if (selection.sessionId !== visits[cursor]?.sessionId) {
2136
+ // An ordinary visit after going back starts a new branch.
2137
+ visits.splice(cursor + 1);
2138
+ visits.push(selection.sessionId ? selection : null);
2139
+ if (visits.length > 60) visits.shift();
2140
+ cursor = visits.length - 1;
2141
+ }
2142
+ refresh();
2143
+ }
2144
+
2145
+ const unsubscribers = [workspace.selection.subscribe(recordSelection),
2146
+ sessions.list.subscribe(refresh), workspaces.list.subscribe(refresh)];
2147
+ recordSelection();
2148
+ return {
2149
+ getSnapshot: () => snapshot,
2150
+ subscribe(listener) {
2151
+ listeners.add(listener);
2152
+ return () => listeners.delete(listener);
2153
+ },
2154
+ go(step) {
2155
+ const index = findVisit(step);
2156
+ if (index === -1) return;
2157
+ const entry = visits[index];
2158
+ restoring = index;
2159
+ try {
2160
+ beforeOpen();
2161
+ workspace.openSession(entry.subagentAddress ?? entry.sessionId);
2162
+ recordSelection();
2163
+ } catch (error) {
2164
+ reportDiagnostic(`could not open conversation history: ${errorMessage(error)}`);
2165
+ } finally {
2166
+ restoring = -1;
2167
+ }
2168
+ },
2169
+ dispose() {
2170
+ for (const unsubscribe of unsubscribers) unsubscribe();
2171
+ listeners.clear();
2172
+ },
2173
+ };
2174
+ }
2175
+
2176
+ function SidebarHistoryButtons({ history }) {
2177
+ const { back, forward } = React.useSyncExternalStore(history.subscribe, history.getSnapshot);
2178
+ const button = (name, key, enabled, step) => {
2179
+ const label = translate({}, key);
2180
+ return h('button', {
2181
+ type: 'button', className: 'cw-icon-button', title: label, 'aria-label': label,
2182
+ disabled: !enabled, onClick: () => history.go(step),
2183
+ }, h(Icon, { name }));
2184
+ };
2185
+ return h('div', { className: 'cw-session-history', role: 'group', 'aria-label': translate({}, 'sessionHistory') },
2186
+ button('back', 'sessionBack', back, -1),
2187
+ button('forward', 'sessionForward', forward, 1));
2188
+ }
2189
+
2190
+ function SidebarSettingsButton() {
2191
+ const buttonRef = React.useRef(null);
2192
+ const [historyHost, setHistoryHost] = React.useState(null);
2193
+ const name = useAccountName();
2194
+ const initial = name.normalize('NFC').match(/[\p{L}\p{N}]/u)?.[0];
2195
+ React.useLayoutEffect(() => {
2196
+ const sidebar = buttonRef.current?.closest('.kB3qhq_root');
2197
+ setHistoryHost(sidebar?.querySelector(':scope > .kB3qhq_topStrip, :scope > .kB3qhq_logoRow') ?? null);
2198
+ }, []);
2199
+ React.useLayoutEffect(() => {
2200
+ const footer = buttonRef.current?.closest('.kB3qhq_footArea');
2201
+ if (!footer || !initial) return;
2202
+ footer.style.setProperty('--cw-account-initial', JSON.stringify(initial));
2203
+ return () => footer.style.removeProperty('--cw-account-initial');
2204
+ }, [initial]);
2205
+ const label = translate({}, 'menuSettings');
2206
+ return h(React.Fragment, null, h('button', {
2207
+ ref: buttonRef, type: 'button', className: 'cw-icon-button cw-sidebar-settings',
2208
+ title: label, 'aria-label': label,
2209
+ onClick: () => runtime.shortcuts.registry.invoke('settings.open', { modal: null }),
2210
+ }, h(Icon, { name: 'gear' })),
2211
+ historyHost && runtime.sessionHistory
2212
+ ? ReactDOM.createPortal(h(SidebarHistoryButtons, { history: runtime.sessionHistory }), historyHost) : null);
2213
+ }
2214
+
2215
+ const TOOL_LABEL_KEYS = {
2216
+ apply_patch: 'toolApplyPatch',
2217
+ edit: 'toolApplyPatch',
2218
+ write: 'toolWrite',
2219
+ bash: 'toolBash',
2220
+ read: 'toolRead',
2221
+ read_image: 'toolRead',
2222
+ grep: 'toolGrep',
2223
+ glob: 'toolGlob',
2224
+ web_search: 'toolWebSearch',
2225
+ web_fetch: 'toolWebFetch',
2226
+ };
2227
+
2228
+ function toolDisplayName(name, t) {
2229
+ const key = TOOL_LABEL_KEYS[name];
2230
+ if (key) return t(key);
2231
+ return String(name || 'tool').replace(/[_-]+/gu, ' ').replace(/^\w/u, (letter) => letter.toUpperCase());
2232
+ }
2233
+
2234
+ function ToolCallMessageBase(props) {
2235
+ const t = (key) => translate(props, key);
2236
+ const summary = summarizeToolCall(props.node?.data);
2237
+ if (!summary) return null;
2238
+ const canInspect = summary.callId && typeof props.inspectCall === 'function';
2239
+ const stateLabel = summary.status === 'error'
2240
+ ? t('toolFailed')
2241
+ : summary.status === 'done'
2242
+ ? t('toolDone')
2243
+ : summary.status === 'preparing'
2244
+ ? t('toolPreparing') : t('toolRunning');
2245
+ const content = [
2246
+ h('span', { className: 'cw-tool-leading', key: 'icon' }, h(Icon, { name: 'tool' })),
2247
+ h('span', { className: 'cw-tool-label', key: 'label' }, toolDisplayName(summary.name, t)),
2248
+ summary.detail ? h('span', { className: 'cw-tool-detail', key: 'detail' }, summary.detail) : null,
2249
+ summary.subCalls > 0 ? h('span', { className: 'cw-tool-count', key: 'count' }, `+${summary.subCalls}`) : null,
2250
+ summary.status !== 'done' ? h('span', {
2251
+ className: 'cw-tool-state', key: 'state', 'data-state': summary.status,
2252
+ }, stateLabel) : null,
2253
+ canInspect ? h('span', { className: 'cw-tool-chevron', key: 'chevron' }, h(Icon, { name: 'chevron' })) : null,
2254
+ ];
2255
+ const rowProps = {
2256
+ className: 'cw-tool-summary',
2257
+ title: canInspect ? t('toolDetails') : stateLabel,
2258
+ 'aria-label': canInspect ? `${t('toolDetails')}: ${summary.name}` : summary.name,
2259
+ };
2260
+ return h('div', { className: 'cw-root cw-tool-row' },
2261
+ h('div', { className: 'cw-tool-row-inner' },
2262
+ canInspect
2263
+ ? h('button', {
2264
+ ...rowProps, type: 'button',
2265
+ onClick: () => props.inspectCall(summary.callId),
2266
+ }, content)
2267
+ : h('div', rowProps, content),
2268
+ ),
2269
+ );
2270
+ }
2271
+
2272
+ function errorMessage(error) {
2273
+ if (!error) return '';
2274
+ return error.error?.message || error.message || String(error);
2275
+ }
2276
+
2277
+ const MAX_IMAGE_BYTES = 8 * 1024 * 1024;
2278
+ const MAX_IMAGES = 4;
2279
+ const MAX_FILES = 6;
2280
+ const MAX_FILE_BYTES = 20 * 1024 * 1024;
2281
+
2282
+ function needsImageRecognition(result, images) {
2283
+ return images.length > 0 && !result?.ok
2284
+ && result?.error?.details?.reason === 'MODEL_DOES_NOT_SUPPORT_IMAGES';
2285
+ }
2286
+
2287
+ async function requestImageDescription(images, question, signal, t) {
2288
+ const response = await fetch(new URL('api/workbench/describeImages', document.baseURI), {
2289
+ method: 'POST',
2290
+ headers: { 'Content-Type': 'application/json' },
2291
+ body: JSON.stringify({ question, images: images.map(({ mediaType, data }) => ({ mediaType, data })) }),
2292
+ signal,
2293
+ });
2294
+ const result = await response.json().catch(() => null);
2295
+ if (!response.ok || !result?.description) {
2296
+ throw new Error([t('imageRecognitionFailed'), result?.error].filter(Boolean).join(' '));
2297
+ }
2298
+ return result.description;
2299
+ }
2300
+
2301
+ function readAsBase64(file) {
2302
+ return new Promise((resolve, reject) => {
2303
+ const reader = new FileReader();
2304
+ reader.onload = () => {
2305
+ const result = String(reader.result || '');
2306
+ const marker = result.indexOf(',');
2307
+ if (marker < 0) {
2308
+ reject(new Error('read-failed'));
2309
+ return;
2310
+ }
2311
+ resolve(result.slice(marker + 1));
2312
+ };
2313
+ reader.onerror = () => reject(new Error('read-failed'));
2314
+ reader.readAsDataURL(file);
2315
+ });
2316
+ }
2317
+ const DRAFT_PREFIX = 'claude-workbench:draft:';
2318
+
2319
+ function draftKey(sessionId) {
2320
+ return `${DRAFT_PREFIX}${String(sessionId)}`;
2321
+ }
2322
+
2323
+ function readDraft(sessionId) {
2324
+ if (!sessionId) return '';
2325
+ try {
2326
+ return localStorage.getItem(draftKey(sessionId)) || '';
2327
+ } catch {
2328
+ return '';
2329
+ }
2330
+ }
2331
+
2332
+ function writeDraft(sessionId, text) {
2333
+ if (!sessionId) return;
2334
+ try {
2335
+ if (text) localStorage.setItem(draftKey(sessionId), text);
2336
+ else localStorage.removeItem(draftKey(sessionId));
2337
+ } catch {
2338
+ // Draft persistence is best-effort when storage is unavailable.
2339
+ }
2340
+ }
2341
+
2342
+ function createAttachmentDrafts() {
2343
+ const drafts = new Map();
2344
+ const listeners = new Set();
2345
+ const empty = { images: [], files: [], submission: null };
2346
+ const read = (id) => drafts.get(id) ?? empty;
2347
+
2348
+ function write(id, draft) {
2349
+ if (draft.images.length || draft.files.length || draft.submission) drafts.set(id, draft);
2350
+ else drafts.delete(id);
2351
+ for (const listener of listeners) listener();
2352
+ }
2353
+
2354
+ return {
2355
+ read,
2356
+ subscribe(listener) {
2357
+ listeners.add(listener);
2358
+ return () => listeners.delete(listener);
2359
+ },
2360
+ replace(id, kind, change) {
2361
+ const draft = read(id);
2362
+ if (!id || draft.submission) return;
2363
+ write(id, { ...draft, [kind]: change(draft[kind]) });
2364
+ },
2365
+ begin(id) {
2366
+ const draft = read(id);
2367
+ if (!id || draft.submission) return null;
2368
+ const submission = Symbol();
2369
+ write(id, { ...draft, submission });
2370
+ return submission;
2371
+ },
2372
+ finish(id, submission, accepted = false) {
2373
+ const draft = read(id);
2374
+ if (draft.submission !== submission) return;
2375
+ write(id, accepted ? empty : { ...draft, submission: null });
2376
+ },
2377
+ };
2378
+ }
2379
+ const attachmentDrafts = createAttachmentDrafts();
2380
+
2381
+ function DraftImage({ file, fallback }) {
2382
+ const [previewUrl, setPreviewUrl] = React.useState('');
2383
+ React.useLayoutEffect(() => {
2384
+ const url = URL.createObjectURL(file);
2385
+ setPreviewUrl(url);
2386
+ return () => URL.revokeObjectURL(url);
2387
+ }, [file]);
2388
+ return previewUrl ? h('img', { src: previewUrl, alt: file.name || fallback }) : null;
2389
+ }
2390
+
2391
+ function voiceDraftInsertion(draft, span, transcript) {
2392
+ if (!span || span.sessionId !== draft.sessionId || span.draftRev !== draft.revision) return null;
2393
+ const { start, end } = span;
2394
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > draft.text.length) return null;
2395
+ return {
2396
+ text: draft.text.slice(0, start) + transcript + draft.text.slice(end),
2397
+ caret: start + transcript.length,
2398
+ };
2399
+ }
2400
+
2401
+ function imageFromFile(file, t) {
2402
+ return new Promise((resolve, reject) => {
2403
+ if (!file || !String(file.type).startsWith('image/')) {
2404
+ reject(new Error(t('imageReadFailed')));
2405
+ return;
2406
+ }
2407
+ if (file.size > MAX_IMAGE_BYTES) {
2408
+ reject(new Error(t('imageTooLarge')));
2409
+ return;
2410
+ }
2411
+ const reader = new FileReader();
2412
+ reader.onerror = () => reject(new Error(t('imageReadFailed')));
2413
+ reader.onload = () => {
2414
+ const dataUrl = typeof reader.result === 'string' ? reader.result : '';
2415
+ const comma = dataUrl.indexOf(',');
2416
+ if (comma < 0) {
2417
+ reject(new Error(t('imageReadFailed')));
2418
+ return;
2419
+ }
2420
+ resolve({
2421
+ name: file.name || t('image'),
2422
+ mediaType: file.type,
2423
+ previewUrl: dataUrl,
2424
+ data: dataUrl.slice(comma + 1),
2425
+ });
2426
+ };
2427
+ reader.readAsDataURL(file);
2428
+ });
2429
+ }
2430
+
2431
+ function rootModelGroup(groups, current) {
2432
+ if (!Array.isArray(groups) || groups.length === 0) return null;
2433
+ const active = groups.find((group) => group.id === current?.provider);
2434
+ if (active) return active;
2435
+ return groups.find((group) => Array.isArray(group.models) && group.models.length > 0) || groups[0];
2436
+ }
2437
+
2438
+ function modelEffortLabel(state, model, t) {
2439
+ const reasoning = model?.reasoning;
2440
+ const effortId = state?.current?.reasoningEffort ?? reasoning?.defaultEffort;
2441
+ if (effortId === undefined) return state?.retainedEffort || '';
2442
+ return reasoning?.efforts?.find((effort) => effort.id === effortId)?.name || effortId;
2443
+ }
2444
+
2445
+ function ModelControl({ props, sessionId, t, openRequest }) {
2446
+ const service = runtime.modelDirectories || runtime.ctx?.get('modelDirectories');
2447
+ let directory;
2448
+ try {
2449
+ directory = service && sessionId ? service.directoryFor(sessionId) : undefined;
2450
+ } catch {
2451
+ directory = undefined;
2452
+ }
2453
+ const [state, setState] = React.useState(() => directory?.store.getSnapshot() || null);
2454
+ const [open, setOpen] = React.useState(false);
2455
+ const [view, setView] = React.useState('root');
2456
+ const [query, setQuery] = React.useState('');
2457
+ const [selecting, setSelecting] = React.useState(false);
2458
+ const [error, setError] = React.useState('');
2459
+ const [menuPos, setMenuPos] = React.useState(null);
2460
+ const rootRef = React.useRef(null);
2461
+ const triggerRef = React.useRef(null);
2462
+ const menuRef = React.useRef(null);
2463
+
2464
+ React.useEffect(() => {
2465
+ if (openRequest && directory) setOpen(true);
2466
+ }, [openRequest, directory]);
2467
+
2468
+ React.useEffect(() => {
2469
+ if (!directory) {
2470
+ setState(null);
2471
+ return undefined;
2472
+ }
2473
+ let active = true;
2474
+ const sync = () => {
2475
+ if (active) setState(directory.store.getSnapshot());
2476
+ };
2477
+ sync();
2478
+ const unsubscribe = directory.store.subscribe(sync);
2479
+ directory.load().catch((cause) => {
2480
+ if (active) setError(`${t('modelLoadFailed')} ${errorMessage(cause)}`);
2481
+ });
2482
+ return () => {
2483
+ active = false;
2484
+ unsubscribe?.();
2485
+ };
2486
+ }, [directory, sessionId]);
2487
+
2488
+ React.useEffect(() => {
2489
+ if (!open) return undefined;
2490
+ const closeOutside = (event) => {
2491
+ if (rootRef.current?.contains(event.target) || menuRef.current?.contains(event.target)) return;
2492
+ setOpen(false);
2493
+ };
2494
+ document.addEventListener('mousedown', closeOutside);
2495
+ return () => document.removeEventListener('mousedown', closeOutside);
2496
+ }, [open]);
2497
+
2498
+ React.useLayoutEffect(() => {
2499
+ if (!open) {
2500
+ setMenuPos(null);
2501
+ return undefined;
2502
+ }
2503
+ const place = () => {
2504
+ const rect = triggerRef.current?.getBoundingClientRect();
2505
+ const menu = menuRef.current;
2506
+ if (!rect || !menu) return;
2507
+ const width = Math.min(340, window.innerWidth - 32);
2508
+ const left = Math.max(16, Math.min(rect.right - width, window.innerWidth - width - 16));
2509
+ const availableHeight = Math.max(160, Math.min(380, rect.top - 24));
2510
+ const height = Math.min(menu.offsetHeight || availableHeight, availableHeight);
2511
+ const top = Math.max(16, rect.top - height - 8);
2512
+ setMenuPos({ left, width, top, maxHeight: availableHeight });
2513
+ };
2514
+ place();
2515
+ const observer = typeof ResizeObserver === 'function'
2516
+ ? new ResizeObserver(place) : null;
2517
+ if (observer && menuRef.current) observer.observe(menuRef.current);
2518
+ window.addEventListener('resize', place);
2519
+ window.addEventListener('scroll', place, true);
2520
+ return () => {
2521
+ observer?.disconnect();
2522
+ window.removeEventListener('resize', place);
2523
+ window.removeEventListener('scroll', place, true);
2524
+ };
2525
+ }, [open]);
2526
+
2527
+ React.useEffect(() => {
2528
+ setOpen(false);
2529
+ setView('root');
2530
+ setQuery('');
2531
+ setError('');
2532
+ }, [sessionId]);
2533
+
2534
+ if (!directory) return null;
2535
+ const groups = Array.isArray(state?.groups) ? state.groups : [];
2536
+ const current = state?.current;
2537
+ const currentGroup = groups.find((group) => group.id === current?.provider);
2538
+ const currentModel = currentGroup?.models.find((model) => model.id === current?.model);
2539
+ const currentName = currentModel?.name || (current ? current.model : t('selectModel'));
2540
+ const effort = modelEffortLabel(state, currentModel, t);
2541
+ const currentEffortId = current?.reasoningEffort ?? currentModel?.reasoning?.defaultEffort;
2542
+ const effortChoices = currentModel?.reasoning
2543
+ ? [
2544
+ ...(currentModel.reasoning.defaultEffort === undefined
2545
+ ? [{ key: 'default', id: undefined, label: t('effortDefault') }]
2546
+ : []),
2547
+ ...(currentModel.reasoning.efforts || []).map((level) => ({
2548
+ key: `effort:${level.id}`, id: level.id, label: level.name,
2549
+ })),
2550
+ ]
2551
+ : [];
2552
+ const totalModels = groups.reduce((count, group) => count + group.models.length, 0);
2553
+ const rootGroup = rootModelGroup(groups, current);
2554
+ const allView = view === 'all';
2555
+ const showSearch = allView && totalModels > 4;
2556
+ const normalized = query.trim().toLowerCase();
2557
+ const filtered = groups.map((group) => ({
2558
+ ...group,
2559
+ models: group.models.filter((model) =>
2560
+ !normalized
2561
+ || model.name.toLowerCase().includes(normalized)
2562
+ || group.name.toLowerCase().includes(normalized)
2563
+ || group.id.toLowerCase().includes(normalized)),
2564
+ })).filter((group) => group.models.length > 0);
2565
+ const visibleGroups = allView
2566
+ ? filtered
2567
+ : (rootGroup ? [rootGroup] : []);
2568
+ const hasMoreGroups = groups.some((group) =>
2569
+ group.id !== rootGroup?.id && Array.isArray(group.models) && group.models.length > 0);
2570
+
2571
+ async function choose(group, model) {
2572
+ setSelecting(true);
2573
+ setError('');
2574
+ try {
2575
+ const result = await directory.select({ provider: group.id, model: model.id });
2576
+ if (!result.ok) {
2577
+ throw new Error(`${result.error.code}: ${result.error.message}`);
2578
+ }
2579
+ setOpen(false);
2580
+ setQuery('');
2581
+ } catch (cause) {
2582
+ setError(`${t('modelSelectFailed')} ${errorMessage(cause)}`);
2583
+ } finally {
2584
+ setSelecting(false);
2585
+ }
2586
+ }
2587
+
2588
+ async function chooseEffort(effortId) {
2589
+ if (!current) return;
2590
+ setSelecting(true);
2591
+ setError('');
2592
+ try {
2593
+ const result = await directory.select({
2594
+ provider: current.provider,
2595
+ model: current.model,
2596
+ ...effortId === undefined ? {} : { reasoningEffort: effortId },
2597
+ });
2598
+ if (!result.ok) {
2599
+ throw new Error(`${result.error.code}: ${result.error.message}`);
2600
+ }
2601
+ setOpen(false);
2602
+ } catch (cause) {
2603
+ setError(`${t('effortSelectFailed')} ${errorMessage(cause)}`);
2604
+ } finally {
2605
+ setSelecting(false);
2606
+ }
2607
+ }
2608
+
2609
+ function onMenuKeyDown(event) {
2610
+ if (event.key === 'Escape') {
2611
+ event.preventDefault();
2612
+ event.stopPropagation();
2613
+ setOpen(false);
2614
+ triggerRef.current?.focus();
2615
+ return;
2616
+ }
2617
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
2618
+ const items = Array.from(menuRef.current?.querySelectorAll('[data-cw-menu-item]') || []);
2619
+ if (items.length === 0) return;
2620
+ event.preventDefault();
2621
+ const active = items.indexOf(document.activeElement);
2622
+ const step = event.key === 'ArrowDown' ? 1 : -1;
2623
+ const next = active < 0
2624
+ ? (step > 0 ? 0 : items.length - 1)
2625
+ : (active + step + items.length) % items.length;
2626
+ items[next]?.focus();
2627
+ }
2628
+
2629
+ return h('div', { className: 'cw-model-wrap', ref: rootRef },
2630
+ h('button', {
2631
+ ref: triggerRef,
2632
+ type: 'button', className: 'cw-model-button',
2633
+ title: currentName, 'aria-label': t('selectModel'),
2634
+ 'aria-expanded': open, disabled: selecting || state?.status === 'selecting',
2635
+ onClick: () => {
2636
+ const next = !open;
2637
+ setOpen(next);
2638
+ if (next) setView('root');
2639
+ setQuery('');
2640
+ setError('');
2641
+ if (next) directory.load().catch((cause) => setError(`${t('modelLoadFailed')} ${errorMessage(cause)}`));
2642
+ },
2643
+ },
2644
+ h('span', { className: 'cw-model-button-label' }, currentName),
2645
+ effort ? h('span', { className: 'cw-model-effort' }, effort) : null,
2646
+ h(Icon, { name: 'chevron' })),
2647
+ open ? ReactDOM.createPortal(
2648
+ h('div', {
2649
+ ref: menuRef, className: 'cw-root cw-model-menu',
2650
+ style: menuPos || {
2651
+ visibility: 'hidden', left: 0, top: 0,
2652
+ width: Math.min(340, window.innerWidth - 32),
2653
+ },
2654
+ role: 'dialog', 'aria-label': t('selectModel'),
2655
+ onKeyDown: onMenuKeyDown,
2656
+ },
2657
+ allView ? h('button', {
2658
+ type: 'button', className: 'cw-model-back', 'data-cw-menu-item': '',
2659
+ onClick: () => { setView('root'); setQuery(''); },
2660
+ }, `‹ ${t('modelBack')}`) : null,
2661
+ showSearch ? h('input', {
2662
+ className: 'cw-model-search', value: query, autoFocus: true,
2663
+ placeholder: t('modelSearch'), 'aria-label': t('modelSearch'),
2664
+ onChange: (event) => setQuery(event.target.value),
2665
+ }) : null,
2666
+ state?.status === 'loading' ? h('div', { className: 'cw-model-empty' }, t('modelLoading')) : null,
2667
+ !state?.status || totalModels === 0
2668
+ ? h('div', { className: 'cw-model-empty' }, t('noModels')) : null,
2669
+ visibleGroups.map((group) => h('div', { key: group.id },
2670
+ h('div', { className: 'cw-model-group-label' }, group.name),
2671
+ group.models.map((model) => {
2672
+ const checked = current?.provider === group.id && current?.model === model.id;
2673
+ return h('button', {
2674
+ type: 'button', key: model.id, className: 'cw-model-option',
2675
+ role: 'menuitemradio', 'aria-checked': checked,
2676
+ 'data-cw-menu-item': '',
2677
+ autoFocus: !showSearch && checked,
2678
+ disabled: selecting || state?.status === 'selecting',
2679
+ onClick: () => void choose(group, model),
2680
+ },
2681
+ h('span', { className: 'cw-model-option-label' }, model.name),
2682
+ checked ? h('span', { className: 'cw-model-option-check' }, '✓') : null);
2683
+ }),
2684
+ )),
2685
+ !allView && effortChoices.length > 0 ? h('div', { className: 'cw-model-efforts' },
2686
+ h('div', { className: 'cw-model-group-label' }, t('effortLabel')),
2687
+ h('div', { className: 'cw-effort-options' },
2688
+ effortChoices.map((level) => h('button', {
2689
+ type: 'button', key: level.key, className: 'cw-effort-option',
2690
+ role: 'menuitemradio', 'aria-checked': currentEffortId === level.id,
2691
+ 'data-cw-menu-item': '',
2692
+ disabled: selecting || state?.status === 'selecting',
2693
+ onClick: () => void chooseEffort(level.id),
2694
+ }, level.label)),
2695
+ ),
2696
+ ) : null,
2697
+ !allView && hasMoreGroups ? h('button', {
2698
+ type: 'button', className: 'cw-model-option cw-model-more',
2699
+ 'data-cw-menu-item': '',
2700
+ onClick: () => { setView('all'); setQuery(''); },
2701
+ },
2702
+ h('span', { className: 'cw-model-option-label' }, t('modelMore')),
2703
+ h(Icon, { name: 'chevron' })) : null,
2704
+ error ? h('div', { className: 'cw-model-error' }, error) : null,
2705
+ ),
2706
+ document.body,
2707
+ ) : null,
2708
+ );
2709
+ }
2710
+
2711
+ function useContextModel(sessionId) {
2712
+ const service = runtime.modelDirectories || runtime.ctx?.get('modelDirectories');
2713
+ let directory;
2714
+ try {
2715
+ directory = service && sessionId ? service.directoryFor(sessionId) : undefined;
2716
+ } catch {
2717
+ directory = undefined;
2718
+ }
2719
+ const [state, setState] = React.useState(() => directory?.store.getSnapshot() || null);
2720
+ React.useEffect(() => {
2721
+ if (!directory) {
2722
+ setState(null);
2723
+ return undefined;
2724
+ }
2725
+ let active = true;
2726
+ const sync = () => {
2727
+ if (active) setState(directory.store.getSnapshot());
2728
+ };
2729
+ sync();
2730
+ const unsubscribe = directory.store.subscribe(sync);
2731
+ directory.load().catch(() => {});
2732
+ return () => {
2733
+ active = false;
2734
+ unsubscribe?.();
2735
+ };
2736
+ }, [directory, sessionId]);
2737
+ const groups = Array.isArray(state?.groups) ? state.groups : [];
2738
+ const current = state?.current;
2739
+ const group = groups.find((item) => item.id === current?.provider);
2740
+ const model = group?.models.find((item) => item.id === current?.model);
2741
+ return {
2742
+ name: model?.name || (current ? current.model : ''),
2743
+ provider: group?.name || current?.provider || '',
2744
+ effort: modelEffortLabel(state, model, (key) => translate({}, key)),
2745
+ };
2746
+ }
2747
+
2748
+ function useAnchoredPopup(open, width = 340, heightLimit = 380) {
2749
+ const triggerRef = React.useRef(null);
2750
+ const menuRef = React.useRef(null);
2751
+ const [position, setPosition] = React.useState(null);
2752
+ React.useLayoutEffect(() => {
2753
+ if (!open) {
2754
+ setPosition(null);
2755
+ return undefined;
2756
+ }
2757
+ const place = () => {
2758
+ const rect = triggerRef.current?.getBoundingClientRect();
2759
+ const menu = menuRef.current;
2760
+ if (!rect || !menu) return;
2761
+ const resolvedWidth = Math.min(width, window.innerWidth - 32);
2762
+ const left = Math.max(16, Math.min(rect.right - resolvedWidth, window.innerWidth - resolvedWidth - 16));
2763
+ const availableHeight = Math.max(160, Math.min(heightLimit, rect.top - 24));
2764
+ const height = Math.min(menu.offsetHeight || availableHeight, availableHeight);
2765
+ setPosition({
2766
+ left, width: resolvedWidth,
2767
+ top: Math.max(16, rect.top - height - 8),
2768
+ maxHeight: availableHeight,
2769
+ });
2770
+ };
2771
+ place();
2772
+ const observer = typeof ResizeObserver === 'function' ? new ResizeObserver(place) : null;
2773
+ if (observer && menuRef.current) observer.observe(menuRef.current);
2774
+ window.addEventListener('resize', place);
2775
+ window.addEventListener('scroll', place, true);
2776
+ return () => {
2777
+ observer?.disconnect();
2778
+ window.removeEventListener('resize', place);
2779
+ window.removeEventListener('scroll', place, true);
2780
+ };
2781
+ }, [open, width, heightLimit]);
2782
+ return { triggerRef, menuRef, position };
2783
+ }
2784
+
2785
+ const PERMISSION_LABEL_KEYS = {
2786
+ 'read-only': 'permissionReadOnly',
2787
+ 'workspace-write': 'permissionWorkspaceWrite',
2788
+ 'full-access': 'permissionFullAccess',
2789
+ 'danger-full-access': 'permissionFullAccess',
2790
+ auto: 'permissionAuto',
2791
+ custom: 'permissionCustom',
2792
+ };
2793
+
2794
+ function permissionLabel(option, t) {
2795
+ if (!option) return t('permissionCustom');
2796
+ const key = PERMISSION_LABEL_KEYS[option.value];
2797
+ return key ? t(key) : (option.name || option.value);
2798
+ }
2799
+
2800
+ function PermissionControl({ props, sessionId, t }) {
2801
+ const projected = props.useProjection('permissions');
2802
+ const currentValue = projected?.currentValue;
2803
+ const [catalog, setCatalog] = React.useState(null);
2804
+ const [status, setStatus] = React.useState('loading');
2805
+ const [error, setError] = React.useState('');
2806
+ const [open, setOpen] = React.useState(false);
2807
+ const [confirming, setConfirming] = React.useState(null);
2808
+ const [busy, setBusy] = React.useState(false);
2809
+ const { triggerRef, menuRef, position } = useAnchoredPopup(open, 280);
2810
+
2811
+ React.useEffect(() => {
2812
+ let active = true;
2813
+ const remote = runtime.permissionPresets;
2814
+ if (!remote?.catalog) {
2815
+ setStatus('error');
2816
+ setError(t('commandsUnavailable'));
2817
+ return undefined;
2818
+ }
2819
+ Promise.resolve(remote.catalog()).then((result) => {
2820
+ if (!active) return;
2821
+ if (!result?.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
2822
+ setCatalog(result.value);
2823
+ setStatus('ready');
2824
+ }).catch((cause) => {
2825
+ if (!active) return;
2826
+ setStatus('error');
2827
+ setError(`${t('permissionLoadFailed')} ${errorMessage(cause)}`);
2828
+ });
2829
+ return () => {
2830
+ active = false;
2831
+ };
2832
+ }, [sessionId]);
2833
+
2834
+ React.useEffect(() => {
2835
+ if (!open) {
2836
+ setConfirming(null);
2837
+ return undefined;
2838
+ }
2839
+ const closeOutside = (event) => {
2840
+ if (triggerRef.current?.contains(event.target) || menuRef.current?.contains(event.target)) return;
2841
+ setOpen(false);
2842
+ };
2843
+ document.addEventListener('mousedown', closeOutside);
2844
+ return () => document.removeEventListener('mousedown', closeOutside);
2845
+ }, [open]);
2846
+
2847
+ const options = Array.isArray(catalog?.options) ? catalog.options : [];
2848
+ const currentOption = options.find((option) => option.value === currentValue)
2849
+ || (currentValue ? { value: currentValue, name: currentValue } : null);
2850
+ const label = permissionLabel(currentOption, t);
2851
+
2852
+ async function apply(option) {
2853
+ setBusy(true);
2854
+ setError('');
2855
+ try {
2856
+ const binding = runtime.sessions?.binding(sessionId);
2857
+ if (!binding) throw new Error(t('sessionNotReady'));
2858
+ const result = await binding.session.command(`/permission ${option.value}`);
2859
+ if (!result?.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
2860
+ if (!result.value?.matched) throw new Error(t('permissionSwitchFailed'));
2861
+ setOpen(false);
2862
+ setConfirming(null);
2863
+ } catch (cause) {
2864
+ setError(`${t('permissionSwitchFailed')} ${errorMessage(cause)}`);
2865
+ } finally {
2866
+ setBusy(false);
2867
+ }
2868
+ }
2869
+
2870
+ function choose(option) {
2871
+ if (option.value === currentValue) {
2872
+ setOpen(false);
2873
+ return;
2874
+ }
2875
+ if (isDangerousPermission(option.value)) {
2876
+ setConfirming(option);
2877
+ return;
2878
+ }
2879
+ void apply(option);
2880
+ }
2881
+
2882
+ if (!currentValue && status !== 'ready') return null;
2883
+ return h('div', { className: 'cw-model-wrap', ref: triggerRef },
2884
+ h('button', {
2885
+ type: 'button', className: 'cw-model-button cw-permission-button',
2886
+ title: `${t('permissionTitle')}: ${label}`, 'aria-label': t('permissionTitle'),
2887
+ 'aria-expanded': open, disabled: busy || status !== 'ready',
2888
+ onClick: () => {
2889
+ setError('');
2890
+ setOpen((value) => !value);
2891
+ },
2892
+ },
2893
+ h(Icon, { name: 'shield' }),
2894
+ h('span', { className: 'cw-model-button-label' }, label),
2895
+ h(Icon, { name: 'chevron' })),
2896
+ open ? ReactDOM.createPortal(
2897
+ h('div', {
2898
+ ref: menuRef, className: 'cw-root cw-model-menu cw-permission-menu',
2899
+ style: position || { visibility: 'hidden', left: 0, top: 0, width: 280 },
2900
+ role: 'dialog', 'aria-label': t('permissionTitle'),
2901
+ onKeyDown: (event) => {
2902
+ if (event.key === 'Escape') {
2903
+ event.preventDefault();
2904
+ setOpen(false);
2905
+ triggerRef.current?.focus();
2906
+ }
2907
+ },
2908
+ },
2909
+ h('div', { className: 'cw-model-group-label' }, t('permissionTitle')),
2910
+ options.map((option) => h('button', {
2911
+ type: 'button', key: option.value, className: 'cw-model-option',
2912
+ role: 'menuitemradio', 'aria-checked': option.value === currentValue,
2913
+ disabled: busy,
2914
+ onClick: () => choose(option),
2915
+ },
2916
+ h('span', { className: 'cw-model-option-label' }, permissionLabel(option, t)),
2917
+ option.value === 'auto' ? h('span', { className: 'cw-permission-badge' }, 'EXP') : null,
2918
+ option.value === currentValue ? h('span', { className: 'cw-model-option-check' }, '✓') : null,
2919
+ )),
2920
+ confirming ? h('div', { className: 'cw-permission-confirm' },
2921
+ confirming.value === 'auto' ? t('permissionConfirmAuto') : t('permissionConfirmFull'),
2922
+ h('div', { className: 'cw-permission-confirm-actions' },
2923
+ h('button', { type: 'button', onClick: () => setConfirming(null) }, t('permissionCancel')),
2924
+ h('button', {
2925
+ type: 'button', 'data-primary': 'true', disabled: busy,
2926
+ onClick: () => void apply(confirming),
2927
+ }, t('permissionEnable')),
2928
+ ),
2929
+ ) : null,
2930
+ error ? h('div', { className: 'cw-model-error' }, error) : null,
2931
+ ),
2932
+ document.body,
2933
+ ) : null,
2934
+ );
2935
+ }
2936
+
2937
+ function MetricTrack({ segments }) {
2938
+ return h('div', { className: 'cw-metric-track', 'aria-hidden': true },
2939
+ segments.map(({ id, width }) => h('span', {
2940
+ key: id, 'data-metric': id, style: { width: `${width}%` },
2941
+ })),
2942
+ );
2943
+ }
2944
+
2945
+ function ContextMeter({ meter, segments, label, t }) {
2946
+ return h(React.Fragment, null,
2947
+ h('div', { className: 'cw-metric-head' },
2948
+ h('span', null, `${label || t('contextUsed')} ${meter.percent}%`),
2949
+ h('span', null, `${meter.approximate ? '~' : ''}${formatTokenCount(meter.used)} / ${formatTokenCount(meter.limit)}`),
2950
+ ),
2951
+ h(MetricTrack, { segments: segments || [{ id: 'messages', width: meter.fill }] }),
2952
+ );
2953
+ }
2954
+
2955
+ function outputFileAddress(sessionId, cwd, path) {
2956
+ const absolute = path.replace(/\\/g, '/');
2957
+ const root = cwd.replace(/\\/g, '/').replace(/\/+$/, '');
2958
+ const relative = root && absolute.startsWith(`${root}/`) ? absolute.slice(root.length + 1) : absolute;
2959
+ const encodedPath = relative.split('/').map((part) => encodeURIComponent(part).replace(/%3A/gi, ':')).join('/');
2960
+ return `dsh-resource://file/session/${encodeURIComponent(sessionId)}/${encodedPath}`;
2961
+ }
2962
+
2963
+ const OUTPUT_IMAGE_TYPES = {
2964
+ avif: 'image/avif', bmp: 'image/bmp', gif: 'image/gif', ico: 'image/x-icon',
2965
+ jpeg: 'image/jpeg', jpg: 'image/jpeg', png: 'image/png', svg: 'image/svg+xml', webp: 'image/webp',
2966
+ };
2967
+ const MAX_THUMBNAIL_BYTES = 8 * 1024 * 1024;
2968
+
2969
+ function OutputIcon({ sessionId, file, mediaType }) {
2970
+ const ref = React.useRef(null);
2971
+ const [src, setSrc] = React.useState('');
2972
+ React.useEffect(() => {
2973
+ if (!mediaType || !(file.bytes > 0 && file.bytes <= MAX_THUMBNAIL_BYTES)) return;
2974
+ const controller = new AbortController();
2975
+ let url;
2976
+ const observer = new IntersectionObserver(([entry]) => {
2977
+ if (!entry.isIntersecting) return;
2978
+ observer.disconnect();
2979
+ // The native full-file reader also caps the response if the file has grown since stat.
2980
+ void runtime.workspaceFiles.readBytes(sessionId, file.path, {}, controller.signal).then((result) => {
2981
+ if (!controller.signal.aborted && result.ok && result.value.data.length <= MAX_THUMBNAIL_BYTES) {
2982
+ url = URL.createObjectURL(new Blob([new Uint8Array(result.value.data)], { type: mediaType }));
2983
+ setSrc(url);
2984
+ }
2985
+ }).catch(() => {});
2986
+ }, { root: ref.current.closest('.cw-context'), rootMargin: '48px' });
2987
+ observer.observe(ref.current);
2988
+ return () => {
2989
+ observer.disconnect();
2990
+ controller.abort();
2991
+ if (url) URL.revokeObjectURL(url);
2992
+ };
2993
+ }, [sessionId, file.path, file.bytes, mediaType]);
2994
+ return h('span', { className: 'cw-output-icon', ref, 'aria-hidden': true },
2995
+ src ? h('img', { src, alt: '', onError: () => setSrc('') }) : h(Icon, { name: mediaType ? 'image' : 'file' }),
2996
+ );
2997
+ }
2998
+
2999
+ function ContextOutputs({ sessionId, cwd, files, t }) {
3000
+ const [verified, setVerified] = React.useState(null);
3001
+ const [error, setError] = React.useState(false);
3002
+ React.useEffect(() => {
3003
+ if (!Array.isArray(files) || !runtime.workspaceFiles?.stat) return;
3004
+ const controller = new AbortController();
3005
+ const signal = controller.signal;
3006
+ async function verifyOutputs() {
3007
+ const available = [];
3008
+ const newest = files.slice().reverse();
3009
+ for (let offset = 0; offset < newest.length && !signal.aborted; offset += 8) {
3010
+ const batch = await Promise.allSettled(newest.slice(offset, offset + 8).map(async (path) => {
3011
+ const result = await runtime.workspaceFiles.stat(sessionId, path, signal);
3012
+ return result.ok ? { path, ...result.value } : null;
3013
+ }));
3014
+ for (const item of batch) if (item.status === 'fulfilled' && item.value) available.push(item.value);
3015
+ if (!signal.aborted && available.length) setVerified([...available]);
3016
+ }
3017
+ if (!signal.aborted) setVerified(available);
3018
+ }
3019
+ void verifyOutputs();
3020
+ return () => controller.abort();
3021
+ }, [sessionId, files]);
3022
+ if (!Array.isArray(files) || !runtime.workspaceFiles?.stat) return null;
3023
+
3024
+ return h('section', { className: 'cw-context-section' },
3025
+ h('h2', { className: 'cw-context-heading' }, t('contextOutputs')),
3026
+ verified?.length ? h('ul', { className: 'cw-output-list' }, verified.map((file) => {
3027
+ const name = file.path.split(/[\\/]/u).pop();
3028
+ const extension = /\.([a-z0-9]{1,8})$/iu.exec(name)?.[1].toLowerCase();
3029
+ const mediaType = extension && Object.hasOwn(OUTPUT_IMAGE_TYPES, extension) ? OUTPUT_IMAGE_TYPES[extension] : '';
3030
+ return h('li', { key: file.path },
3031
+ h('button', {
3032
+ type: 'button', className: 'cw-output-row', title: file.path,
3033
+ onClick: () => {
3034
+ try {
3035
+ runtime.sidebarRight.openResource(outputFileAddress(sessionId, cwd, file.path), { destination: 'sidebar' });
3036
+ setError(false);
3037
+ } catch (failure) {
3038
+ reportDiagnostic(`output preview failed: ${failure?.message || failure}`);
3039
+ setError(true);
3040
+ }
3041
+ },
3042
+ },
3043
+ h(OutputIcon, { key: file.version, sessionId, file, mediaType }),
3044
+ h('span', { className: 'cw-output-name' }, name),
3045
+ extension ? h('span', { className: 'cw-output-type', 'aria-hidden': true }, extension.toUpperCase()) : null),
3046
+ );
3047
+ })) : h('div', { className: 'cw-context-subtle' },
3048
+ t(verified === null && files.length ? 'contextOutputsLoading' : 'contextOutputsEmpty')),
3049
+ error ? h('p', { className: 'cw-context-subtle', role: 'alert' }, t('contextOutputOpenFailed')) : null,
3050
+ );
3051
+ }
3052
+
3053
+ function ContextPanelBase(props) {
3054
+ const t = (key) => translate(props, key);
3055
+ const state = props.useSessions((value) => value) || {};
3056
+ const session = props.useSession((value) => value);
3057
+ const row = state.byId?.[props.sessionId] || {};
3058
+ const title = row.blank ? t('newChat') : (row.title || t('untitled'));
3059
+ const model = useContextModel(props.sessionId);
3060
+ const meter = contextMeter(props.useProjection('contextPressure'));
3061
+ const breakdown = props.useProjection('contextBreakdown');
3062
+ const stats = props.useProjection('sessionStats');
3063
+ const outputFiles = props.useProjection('workbenchOutputs');
3064
+ const pending = session?.pendingSubmissions?.length ?? 0;
3065
+ const status = session?.running ? t('contextRunning') : t('contextIdle');
3066
+ const { categories, segments } = contextComposition(breakdown, meter);
3067
+ const cwd = row.cwd || '';
3068
+ const workspace = cwd.split(/[\\/]/u).filter(Boolean).pop() || t('contextWorkspace');
3069
+
3070
+ function rowView(label, value) {
3071
+ return h('div', { className: 'cw-context-row' },
3072
+ h('span', { className: 'cw-context-label' }, label),
3073
+ h('span', { className: 'cw-context-value', title: value }, value || '—'),
3074
+ );
3075
+ }
3076
+
3077
+ function legendRow(id, label, tokens, estimated) {
3078
+ return h(React.Fragment, { key: id },
3079
+ h('dt', { 'data-metric': id }, h('span', null, label)),
3080
+ h('dd', null, `${estimated ? '~' : ''}${formatTokenCount(tokens)}`),
3081
+ h('dd', null, meter ? `${(100 * tokens / meter.limit).toFixed(1)}%` : '—'),
3082
+ );
3083
+ }
3084
+
3085
+ return h('aside', { className: 'cw-root cw-context' },
3086
+ h('section', { className: 'cw-context-section' },
3087
+ h('h2', { className: 'cw-context-heading' }, t('contextEnvironment')),
3088
+ h('div', { className: 'cw-context-environment' }, h(Icon, { name: 'folder' }),
3089
+ h('div', { className: 'cw-context-environment-text' },
3090
+ h('span', null, workspace),
3091
+ cwd ? h('span', { className: 'cw-context-subtle', title: cwd }, cwd) : null,
3092
+ ),
3093
+ ),
3094
+ h('div', { className: 'cw-context-environment' }, h(Icon, { name: 'model' }),
3095
+ h('div', { className: 'cw-context-environment-text' },
3096
+ h('span', null, model.name || t('selectModel')),
3097
+ model.provider ? h('span', { className: 'cw-context-subtle' }, model.provider) : null,
3098
+ model.effort ? h('span', { className: 'cw-context-subtle' }, model.effort) : null,
3099
+ ),
3100
+ ),
3101
+ ),
3102
+ h('section', { className: 'cw-context-section' },
3103
+ h('h2', { className: 'cw-context-heading' }, t('contextTitle')),
3104
+ meter ? h(ContextMeter, { meter, segments, t }) : null,
3105
+ meter || categories.length ? h('dl', { className: 'cw-context-legend' },
3106
+ ...categories.map(([id, key, tokens]) => legendRow(id, t(key), tokens, true)),
3107
+ meter ? legendRow('free', t('contextFree'), Math.max(0, meter.limit - meter.used), meter.approximate) : null,
3108
+ ) : h('div', { className: 'cw-context-subtle' }, t('contextEmpty')),
3109
+ categories.length ? h('p', { className: 'cw-context-subtle cw-context-note' }, t('contextEstimate')) : null,
3110
+ ),
3111
+ h(ContextOutputs, { key: props.sessionId, sessionId: props.sessionId, cwd, files: outputFiles, t }),
3112
+ h('section', { className: 'cw-context-section' },
3113
+ h('details', { className: 'cw-context-details' },
3114
+ h('summary', null, h(Icon, { name: 'chevron' }), t('contextSession')),
3115
+ rowView(t('contextSession'), title),
3116
+ rowView(t('contextStatus'), status),
3117
+ rowView(t('contextQueued'), String(pending)),
3118
+ stats ? rowView(t('contextTurns'), String(stats.turns ?? 0)) : null,
3119
+ stats ? rowView(t('contextSteps'), String(stats.steps ?? 0)) : null,
3120
+ rowView(t('contextSessionId'), String(props.sessionId || '')),
3121
+ ),
3122
+ ),
3123
+ );
3124
+ }
3125
+
3126
+ const USAGE_WINDOW_LABELS = {
3127
+ fiveHours: 'usageFiveHours', sevenDays: 'usageSevenDays',
3128
+ monthTotal: 'usageMonthTotal',
3129
+ };
3130
+
3131
+ function formatMoney({ currency, amount }) {
3132
+ return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(amount);
3133
+ }
3134
+
3135
+ function ProviderUsageSection({ t }) {
3136
+ const [usage, setUsage] = React.useState(null);
3137
+
3138
+ React.useEffect(() => {
3139
+ const controller = new AbortController();
3140
+ fetch(new URL('api/workbench/providerUsage', document.baseURI), {
3141
+ cache: 'no-store', signal: controller.signal,
3142
+ }).then((response) => {
3143
+ if (!response.ok) throw new Error('Provider usage request failed');
3144
+ return response.json();
3145
+ }).then((result) => {
3146
+ if (!controller.signal.aborted) setUsage(result);
3147
+ }).catch(() => {
3148
+ if (!controller.signal.aborted) setUsage({});
3149
+ });
3150
+ return () => controller.abort();
3151
+ }, []);
3152
+
3153
+ if (!Array.isArray(usage) || usage.length === 0) return null;
3154
+ const locale = document.documentElement.lang || undefined;
3155
+ const resetFormat = new Intl.DateTimeFormat(locale, {
3156
+ month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit',
3157
+ });
3158
+ const percentFormat = new Intl.NumberFormat(locale, { style: 'percent', maximumFractionDigits: 1 });
3159
+ const creditFormat = new Intl.NumberFormat(locale, { maximumFractionDigits: 2 });
3160
+ const metric = (id, label, value) => h('div', {
3161
+ key: id, className: 'cw-metric-head cw-provider-balance',
3162
+ }, h('span', null, label), h('strong', null, value));
3163
+ const windowView = ({ id, ratio, resetAt, credits }) => h('div', { className: 'cw-provider-window', key: id },
3164
+ h('div', { className: 'cw-metric-head' },
3165
+ h('span', null, t(USAGE_WINDOW_LABELS[id])),
3166
+ h('span', null, `${resetAt > Date.now() ? `${resetFormat.format(resetAt)} ${t('usageReset')} · ` : ''}${t('contextUsed')} ${percentFormat.format(ratio)}`),
3167
+ ),
3168
+ h(MetricTrack, { segments: [{ id: ratio >= 0.95 ? 'quota-high' : 'messages', width: Math.min(100, 100 * ratio) }] }),
3169
+ credits ? h('p', { className: 'cw-context-subtle' }, t('usageCreditsRemaining').replace('{amount}',
3170
+ `${creditFormat.format(credits.remaining)} / ${creditFormat.format(credits.total)}`)) : null,
3171
+ );
3172
+
3173
+ return h('div', { className: 'cw-provider-usage', 'aria-live': 'polite' }, usage.map((provider) => {
3174
+ const name = provider.id === 'dreamina' ? t('usageDreaminaName') : provider.name;
3175
+ const duplicate = usage.some((other) => other !== provider && other.name === provider.name);
3176
+ return h('section', { key: provider.id },
3177
+ h('h3', { className: 'cw-metric-head' },
3178
+ h('span', null, duplicate ? `${name} · ${provider.id}` : name),
3179
+ provider.plan ? h('strong', null, provider.plan) : null,
3180
+ ),
3181
+ provider.balances?.length
3182
+ ? metric('balance', t('usageBalance'), provider.balances.map(formatMoney).join(' · ')) : null,
3183
+ provider.balance ? metric('extra', t('usageExtraBalance'), formatMoney(provider.balance)) : null,
3184
+ provider.charges?.map((charge) => metric(charge.id,
3185
+ t(charge.id === 'spent' ? 'usageKeySpent' : 'usageKeyRemaining'), formatMoney(charge))),
3186
+ provider.windows?.filter(({ id }) => Object.hasOwn(USAGE_WINDOW_LABELS, id)).map(windowView),
3187
+ Number.isFinite(provider.credits)
3188
+ ? metric('credits', t('usageDreaminaBalance'), creditFormat.format(provider.credits)) : null,
3189
+ provider.resetAt > Date.now() ? h('p', { className: 'cw-metric-head cw-context-subtle' },
3190
+ h('span', null, t('usageDreaminaReset')),
3191
+ h('time', { dateTime: new Date(provider.resetAt).toISOString() }, resetFormat.format(provider.resetAt)),
3192
+ ) : null,
3193
+ );
3194
+ }));
3195
+ }
3196
+
3197
+ function UsageControl({ useProjection, t }) {
3198
+ const meter = contextMeter(useProjection('contextPressure'));
3199
+ const usage = useProjection('tokenUsage');
3200
+ const [open, setOpen] = React.useState(false);
3201
+ const { triggerRef, menuRef, position } = useAnchoredPopup(open, 340, 660);
3202
+ const ready = Boolean(position);
3203
+ const popupId = React.useId();
3204
+ const rows = [
3205
+ ['messages', 'usageInput', usage?.uncachedInputTokens],
3206
+ ['output', 'usageOutput', usage?.outputTokens],
3207
+ ['read', 'usageCacheRead', usage?.cacheReadTokens],
3208
+ ['write', 'usageCacheWrite', usage?.cacheWriteTokens],
3209
+ ].filter(([, , tokens]) => Number.isFinite(tokens));
3210
+ const total = rows.reduce((sum, [, , tokens]) => sum + tokens, 0);
3211
+ const level = meter?.fill >= 95 ? 'high' : meter?.fill >= 80 ? 'warn' : undefined;
3212
+ const label = meter ? `${t('usageTitle')} · ${t('contextUsed')} ${meter.percent}%` : t('usageTitle');
3213
+
3214
+ React.useEffect(() => {
3215
+ if (!open) return undefined;
3216
+ const dismiss = (event) => {
3217
+ if (triggerRef.current?.contains(event.target) || menuRef.current?.contains(event.target)) return;
3218
+ setOpen(false);
3219
+ };
3220
+ const escape = (event) => {
3221
+ if (event.key !== 'Escape') return;
3222
+ event.preventDefault();
3223
+ setOpen(false);
3224
+ triggerRef.current?.focus();
3225
+ };
3226
+ document.addEventListener('pointerdown', dismiss);
3227
+ document.addEventListener('keydown', escape);
3228
+ return () => {
3229
+ document.removeEventListener('pointerdown', dismiss);
3230
+ document.removeEventListener('keydown', escape);
3231
+ };
3232
+ }, [open, triggerRef, menuRef]);
3233
+
3234
+ React.useEffect(() => {
3235
+ if (open && ready) menuRef.current?.focus({ preventScroll: true });
3236
+ }, [open, ready, menuRef]);
3237
+
3238
+ return h(React.Fragment, null,
3239
+ h('button', {
3240
+ ref: triggerRef, className: 'cw-icon-button cw-usage-trigger', type: 'button',
3241
+ 'data-level': level, title: label, 'aria-label': label, 'aria-expanded': open,
3242
+ 'aria-haspopup': 'dialog', 'aria-controls': open ? popupId : undefined,
3243
+ onClick: () => setOpen((value) => !value),
3244
+ },
3245
+ h('svg', { className: 'cw-usage-ring', viewBox: '0 0 20 20', fill: 'none', 'aria-hidden': true },
3246
+ h('circle', { cx: 10, cy: 10, r: 7, stroke: 'var(--cw-metric-ring)', strokeWidth: 2 }),
3247
+ meter?.fill > 0 ? h('circle', {
3248
+ cx: 10, cy: 10, r: 7, stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round',
3249
+ pathLength: 100, strokeDasharray: `${meter.fill} 100`, transform: 'rotate(-90 10 10)',
3250
+ }) : null,
3251
+ ),
3252
+ ),
3253
+ open ? ReactDOM.createPortal(
3254
+ h('div', {
3255
+ id: popupId, ref: menuRef, className: 'cw-root cw-usage-popover',
3256
+ style: position || { visibility: 'hidden', width: 340 },
3257
+ tabIndex: -1, role: 'dialog', 'aria-label': t('usageTitle'),
3258
+ },
3259
+ h('div', { className: 'cw-usage-header' },
3260
+ h('h2', null, t('usageTitle')),
3261
+ runtime.sidebarRight ? h('button', {
3262
+ type: 'button', className: 'cw-usage-link', onClick: () => {
3263
+ setOpen(false);
3264
+ runtime.sidebarRight.openTab(CONTEXT_TAB);
3265
+ },
3266
+ }, `${t('usageDetails')} →`) : null,
3267
+ ),
3268
+ h(ProviderUsageSection, { t }),
3269
+ meter ? h(ContextMeter, { meter, t, label: t('contextUsage') }) : null,
3270
+ total > 0 ? h('section', { className: 'cw-usage-breakdown' },
3271
+ h('div', { className: 'cw-metric-head' },
3272
+ h('span', null, t('usageTotal')),
3273
+ h('span', null, formatTokenCount(total)),
3274
+ ),
3275
+ h(MetricTrack, { segments: rows.filter(([, , tokens]) => tokens > 0).map(([id, , tokens]) => ({
3276
+ id, width: 100 * tokens / total,
3277
+ })) }),
3278
+ h('dl', null, rows.map(([id, key, tokens]) => h('div', { key: id, className: 'cw-context-row' },
3279
+ h('dt', { className: 'cw-context-label', 'data-metric': id }, t(key)),
3280
+ h('dd', { className: 'cw-context-value' }, formatTokenCount(tokens)),
3281
+ ))),
3282
+ ) : h('p', { className: 'cw-context-subtle cw-context-note' }, t('usageEmpty')),
3283
+ ),
3284
+ document.body,
3285
+ ) : null,
3286
+ );
3287
+ }
3288
+
3289
+ function BrandMark() {
3290
+ return h('svg', { viewBox: BRAND_VIEW_BOX, 'aria-hidden': 'true' },
3291
+ BRAND_RAYS.map((ray, index) => h('path', {
3292
+ key: ray.transform, ...ray, fill: index % 2 ? 'var(--cw-brand-ray-shade)' : 'var(--cw-brand-ray)',
3293
+ })),
3294
+ );
3295
+ }
3296
+
3297
+ function useAccountName() {
3298
+ const [name, setName] = React.useState('');
3299
+ React.useEffect(() => {
3300
+ const slots = runtime.ctx?.slots;
3301
+ if (!slots) return;
3302
+ const launcherSlot = 'settings.launcher';
3303
+ let unsubscribeAccount;
3304
+
3305
+ function connectAccount() {
3306
+ unsubscribeAccount?.();
3307
+ const launcher = slots.entriesOfSlot(launcherSlot).find((entry) => entry.locale === 'settings.account');
3308
+ const account = launcher?.inject?.()?.hooks?.account;
3309
+ const update = () => {
3310
+ const state = account?.getSnapshot();
3311
+ const profile = state?.details?.profile;
3312
+ const displayName = state?.view?.status === 'credential-stored' && profile?.status === 'ready'
3313
+ ? profile.value.name
3314
+ : undefined;
3315
+ setName(typeof displayName === 'string' ? displayName.trim() : '');
3316
+ };
3317
+ unsubscribeAccount = account?.subscribe(update);
3318
+ update();
3319
+ }
3320
+
3321
+ const unsubscribeSlot = slots.subscribe(launcherSlot, connectAccount);
3322
+ connectAccount();
3323
+ return () => {
3324
+ unsubscribeSlot();
3325
+ unsubscribeAccount?.();
3326
+ };
3327
+ }, []);
3328
+ return name;
3329
+ }
3330
+
3331
+ function greetingKey() {
3332
+ const hour = new Date().getHours();
3333
+ if (hour < 12) return 'greetingMorning';
3334
+ if (hour < 18) return 'greetingAfternoon';
3335
+ return 'greetingEvening';
3336
+ }
3337
+
3338
+ function HeroGreetingBase() {
3339
+ const name = useAccountName();
3340
+ const [key, setKey] = React.useState(greetingKey);
3341
+ React.useEffect(() => {
3342
+ const timer = setInterval(() => setKey(greetingKey()), 60_000);
3343
+ return () => clearInterval(timer);
3344
+ }, []);
3345
+ const greeting = translate({}, key);
3346
+ const text = name
3347
+ ? translate({}, 'greetingWithName').replace('{greeting}', () => greeting).replace('{name}', () => name)
3348
+ : greeting;
3349
+ return h('span', { className: 'cw-hero-greeting', role: 'heading', 'aria-level': 1 },
3350
+ h(BrandMark), h('span', null, text),
3351
+ );
3352
+ }
3353
+
3354
+ function ContextButton({ sessionId: id }) {
3355
+ const headerRef = React.useRef(null);
3356
+ const [activeKind, setActiveKind] = React.useState();
3357
+ const sidebar = runtime.sidebarRight;
3358
+ React.useEffect(() => {
3359
+ if (!id || !sidebar) return;
3360
+ const update = () => setActiveKind(sidebar.mounted.getSnapshot() === id && sidebar.isExpanded()
3361
+ ? sidebar.active()?.kind : undefined);
3362
+ // Membership changes have a service subscription. The tab strip reflects native focus and visibility.
3363
+ const observer = new MutationObserver((records) => {
3364
+ if (records.some(({ target }) => target.closest('[data-sidebar-right-session]'))) update();
3365
+ });
3366
+ observer.observe(document.body, {
3367
+ subtree: true,
3368
+ attributes: true,
3369
+ attributeFilter: ['data-sidebar-right-open', 'data-dockkit-pane-active', 'data-dockkit-float-active', 'aria-selected'],
3370
+ });
3371
+ const unsubscribeMounted = sidebar.mounted.subscribe(update);
3372
+ const unsubscribeTabs = sidebar.openTabs.subscribe(update);
3373
+ update();
3374
+ return () => {
3375
+ observer.disconnect();
3376
+ unsubscribeMounted();
3377
+ unsubscribeTabs();
3378
+ };
3379
+ }, [id, sidebar]);
3380
+
3381
+ React.useEffect(() => {
3382
+ const root = headerRef.current?.closest('.kiGsTW_root');
3383
+ const conversation = runtime.ctx.get('uiConversation');
3384
+ if (id && root && conversation && runtime.sessions.binding(id)) {
3385
+ return bindWorkSummaries(root, conversation.binding(id), runtime.ctx.locale);
3386
+ }
3387
+ }, [id]);
3388
+
3389
+ React.useEffect(() => {
3390
+ if (!id || !sidebar || runtime.contextAutoOpened) return undefined;
3391
+ if (window.innerWidth < 980) return undefined;
3392
+ const timer = setTimeout(() => {
3393
+ if (sidebar.mounted.getSnapshot() !== id) return;
3394
+ runtime.contextAutoOpened = true;
3395
+ try {
3396
+ if (!sidebar.isExpanded() && sidebar.tabsIn(id).every((tab) => tab.kind === 'guide')) {
3397
+ sidebar.openTab(CONTEXT_TAB);
3398
+ }
3399
+ } catch (error) {
3400
+ console.error('[claude-workbench] could not open session details', error);
3401
+ }
3402
+ }, 260);
3403
+ return () => clearTimeout(timer);
3404
+ }, [id, sidebar]);
3405
+
3406
+ const buttons = [
3407
+ { kind: CONTEXT_TAB, icon: 'context', label: translate({}, 'contextOpen') },
3408
+ { kind: 'terminal', icon: 'terminal', label: translate({}, 'headerTerminal') },
3409
+ { kind: 'browser', icon: 'browser', label: translate({}, 'headerBrowser') },
3410
+ { kind: 'files', icon: 'folder', label: translate({}, 'headerFiles') },
3411
+ ];
3412
+ return h('div', { ref: headerRef, className: 'cw-header-actions' }, buttons.map((item) =>
3413
+ h('button', {
3414
+ type: 'button', key: item.kind, className: 'cw-context-button',
3415
+ title: item.label, 'aria-label': item.label, 'aria-pressed': item.kind === activeKind,
3416
+ onClick: () => {
3417
+ if (!sidebar || sidebar.mounted.getSnapshot() !== id) return;
3418
+ try {
3419
+ if (sidebar.active()?.kind === item.kind) {
3420
+ sidebar.toggleExpanded();
3421
+ return;
3422
+ }
3423
+ const existing = sidebar.tabsIn(id).findLast((tab) => tab.kind === item.kind);
3424
+ if (existing) {
3425
+ sidebar.focus(existing.id);
3426
+ if (!sidebar.isExpanded()) sidebar.toggleExpanded();
3427
+ } else {
3428
+ sidebar.openTab(item.kind);
3429
+ }
3430
+ } catch (error) {
3431
+ console.error(`[claude-workbench] could not open ${item.kind}`, error);
3432
+ }
3433
+ },
3434
+ }, h(Icon, { name: item.icon }))));
3435
+ }
3436
+
3437
+ function ComposerAddMenu({ disabled, onImages, onFiles, onCommands, onContext, onPlugins, t }) {
3438
+ const [open, setOpen] = React.useState(false);
3439
+ const { triggerRef, menuRef, position } = useAnchoredPopup(open, 220);
3440
+
3441
+ React.useEffect(() => {
3442
+ if (!open) return undefined;
3443
+ const closeOutside = (event) => {
3444
+ if (triggerRef.current?.contains(event.target) || menuRef.current?.contains(event.target)) return;
3445
+ setOpen(false);
3446
+ };
3447
+ document.addEventListener('mousedown', closeOutside);
3448
+ return () => document.removeEventListener('mousedown', closeOutside);
3449
+ }, [open]);
3450
+
3451
+ const items = [
3452
+ { key: 'image', icon: 'plus', label: t('addImage'), action: onImages },
3453
+ { key: 'files', icon: 'folder', label: t('addFiles'), action: onFiles },
3454
+ { key: 'commands', icon: 'slash', label: t('addCommands'), action: onCommands },
3455
+ { key: 'context', icon: 'info', label: t('addContext'), action: onContext },
3456
+ { key: 'plugins', icon: 'panel', label: t('addPlugins'), action: onPlugins },
3457
+ ];
3458
+
3459
+ return h(React.Fragment, null,
3460
+ h('button', {
3461
+ ref: triggerRef, type: 'button',
3462
+ className: 'cw-icon-button cw-attach-button',
3463
+ title: t('addMenuTitle'), 'aria-label': t('addMenuTitle'),
3464
+ 'aria-expanded': open, disabled,
3465
+ onClick: () => setOpen((value) => !value),
3466
+ }, h(Icon, { name: 'plus' })),
3467
+ open ? ReactDOM.createPortal(
3468
+ h('div', {
3469
+ ref: menuRef, className: 'cw-root cw-model-menu',
3470
+ style: position || { visibility: 'hidden', left: 0, top: 0, width: 220 },
3471
+ role: 'menu', 'aria-label': t('addMenuTitle'),
3472
+ onKeyDown: (event) => {
3473
+ if (event.key === 'Escape') {
3474
+ event.preventDefault();
3475
+ setOpen(false);
3476
+ triggerRef.current?.focus();
3477
+ }
3478
+ },
3479
+ },
3480
+ h('div', { className: 'cw-model-group-label' }, t('addMenuTitle')),
3481
+ items.map((item) => h('button', {
3482
+ type: 'button', key: item.key, className: 'cw-model-option',
3483
+ role: 'menuitem', 'data-cw-menu-item': '',
3484
+ onClick: () => {
3485
+ setOpen(false);
3486
+ item.action?.();
3487
+ },
3488
+ },
3489
+ h(Icon, { name: item.icon }),
3490
+ h('span', { className: 'cw-model-option-label' }, item.label),
3491
+ )),
3492
+ ),
3493
+ document.body,
3494
+ ) : null,
3495
+ );
3496
+ }
3497
+
3498
+ function FolderControl({ cwd, disabled, t, onError }) {
3499
+ const [busy, setBusy] = React.useState(false);
3500
+ const label = String(cwd || '').split(/[\\/]/u).filter(Boolean).pop() || t('workspaceLabel');
3501
+ async function choose() {
3502
+ if (busy || disabled) return;
3503
+ setBusy(true);
3504
+ try {
3505
+ const path = await runtime.workspace?.pickDirectory?.();
3506
+ if (!path) return;
3507
+ const view = await runtime.workspaces?.create?.({ path });
3508
+ if (view?.workspaceId) await runtime.workspace?.openWorkspace?.(view.workspaceId);
3509
+ } catch (cause) {
3510
+ onError?.(`${t('workspaceOpenFailed')} ${errorMessage(cause)}`);
3511
+ } finally {
3512
+ setBusy(false);
3513
+ }
3514
+ }
3515
+ return h('button', {
3516
+ type: 'button', className: 'cw-model-button cw-permission-button cw-folder-button',
3517
+ title: cwd ? `${t('workspaceChange')}\n${cwd}` : t('workspaceChange'),
3518
+ 'aria-label': t('workspaceChange'), disabled: busy || disabled,
3519
+ onClick: () => void choose(),
3520
+ },
3521
+ h(Icon, { name: 'folder' }),
3522
+ h('span', { className: 'cw-model-button-label' }, label),
3523
+ );
3524
+ }
3525
+
3526
+ function ComposerBase(props) {
3527
+ const session = props.useSession((value) => value);
3528
+ const promptAllowed = props.useAccess('session/prompt') !== false;
3529
+ const cancelAllowed = props.useAccess('session/cancel') !== false;
3530
+ const sessionId = props.sessionId;
3531
+ const [text, setText] = React.useState(() => readDraft(sessionId));
3532
+ const readAttachments = React.useCallback(() => attachmentDrafts.read(sessionId), [sessionId]);
3533
+ const { images, files, submission: draftSubmission } =
3534
+ React.useSyncExternalStore(attachmentDrafts.subscribe, readAttachments);
3535
+ const [commandSending, setSending] = React.useState(false);
3536
+ const sending = commandSending || Boolean(draftSubmission);
3537
+ const [recognizing, setRecognizing] = React.useState(false);
3538
+ const [error, setError] = React.useState('');
3539
+ const [commandRows, setCommandRows] = React.useState([]);
3540
+ const [commandStatus, setCommandStatus] = React.useState('idle');
3541
+ const [commandError, setCommandError] = React.useState('');
3542
+ const [commandActive, setCommandActive] = React.useState(0);
3543
+ const [commandDismissed, setCommandDismissed] = React.useState(null);
3544
+ const [commandLauncherOpen, setCommandLauncherOpen] = React.useState(false);
3545
+ const [modelOpenRequest, setModelOpenRequest] = React.useState(0);
3546
+ const [caret, setCaret] = React.useState(0);
3547
+ const [mentionRows, setMentionRows] = React.useState([]);
3548
+ const [mentionStatus, setMentionStatus] = React.useState('idle');
3549
+ const [mentionActive, setMentionActive] = React.useState(0);
3550
+ const [mentionDismissed, setMentionDismissed] = React.useState(null);
3551
+ const composerRef = React.useRef(null);
3552
+ const fileInputRef = React.useRef(null);
3553
+ const uploadInputRef = React.useRef(null);
3554
+ const textareaRef = React.useRef(null);
3555
+ const voiceDraft = React.useRef({ sessionId, text, revision: 0, locked: true });
3556
+ const commandFetchedFor = React.useRef(null);
3557
+ const visionRequest = React.useRef(null);
3558
+ const sessionRef = React.useRef(sessionId);
3559
+ sessionRef.current = sessionId;
3560
+ const preference = React.useSyncExternalStore(subscribeComposerSettings, readBusyEnter);
3561
+ React.useEffect(() => {
3562
+ sessionRef.current = sessionId;
3563
+ return () => {
3564
+ sessionRef.current = undefined;
3565
+ visionRequest.current?.abort();
3566
+ };
3567
+ }, [sessionId]);
3568
+ React.useEffect(() => {
3569
+ setText(readDraft(sessionId));
3570
+ setSending(false);
3571
+ setRecognizing(false);
3572
+ setError('');
3573
+ setCommandRows([]);
3574
+ setCommandStatus('idle');
3575
+ setCommandError('');
3576
+ setCommandActive(0);
3577
+ setCommandDismissed(null);
3578
+ setCommandLauncherOpen(false);
3579
+ setModelOpenRequest(0);
3580
+ setCaret(0);
3581
+ setMentionRows([]);
3582
+ setMentionStatus('idle');
3583
+ setMentionActive(0);
3584
+ setMentionDismissed(null);
3585
+ commandFetchedFor.current = null;
3586
+ }, [sessionId]);
3587
+ // Acceptance can arrive while this composer is being reopened in another slot instance.
3588
+ React.useEffect(() => {
3589
+ if (!draftSubmission) setText(readDraft(sessionId));
3590
+ }, [sessionId, draftSubmission]);
3591
+
3592
+ React.useLayoutEffect(() => {
3593
+ const textarea = textareaRef.current;
3594
+ if (!textarea || globalThis.CSS?.supports('field-sizing', 'content')) return;
3595
+ textarea.style.height = 'auto';
3596
+ textarea.style.height = `${textarea.scrollHeight}px`;
3597
+ }, [text]);
3598
+ React.useEffect(() => {
3599
+ if (!commandLauncherOpen) return undefined;
3600
+ const dismiss = (event) => {
3601
+ if (!composerRef.current?.contains(event.target)) setCommandLauncherOpen(false);
3602
+ };
3603
+ document.addEventListener('mousedown', dismiss);
3604
+ return () => document.removeEventListener('mousedown', dismiss);
3605
+ }, [commandLauncherOpen]);
3606
+ const running = Boolean(session?.running);
3607
+ const unavailable = Boolean(props.disabled || props.blocked || !sessionId || !promptAllowed);
3608
+ const voiceLocked = unavailable || sending;
3609
+ const previousVoiceDraft = voiceDraft.current;
3610
+ voiceDraft.current = {
3611
+ sessionId, text, locked: voiceLocked,
3612
+ revision: previousVoiceDraft.sessionId === sessionId && previousVoiceDraft.text === text
3613
+ ? previousVoiceDraft.revision : previousVoiceDraft.revision + 1,
3614
+ };
3615
+ const voiceInputActions = React.useMemo(() => ({
3616
+ captureInsertion() {
3617
+ const draft = voiceDraft.current;
3618
+ const editor = textareaRef.current;
3619
+ return {
3620
+ sessionId: draft.sessionId,
3621
+ draftRev: draft.revision,
3622
+ start: editor?.selectionStart ?? draft.text.length,
3623
+ end: editor?.selectionEnd ?? draft.text.length,
3624
+ };
3625
+ },
3626
+ insertText(transcript, span) {
3627
+ const draft = voiceDraft.current;
3628
+ const editor = textareaRef.current;
3629
+ if (!editor || draft.locked || editor.value !== draft.text) return false;
3630
+ const next = voiceDraftInsertion(draft, span, transcript);
3631
+ if (!next) return false;
3632
+ voiceDraft.current = { ...draft, text: next.text, revision: draft.revision + 1 };
3633
+ setText(next.text);
3634
+ writeDraft(draft.sessionId, next.text);
3635
+ setCommandLauncherOpen(false);
3636
+ setCommandDismissed(null);
3637
+ setMentionDismissed(null);
3638
+ setCaret(next.caret);
3639
+ requestAnimationFrame(() => {
3640
+ if (textareaRef.current !== editor || editor.value !== next.text || editor.disabled) return;
3641
+ editor.focus();
3642
+ editor.setSelectionRange(next.caret, next.caret);
3643
+ });
3644
+ return true;
3645
+ },
3646
+ }), []);
3647
+ const t = (key) => translate(props, key);
3648
+ const canSend = Boolean(!unavailable && !sending && (text.trim() || images.length || files.length));
3649
+ const slashQuery = images.length === 0 ? commandQuery(text) : null;
3650
+ const commandMenuVisible = commandLauncherOpen || (slashQuery !== null && commandDismissed !== text);
3651
+ const commandChoices = commandLauncherOpen && text.trim() && slashQuery === null
3652
+ ? commandRows.filter((row) => !row.hasInput) : commandRows;
3653
+ const filteredCommands = filterCommandRows(commandChoices, commandLauncherOpen ? '' : (slashQuery ?? ''));
3654
+ const activeCommandIndex = filteredCommands.length === 0
3655
+ ? 0 : Math.min(commandActive, filteredCommands.length - 1);
3656
+ const activeCommand = filteredCommands[activeCommandIndex];
3657
+
3658
+ const sessionsState = typeof props.useSessions === 'function'
3659
+ ? props.useSessions((value) => value) : null;
3660
+ const cwd = sessionsState?.byId?.[sessionId]?.cwd || session?.cwd || '';
3661
+ const mention = mentionQuery(text, caret);
3662
+ const mentionToken = mention && text.slice(mention.start, caret);
3663
+ const mentionMenuVisible = mention !== null
3664
+ && !commandMenuVisible && slashQuery === null
3665
+ && mentionDismissed !== mentionToken
3666
+ && !unavailable;
3667
+ const activeMentionIndex = Math.min(mentionActive, Math.max(0, mentionRows.length - 1));
3668
+ const activeMention = mentionRows[activeMentionIndex];
3669
+ const mentionSearch = mentionMenuVisible ? mention.query : null;
3670
+ const mentionQuoted = mention?.quoted === true;
3671
+
3672
+ React.useEffect(() => {
3673
+ if (mentionSearch === null || !sessionId) return undefined;
3674
+ const controller = new AbortController();
3675
+ setMentionRows([]);
3676
+ setMentionActive(0);
3677
+ setMentionStatus('loading');
3678
+ const timer = setTimeout(async () => {
3679
+ const { signal } = controller;
3680
+ const results = await Promise.allSettled([
3681
+ Promise.resolve().then(() => runtime.fileReferences.list(sessionId, mentionSearch, signal)),
3682
+ mentionQuoted ? { ok: true, value: [] } : Promise.resolve().then(() =>
3683
+ runtime.sessionReferences.candidates(sessionId, mentionSearch, signal)),
3684
+ ]);
3685
+ if (signal.aborted) return;
3686
+ const catalogs = results.map((result) => result.status === 'fulfilled' && result.value?.ok
3687
+ ? result.value.value : undefined);
3688
+ setMentionRows(referenceRows(catalogs[0] || [], catalogs[1] || [], mentionQuoted));
3689
+ setMentionStatus(catalogs[0] || (!mentionQuoted && catalogs[1]) ? 'ready' : 'error');
3690
+ }, 100);
3691
+ return () => {
3692
+ clearTimeout(timer);
3693
+ controller.abort();
3694
+ };
3695
+ }, [mentionSearch, mentionQuoted, sessionId]);
3696
+
3697
+ React.useEffect(() => {
3698
+ if (!commandMenuVisible || !sessionId || commandFetchedFor.current === sessionId) return undefined;
3699
+ setCommandStatus('loading');
3700
+ setCommandError('');
3701
+ setCommandRows([]);
3702
+ const commands = runtime.commandsRemote;
3703
+ if (typeof commands?.list !== 'function') {
3704
+ setCommandStatus('error');
3705
+ setCommandError(t('commandsUnavailable'));
3706
+ return undefined;
3707
+ }
3708
+ let active = true;
3709
+ Promise.resolve().then(() => commands.list(sessionId)).then((result) => {
3710
+ if (!active) return;
3711
+ if (!result?.ok) throw new Error(errorMessage(result));
3712
+ commandFetchedFor.current = sessionId;
3713
+ const rows = commandRowsFromDescriptors(result.value);
3714
+ if (runtime.modelDirectories && !rows.some((row) => row.name === 'model')) {
3715
+ rows.push({ name: 'model', label: 'model', description: t('selectModel') });
3716
+ }
3717
+ setCommandRows(rows);
3718
+ setCommandStatus('ready');
3719
+ setCommandActive(0);
3720
+ }).catch((cause) => {
3721
+ if (!active) return;
3722
+ setCommandStatus('error');
3723
+ setCommandError(`${t('commandLoadFailed')} ${errorMessage(cause)}`);
3724
+ });
3725
+ return () => {
3726
+ active = false;
3727
+ };
3728
+ }, [commandMenuVisible, sessionId]);
3729
+
3730
+ function addImageFiles(fileList) {
3731
+ const available = Math.max(0, MAX_IMAGES - readAttachments().images.length);
3732
+ const incoming = Array.from(fileList || []).filter((file) => file?.type?.startsWith('image/')).slice(0, available);
3733
+ if (incoming.length === 0) return;
3734
+ if (incoming.some((file) => file.size > MAX_IMAGE_BYTES)) {
3735
+ setError(t('imageTooLarge'));
3736
+ return;
3737
+ }
3738
+ setError('');
3739
+ attachmentDrafts.replace(sessionId, 'images', (current) => [...current, ...incoming]);
3740
+ }
3741
+
3742
+ function onPaste(event) {
3743
+ const items = Array.from(event.clipboardData?.items || []);
3744
+ const pasted = items.filter((item) => item.kind === 'file').map((item) => item.getAsFile()).filter(Boolean);
3745
+ if (pasted.length === 0) return;
3746
+ event.preventDefault();
3747
+ addImageFiles(pasted.filter((file) => file.type.startsWith('image/')));
3748
+ addDraftFiles(pasted.filter((file) => !file.type.startsWith('image/')));
3749
+ }
3750
+
3751
+ function addDraftFiles(fileList) {
3752
+ const incoming = Array.from(fileList || []).filter(Boolean);
3753
+ if (incoming.length === 0) return;
3754
+ if (readAttachments().files.length + incoming.length > MAX_FILES) {
3755
+ setError(t('fileTooMany'));
3756
+ return;
3757
+ }
3758
+ if (incoming.some((file) => file.size > MAX_FILE_BYTES)) {
3759
+ setError(t('fileTooLarge'));
3760
+ return;
3761
+ }
3762
+ setError('');
3763
+ attachmentDrafts.replace(sessionId, 'files', (current) => [...current, ...incoming]);
3764
+ }
3765
+
3766
+ async function stageDraftFiles(outgoing) {
3767
+ const staged = [];
3768
+ if (outgoing.length === 0) return staged;
3769
+ const uploads = runtime.fileUploads;
3770
+ if (!uploads?.upload) throw new Error(t('fileUploadFailed'));
3771
+ try {
3772
+ for (const file of outgoing) {
3773
+ const data = await readAsBase64(file);
3774
+ const result = await uploads.upload(sessionId, { data, name: file.name });
3775
+ if (!result?.ok) throw new Error(errorMessage(result));
3776
+ staged.push({
3777
+ receiptId: result.value.receiptId,
3778
+ name: result.value.file?.name || file.name,
3779
+ bytes: result.value.file?.bytes ?? file.size,
3780
+ });
3781
+ }
3782
+ } catch (cause) {
3783
+ throw new Error(`${t('fileUploadFailed')} ${errorMessage(cause)}`, { cause });
3784
+ }
3785
+ return staged;
3786
+ }
3787
+
3788
+ async function runCommandLine(line, preserveDraft = false) {
3789
+ if (line === '/model' && runtime.modelDirectories) {
3790
+ setModelOpenRequest((current) => current + 1);
3791
+ } else {
3792
+ const commands = runtime.commandsRemote;
3793
+ if (typeof commands?.execute !== 'function') throw new Error(t('commandsUnavailable'));
3794
+ const outcome = await executeHostCommand(commands, sessionId, line);
3795
+ if (!outcome.ok) {
3796
+ throw new Error(outcome.error === 'unmatched' ? t('commandNotFound') : outcome.error);
3797
+ }
3798
+ }
3799
+ if (!preserveDraft) {
3800
+ if (readDraft(sessionId) === text) writeDraft(sessionId, '');
3801
+ if (sessionRef.current === sessionId && voiceDraft.current.text === text) setText('');
3802
+ }
3803
+ }
3804
+
3805
+ async function chooseCommand(row) {
3806
+ if (!row) return;
3807
+ const preserveDraft = commandLauncherOpen && slashQuery === null;
3808
+ setCommandLauncherOpen(false);
3809
+ setCommandDismissed(text);
3810
+ if (row.hasInput) {
3811
+ const insertion = commandInsertion(row);
3812
+ setText(insertion);
3813
+ writeDraft(sessionId, insertion);
3814
+ setCommandDismissed(insertion);
3815
+ requestAnimationFrame(() => textareaRef.current?.focus());
3816
+ return;
3817
+ }
3818
+ setSending(true);
3819
+ setError('');
3820
+ try {
3821
+ await runCommandLine(`/${row.name}`, preserveDraft);
3822
+ } catch (cause) {
3823
+ setError(errorMessage(cause));
3824
+ } finally {
3825
+ if (sessionRef.current === sessionId) setSending(false);
3826
+ }
3827
+ }
3828
+
3829
+ function chooseMention(entry) {
3830
+ if (!entry || !mention) return;
3831
+ const suffix = text.slice(mention.end);
3832
+ const spacing = /^\s/u.test(suffix) ? '' : ' ';
3833
+ const next = `${text.slice(0, mention.start)}${entry.mention}${spacing}${suffix}`;
3834
+ const nextCaret = mention.start + entry.mention.length + spacing.length;
3835
+ setText(next);
3836
+ writeDraft(sessionId, next);
3837
+ setMentionDismissed(null);
3838
+ setMentionActive(0);
3839
+ setCaret(nextCaret);
3840
+ requestAnimationFrame(() => {
3841
+ const node = textareaRef.current;
3842
+ if (!node) return;
3843
+ node.focus();
3844
+ node.setSelectionRange(nextCaret, nextCaret);
3845
+ });
3846
+ }
3847
+
3848
+ async function submit(alternate = false) {
3849
+ const draft = text;
3850
+ const { images: draftImages, files: draftFiles } = readAttachments();
3851
+ if (!canSend) return;
3852
+ setError('');
3853
+ const commandLine = draftImages.length === 0 && draftFiles.length === 0
3854
+ && draft.trim().startsWith('/')
3855
+ ? draft.trim() : null;
3856
+ if (commandLine) {
3857
+ setSending(true);
3858
+ try {
3859
+ await runCommandLine(commandLine);
3860
+ } catch (cause) {
3861
+ setError(errorMessage(cause));
3862
+ } finally {
3863
+ if (sessionRef.current === sessionId) setSending(false);
3864
+ }
3865
+ return;
3866
+ }
3867
+ const draftToken = attachmentDrafts.begin(sessionId);
3868
+ if (!draftToken) return;
3869
+ let submission;
3870
+ let accepted = false;
3871
+ let reference;
3872
+ const mode = submitMode(session, preference, alternate);
3873
+ try {
3874
+ const binding = runtime.sessions?.binding(sessionId);
3875
+ if (!binding) throw new Error(t('sessionNotReady'));
3876
+ reference = runtime.sessions.retain(sessionId, { source: 'workbenchPrompt' });
3877
+ await reference.ready;
3878
+ const outgoingImages = await Promise.all(draftImages.map((file) => imageFromFile(file, t)));
3879
+ const outgoingFiles = await stageDraftFiles(draftFiles);
3880
+ const send = (message, attachedImages, attachedFiles, signal) => {
3881
+ const content = [
3882
+ ...(message.trim() ? [{ type: 'text', text: message }] : []),
3883
+ ...attachedImages.map(({ mediaType, data, name }) => ({ type: 'image', mediaType, data, name })),
3884
+ ...attachedFiles.map(({ receiptId }) => ({ type: 'file', receiptId })),
3885
+ ];
3886
+ submission = binding.session.beginSubmission({
3887
+ mode,
3888
+ text: message,
3889
+ attachments: [
3890
+ ...attachedImages.map(({ previewUrl, name }) => ({ type: 'image', value: { previewUrl, name } })),
3891
+ ...attachedFiles.map(({ name, bytes }) => ({ type: 'file', value: { name, bytes } })),
3892
+ ],
3893
+ onAccept: () => {
3894
+ accepted = true;
3895
+ if (readDraft(sessionId) === draft) writeDraft(sessionId, '');
3896
+ if (sessionRef.current === sessionId && voiceDraft.current.text === draft) setText('');
3897
+ attachmentDrafts.finish(sessionId, draftToken, true);
3898
+ },
3899
+ });
3900
+ return binding.session.prompt(content, mode, signal, submission.requestId);
3901
+ };
3902
+
3903
+ let result = await send(draft, outgoingImages, outgoingFiles);
3904
+ if (needsImageRecognition(result, outgoingImages) && sessionRef.current === sessionId) {
3905
+ submission.abandon();
3906
+ submission = undefined;
3907
+ const request = new AbortController();
3908
+ visionRequest.current = request;
3909
+ setRecognizing(true);
3910
+ try {
3911
+ const description = await requestImageDescription(outgoingImages, draft, request.signal, t);
3912
+ const retainedFiles = [...outgoingFiles];
3913
+ if (result.error.code === 'session/attachment-invalid') {
3914
+ const uploads = runtime.fileUploads;
3915
+ if (!uploads?.upload) throw new Error(t('fileUploadFailed'));
3916
+ for (const { data, name } of outgoingImages) {
3917
+ request.signal.throwIfAborted();
3918
+ const upload = await uploads.upload(sessionId, { data, name }, request.signal);
3919
+ if (!upload?.ok) throw new Error(`${t('fileUploadFailed')} ${errorMessage(upload)}`);
3920
+ retainedFiles.push({
3921
+ receiptId: upload.value.receiptId,
3922
+ name: upload.value.file?.name || name,
3923
+ bytes: upload.value.file?.bytes,
3924
+ });
3925
+ }
3926
+ }
3927
+ request.signal.throwIfAborted();
3928
+ const message = `${draft.trim() ? draft : t('imageOnlyRequest')}\n\n[${t('imageRecognitionContext')}]\n${description}`;
3929
+ result = await send(message, [], retainedFiles, request.signal);
3930
+ } finally {
3931
+ if (visionRequest.current === request) {
3932
+ visionRequest.current = null;
3933
+ if (sessionRef.current === sessionId) setRecognizing(false);
3934
+ }
3935
+ }
3936
+ }
3937
+ if (!result?.ok) {
3938
+ throw new Error(errorMessage(result));
3939
+ }
3940
+ } catch (cause) {
3941
+ if (submission && !accepted) submission.abandon();
3942
+ if (sessionRef.current === sessionId) {
3943
+ setError(`${t('sendFailed')} ${errorMessage(cause)}`);
3944
+ }
3945
+ } finally {
3946
+ attachmentDrafts.finish(sessionId, draftToken);
3947
+ reference?.release();
3948
+ }
3949
+ }
3950
+
3951
+ async function stop() {
3952
+ if (!sessionId) return;
3953
+ setError('');
3954
+ try {
3955
+ const binding = runtime.sessions?.binding(sessionId);
3956
+ if (!binding) throw new Error(t('sessionNotReady'));
3957
+ const result = await binding.session.cancel();
3958
+ if (!result?.ok) throw new Error(errorMessage(result));
3959
+ } catch (cause) {
3960
+ setError(`${t('stopFailed')} ${errorMessage(cause)}`);
3961
+ }
3962
+ }
3963
+
3964
+ function onKeyDown(event) {
3965
+ if (event.isComposing || event.nativeEvent?.isComposing) return;
3966
+ if (mentionMenuVisible) {
3967
+ if (event.key === 'Escape') {
3968
+ event.preventDefault();
3969
+ setMentionDismissed(mentionToken);
3970
+ return;
3971
+ }
3972
+ if ((event.key === 'ArrowDown' || event.key === 'ArrowUp') && mentionRows.length > 0) {
3973
+ event.preventDefault();
3974
+ const step = event.key === 'ArrowDown' ? 1 : -1;
3975
+ setMentionActive((current) => (
3976
+ current + step + mentionRows.length
3977
+ ) % mentionRows.length);
3978
+ return;
3979
+ }
3980
+ if ((event.key === 'Enter' && !event.shiftKey || event.key === 'Tab') && activeMention) {
3981
+ event.preventDefault();
3982
+ chooseMention(activeMention);
3983
+ return;
3984
+ }
3985
+ }
3986
+ if (commandMenuVisible) {
3987
+ if (event.key === 'Escape') {
3988
+ event.preventDefault();
3989
+ setCommandLauncherOpen(false);
3990
+ setCommandDismissed(text);
3991
+ return;
3992
+ }
3993
+ if ((event.key === 'ArrowDown' || event.key === 'ArrowUp') && filteredCommands.length > 0) {
3994
+ event.preventDefault();
3995
+ const step = event.key === 'ArrowDown' ? 1 : -1;
3996
+ setCommandActive((current) => (
3997
+ current + step + filteredCommands.length
3998
+ ) % filteredCommands.length);
3999
+ return;
4000
+ }
4001
+ if ((event.key === 'Enter' && !event.shiftKey || event.key === 'Tab') && activeCommand) {
4002
+ event.preventDefault();
4003
+ void chooseCommand(activeCommand);
4004
+ return;
4005
+ }
4006
+ }
4007
+ if (event.key !== 'Enter' || event.shiftKey) return;
4008
+ event.preventDefault();
4009
+ void submit(event.metaKey || event.ctrlKey);
4010
+ }
4011
+
4012
+ if (!sessionId && props.onRequestWorkspace) {
4013
+ return h('div', { className: 'cw-root cw-composer' },
4014
+ h('button', { type: 'button', className: 'cw-workspace-button', onClick: props.onRequestWorkspace }, t('chooseWorkspace')));
4015
+ }
4016
+
4017
+ return h('div', { ref: composerRef, className: `cw-root cw-composer cw-composer-${props.variant || 'composer'}` },
4018
+ commandMenuVisible ? h('div', {
4019
+ className: 'cw-command-menu', role: 'listbox',
4020
+ 'aria-label': t('composerHint'),
4021
+ },
4022
+ commandStatus === 'loading'
4023
+ ? h('div', { className: 'cw-command-status' }, t('commandLoading')) : null,
4024
+ commandStatus === 'error'
4025
+ ? h('div', { className: 'cw-command-status', 'data-error': true }, commandError) : null,
4026
+ commandStatus === 'ready' && filteredCommands.length === 0
4027
+ ? h('div', { className: 'cw-command-status' }, t('commandNoMatch')) : null,
4028
+ filteredCommands.map((row, index) => h('button', {
4029
+ type: 'button', key: row.name,
4030
+ className: 'cw-command-row', role: 'option',
4031
+ 'aria-selected': index === activeCommandIndex,
4032
+ 'data-active': index === activeCommandIndex ? 'true' : undefined,
4033
+ onMouseEnter: () => setCommandActive(index),
4034
+ onClick: () => void chooseCommand(row),
4035
+ },
4036
+ h('span', { className: 'cw-command-name' }, `/${row.name}`),
4037
+ h('span', { className: 'cw-command-detail' }, row.description),
4038
+ row.hint ? h('span', { className: 'cw-command-hint' }, row.hint) : null,
4039
+ )),
4040
+ ) : null,
4041
+ mentionMenuVisible ? h('div', {
4042
+ className: 'cw-command-menu cw-mention-menu', role: 'listbox',
4043
+ 'aria-label': t('mentionTitle'),
4044
+ },
4045
+ h('div', { className: 'cw-mention-heading' }, t('mentionTitle')),
4046
+ mentionStatus === 'loading'
4047
+ ? h('div', { className: 'cw-command-status' }, t('mentionLoading')) : null,
4048
+ mentionStatus === 'error'
4049
+ ? h('div', { className: 'cw-command-status', 'data-error': true }, t('mentionUnavailable')) : null,
4050
+ mentionStatus === 'ready' && mentionRows.length === 0
4051
+ ? h('div', { className: 'cw-command-status' }, t('mentionNoMatch')) : null,
4052
+ mentionRows.map((entry, index) => h('button', {
4053
+ type: 'button', key: entry.key, className: 'cw-command-row', role: 'option',
4054
+ 'aria-selected': index === activeMentionIndex,
4055
+ 'data-active': index === activeMentionIndex ? 'true' : undefined,
4056
+ onMouseEnter: () => setMentionActive(index),
4057
+ onClick: () => chooseMention(entry),
4058
+ },
4059
+ h(Icon, { name: entry.icon }),
4060
+ h('span', { className: 'cw-command-name' }, entry.label),
4061
+ )),
4062
+ ) : null,
4063
+ h('div', { className: 'cw-composer-card' },
4064
+ h('input', {
4065
+ ref: fileInputRef, type: 'file', accept: 'image/*', multiple: true,
4066
+ style: { display: 'none' },
4067
+ onChange: (event) => {
4068
+ addImageFiles(event.target.files);
4069
+ event.target.value = '';
4070
+ },
4071
+ }),
4072
+ h('input', {
4073
+ ref: uploadInputRef, type: 'file', multiple: true,
4074
+ style: { display: 'none' },
4075
+ onChange: (event) => {
4076
+ addDraftFiles(event.target.files);
4077
+ event.target.value = '';
4078
+ },
4079
+ }),
4080
+ files.length > 0 ? h('div', { className: 'cw-composer-files' },
4081
+ files.map((file, index) => h('div', {
4082
+ className: 'cw-composer-file', key: `${file.name}-${index}`,
4083
+ },
4084
+ h(Icon, { name: 'file' }),
4085
+ h('span', { className: 'cw-composer-file-name', title: file.name }, file.name),
4086
+ h('span', { className: 'cw-composer-file-size' }, formatBytes(file.size)),
4087
+ h('button', {
4088
+ type: 'button', title: t('removeFile'), 'aria-label': `${t('removeFile')}: ${file.name}`,
4089
+ disabled: sending,
4090
+ onClick: () => attachmentDrafts.replace(sessionId, 'files', (current) =>
4091
+ current.filter((_, itemIndex) => itemIndex !== index)),
4092
+ }, h(Icon, { name: 'close' })),
4093
+ )),
4094
+ ) : null,
4095
+ images.length > 0 ? h('div', { className: 'cw-composer-images' },
4096
+ images.map((image, index) => h('div', { className: 'cw-composer-image', key: `${image.name}-${index}` },
4097
+ h(DraftImage, { file: image, fallback: t('image') }),
4098
+ h('button', {
4099
+ type: 'button', title: t('removeImage'), 'aria-label': t('removeImage'),
4100
+ disabled: sending,
4101
+ onClick: () => attachmentDrafts.replace(sessionId, 'images', (current) =>
4102
+ current.filter((_, itemIndex) => itemIndex !== index)),
4103
+ }, h(Icon, { name: 'close' })),
4104
+ )),
4105
+ ) : null,
4106
+ h('div', { className: 'cw-composer-row' },
4107
+ h(ComposerAddMenu, {
4108
+ disabled: unavailable || sending,
4109
+ t,
4110
+ onImages: () => {
4111
+ if (images.length < MAX_IMAGES) fileInputRef.current?.click();
4112
+ },
4113
+ onFiles: () => {
4114
+ if (files.length < MAX_FILES) uploadInputRef.current?.click();
4115
+ },
4116
+ onCommands: () => {
4117
+ setCommandLauncherOpen(true);
4118
+ setCommandActive(0);
4119
+ setCommandDismissed(null);
4120
+ requestAnimationFrame(() => textareaRef.current?.focus());
4121
+ },
4122
+ onContext: () => {
4123
+ try {
4124
+ runtime.sidebarRight?.openTab(CONTEXT_TAB);
4125
+ } catch (error) {
4126
+ console.error('[claude-workbench] could not open context', error);
4127
+ }
4128
+ },
4129
+ onPlugins: () => {
4130
+ try {
4131
+ runtime.ctx?.get('layout')?.selectPanel('plugins');
4132
+ } catch (error) {
4133
+ console.error('[claude-workbench] could not open plugins', error);
4134
+ }
4135
+ },
4136
+ }),
4137
+ h('textarea', {
4138
+ ref: textareaRef,
4139
+ className: 'cw-composer-input', rows: 1, value: text,
4140
+ onChange: (event) => {
4141
+ setText(event.target.value);
4142
+ writeDraft(sessionId, event.target.value);
4143
+ setCommandLauncherOpen(false);
4144
+ setCommandDismissed(null);
4145
+ setMentionDismissed(null);
4146
+ setCaret(event.target.selectionStart ?? 0);
4147
+ },
4148
+ onSelect: (event) => setCaret(event.currentTarget.selectionStart ?? 0),
4149
+ onKeyDown, onPaste,
4150
+ placeholder: unavailable ? t('inputDisabled') : (props.placeholder || t('inputPlaceholder')),
4151
+ 'aria-label': t('inputPlaceholder'),
4152
+ disabled: unavailable || sending,
4153
+ }),
4154
+ sessionId ? h('div', { className: 'cw-composer-voice' },
4155
+ props.renderSlot(WORKBENCH_VOICE_SLOT, {
4156
+ inputActions: voiceInputActions,
4157
+ locked: voiceLocked,
4158
+ onActiveChange: ignoreVoiceActivity,
4159
+ }),
4160
+ ) : null,
4161
+ h('div', { className: 'cw-composer-actions' },
4162
+ sending ? h('span', { className: 'cw-composer-state', role: 'status' },
4163
+ t(recognizing ? 'imageRecognizing' : 'sending')) : null,
4164
+ running && cancelAllowed ? h('button', {
4165
+ type: 'button', className: 'cw-send-button cw-stop-button', onClick: () => void stop(),
4166
+ title: t('stop'), 'aria-label': t('stop'),
4167
+ }, h(Icon, { name: 'stop' })) : null,
4168
+ h('button', {
4169
+ type: 'button', className: 'cw-send-button', disabled: !canSend,
4170
+ onClick: () => void submit(),
4171
+ title: running ? `${t(submitMode(session, preference, false))}\n${t('busySendHint')}` : t('send'),
4172
+ 'aria-label': running ? t(submitMode(session, preference, false)) : t('send'),
4173
+ }, h(Icon, { name: 'up' })),
4174
+ ),
4175
+ ),
4176
+ props.accessory || null,
4177
+ error ? h('div', { className: 'cw-composer-error', role: 'alert' }, error) : null,
4178
+ props.blocked?.reason ? h('div', { className: 'cw-composer-error' }, props.blocked.reason) : null,
4179
+ ),
4180
+ h('div', { className: 'cw-composer-footer' },
4181
+ h('div', { className: 'cw-composer-hint' }, t('composerHint')),
4182
+ h('div', { className: 'cw-footer-controls' },
4183
+ h(FolderControl, { cwd, disabled: unavailable || sending, t, onError: (message) => setError(message) }),
4184
+ h(PermissionControl, { props, sessionId, t }),
4185
+ h(ModelControl, { props, sessionId, t, openRequest: modelOpenRequest }),
4186
+ sessionId ? h(UsageControl, { key: sessionId, useProjection: props.useProjection, t }) : null,
4187
+ ),
4188
+ ),
4189
+ );
4190
+ }
4191
+
4192
+ const HeroGreeting = withBoundary(HeroGreetingBase);
4193
+ const ToolCallMessage = withBoundary(ToolCallMessageBase);
4194
+ const Composer = withBoundary(ComposerBase);
4195
+ const ContextPanel = withBoundary(ContextPanelBase);
4196
+ const HeaderContextButton = withBoundary(ContextButton);
4197
+
4198
+ function registerComposer(slots) {
4199
+ const disposeComposer = slots.register({
4200
+ name: 'conversation.composer.bar', priority: -1,
4201
+ children: { [WORKBENCH_VOICE_SLOT]: { kind: 'single', scope: 'session' } },
4202
+ }, Composer);
4203
+ let voiceEntry;
4204
+ let disposeVoice;
4205
+ function syncVoice() {
4206
+ // DSH owns the original activity slot. Reuse its microphone contribution
4207
+ // in a Workbench child slot without redeclaring the native slot.
4208
+ const entry = slots.entriesOfSlot(NATIVE_INPUT_ACTIVITY)[0];
4209
+ if (entry === voiceEntry) return;
4210
+ disposeVoice?.();
4211
+ disposeVoice = undefined;
4212
+ voiceEntry = entry;
4213
+ if (entry?.locale !== 'voice-input') return;
4214
+ try {
4215
+ disposeVoice = slots.register({
4216
+ name: WORKBENCH_VOICE_SLOT, locale: entry.locale, inject: entry.inject,
4217
+ }, entry.component);
4218
+ } catch (error) {
4219
+ reportDiagnostic(`voice input activation failed: ${error?.message || error}`);
4220
+ }
4221
+ }
4222
+ const unsubscribe = slots.subscribe(NATIVE_INPUT_ACTIVITY, syncVoice);
4223
+ syncVoice();
4224
+ return () => {
4225
+ unsubscribe();
4226
+ disposeVoice?.();
4227
+ disposeComposer();
4228
+ };
4229
+ }
4230
+
4231
+ return {
4232
+ inject: [
4233
+ 'slots', 'locale', 'shortcuts', 'configForms', 'uiWorkspace', 'workspaces', 'sessions', 'remote',
4234
+ ],
4235
+ ...(typeof window !== 'undefined' && window.__CLAUDE_WORKBENCH_TEST__
4236
+ ? { testHooks: { commandQuery, filterCommandRows, commandRowsFromDescriptors, commandInsertion, executeHostCommand, summarizeToolCall, summarizeWorkGroup, isDangerousPermission, rootModelGroup, formatTokenCount, contextPercent, mentionQuery, referenceRows, submitMode, formatBytes, voiceDraftInsertion, createSessionHistory, createAttachmentDrafts } }
4237
+ : {}),
4238
+ apply(ctx) {
4239
+ runtime.ctx = ctx;
4240
+ runtime.workspace = ctx.get('uiWorkspace');
4241
+ runtime.workspaces = ctx.get('workspaces');
4242
+ runtime.sessions = ctx.get('sessions');
4243
+ runtime.composerSettings = ctx.configForms.get('ui-conversation');
4244
+ for (const [property, namespace] of [
4245
+ ['commandsRemote', 'commands'], ['fileReferences', 'fileReferences'],
4246
+ ['sessionReferences', 'sessionReferenceResolver'], ['permissionPresets', 'permissionPresets'],
4247
+ ['fileUploads', 'fileUploads'], ['workspaceFiles', 'workspaceFiles'],
4248
+ ]) {
4249
+ ctx.inject([`remote.${namespace}`], (scope) => {
4250
+ scope.effect(() => {
4251
+ const service = scope.remote?.[namespace];
4252
+ runtime[property] = service;
4253
+ return () => {
4254
+ if (runtime[property] === service) runtime[property] = undefined;
4255
+ };
4256
+ }, `claude-workbench: ${namespace}`);
4257
+ });
4258
+ }
4259
+ runtime.shortcuts = ctx.get('shortcuts');
4260
+ runtime.modelDirectories = ctx.get('modelDirectories');
4261
+ if (runtime.workspace?.selection) {
4262
+ ctx.effect(() => {
4263
+ runtime.sessionHistory = createSessionHistory(runtime.workspace, runtime.sessions, runtime.workspaces,
4264
+ () => ctx.get('layout')?.beginNavigation?.());
4265
+ return () => {
4266
+ runtime.sessionHistory.dispose();
4267
+ runtime.sessionHistory = undefined;
4268
+ };
4269
+ }, 'claude-workbench: session visit history');
4270
+ }
4271
+ ctx.effect(() => {
4272
+ const style = document.createElement('style');
4273
+ style.dataset.pluginCss = '@local/claude-code-workbench';
4274
+ style.textContent = CSS;
4275
+ document.head.appendChild(style);
4276
+ document.documentElement.setAttribute('data-workbench-theme', '');
4277
+ return () => {
4278
+ document.documentElement.removeAttribute('data-workbench-theme');
4279
+ style.remove();
4280
+ };
4281
+ }, 'claude-workbench: presentation styles');
4282
+ try {
4283
+ ctx.inject?.(['layout'], (scope) => {
4284
+ // Seed the native width once when the frame is created. Dragging uses the same action.
4285
+ scope.get('layout')?.panels?.setRightbar?.(RIGHT_SIDEBAR_MIN_WIDTH);
4286
+ });
4287
+ } catch (error) {
4288
+ reportDiagnostic(`right sidebar default width failed: ${error?.message || error}`);
4289
+ }
4290
+ try {
4291
+ if (ctx.inject) {
4292
+ ctx.inject(['modelDirectories'], (scope) => {
4293
+ runtime.modelDirectories = scope.modelDirectories;
4294
+ scope.effect(() => () => {
4295
+ if (runtime.modelDirectories === scope.modelDirectories) runtime.modelDirectories = undefined;
4296
+ }, 'claude-workbench: model directories');
4297
+ });
4298
+ }
4299
+ } catch (error) {
4300
+ reportDiagnostic(`model directory injection failed: ${error?.message || error}`);
4301
+ }
4302
+ try {
4303
+ ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'claude-workbench: locale');
4304
+ runtime.t = ctx.locale.bind(NS);
4305
+ ctx.effect(() => {
4306
+ const locale = ctx.locale;
4307
+ const translate = locale.translate;
4308
+ if (typeof translate !== 'function') return;
4309
+ let active = true;
4310
+ // The native status supplies its own localized elapsed time and shimmer.
4311
+ function translateRunningStatus(namespace, key, params) {
4312
+ if (active && namespace === 'chat') {
4313
+ if (key === 'chat.deepDiving') return 'ThinKing';
4314
+ if (key === 'chat.deepDivingFor' && params?.duration) return `ThinKing · ${params.duration}`;
4315
+ }
4316
+ return translate.call(this, namespace, key, params);
4317
+ }
4318
+ locale.translate = translateRunningStatus;
4319
+ return () => {
4320
+ active = false;
4321
+ if (locale.translate === translateRunningStatus) locale.translate = translate;
4322
+ };
4323
+ }, 'claude-workbench: running status label');
4324
+ } catch (error) {
4325
+ reportDiagnostic(`locale registration failed: ${error?.message || error}`);
4326
+ }
4327
+ try {
4328
+ ctx.inject(['theme'], (scope) => {
4329
+ scope.effect(() => scope.theme.overrideTokens('@local/claude-code-workbench', THEME_TOKENS),
4330
+ 'claude-workbench: appearance');
4331
+ });
4332
+ } catch (error) {
4333
+ reportDiagnostic(`theme registration failed: ${error?.message || error}`);
4334
+ }
4335
+
4336
+ function install(label, register) {
4337
+ try {
4338
+ return register();
4339
+ } catch (error) {
4340
+ reportDiagnostic(`${label} registration failed: ${error?.message || error}`);
4341
+ return () => {};
4342
+ }
4343
+ }
4344
+
4345
+ function guardInjection(label, register) {
4346
+ return () => {
4347
+ try {
4348
+ return register();
4349
+ } catch (error) {
4350
+ reportDiagnostic(`${label} activation failed: ${error?.message || error}`);
4351
+ return () => {};
4352
+ }
4353
+ };
4354
+ }
4355
+
4356
+ ctx.effect(() => {
4357
+ let active = true;
4358
+ const disposers = [];
4359
+ const timer = setTimeout(() => {
4360
+ if (!active) return;
4361
+ disposers.push(
4362
+ install('image generation settings', () => ctx.slots.inject(
4363
+ 'plugins.bundle.config',
4364
+ guardInjection('image generation settings', () => ctx.slots.register({
4365
+ name: 'plugins.bundle.config', key: 'dsh-fps-workbench', locale: NS,
4366
+ }, withBoundary(ImageGenerationSettings))),
4367
+ )),
4368
+ install('new session greeting', () => ctx.slots.inject(
4369
+ 'conversation.hero.brand.mark',
4370
+ guardInjection('new session greeting', () => ctx.slots.register({
4371
+ name: 'conversation.hero.brand.mark', priority: -1, locale: NS,
4372
+ }, HeroGreeting)),
4373
+ )),
4374
+ install('sidebar settings', () => ctx.slots.inject(
4375
+ 'sidebar.footer.action',
4376
+ guardInjection('sidebar settings', () => ctx.slots.register({
4377
+ name: 'sidebar.footer.action', id: 'workbench-settings', order: 50,
4378
+ }, SidebarSettingsButton)),
4379
+ )),
4380
+ install('chat nodes', () => ctx.slots.inject(
4381
+ 'conversation.chat.node',
4382
+ guardInjection('chat nodes', () => ctx.slots.register({
4383
+ name: 'conversation.chat.node', key: 'tool-call', priority: -1,
4384
+ }, ToolCallMessage)),
4385
+ )),
4386
+ install('composer', () => ctx.slots.inject(
4387
+ 'conversation.composer.bar',
4388
+ guardInjection('composer', () => registerComposer(ctx.slots)),
4389
+ )),
4390
+ install('context tab', () => {
4391
+ const tabs = ctx.get('sidebarRightTabs');
4392
+ runtime.sidebarRight = ctx.get('sidebarRight');
4393
+ if (!tabs) return () => {};
4394
+ const disposeType = ctx.effect(() => tabs.register({
4395
+ id: CONTEXT_TAB,
4396
+ kind: CONTEXT_TAB,
4397
+ keepMounted: true,
4398
+ title: () => translate({}, 'contextTitle'),
4399
+ }), 'claude-workbench: context tab type');
4400
+ const disposeBody = ctx.slots.inject(
4401
+ 'sidebar.right.pane.tab',
4402
+ guardInjection('context body', () => ctx.slots.register({
4403
+ name: 'sidebar.right.pane.tab', key: CONTEXT_TAB,
4404
+ }, ContextPanel)),
4405
+ );
4406
+ const disposeHeader = ctx.slots.inject(
4407
+ 'conversation.session.header.utilities',
4408
+ guardInjection('session header utilities', () => ctx.slots.register({
4409
+ name: 'conversation.session.header.utilities', id: 'workbench-panes', order: 50,
4410
+ }, HeaderContextButton)),
4411
+ );
4412
+ return () => {
4413
+ disposeHeader();
4414
+ disposeBody();
4415
+ disposeType();
4416
+ };
4417
+ }),
4418
+ );
4419
+ }, 0);
4420
+ return () => {
4421
+ active = false;
4422
+ clearTimeout(timer);
4423
+ for (const dispose of disposers) {
4424
+ try {
4425
+ dispose();
4426
+ } catch (error) {
4427
+ console.error('[claude-workbench] cleanup failed', error);
4428
+ }
4429
+ }
4430
+ };
4431
+ }, 'claude-workbench: delayed slot registration');
4432
+ },
4433
+ };
4434
+ } catch (error) {
4435
+ console.error('[claude-workbench] Client factory failed', error);
4436
+ return {
4437
+ inject: ['slots'],
4438
+ apply(ctx) {
4439
+ try {
4440
+ ctx.effect(() => ctx.slots.register({
4441
+ name: 'conversation.composer.dock', id: 'claude-workbench-load-error', order: 9000,
4442
+ }, () => null), 'claude-workbench: inert fallback');
4443
+ } catch (registrationError) {
4444
+ console.error('[claude-workbench] fallback registration failed', registrationError);
4445
+ }
4446
+ },
4447
+ };
4448
+ }
4449
+ },
4450
+ });