@aibolabs/presentation-workbench 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/bundle.mjs +12 -0
- package/capability.js +18 -0
- package/code-highlight.d.ts +2 -0
- package/code-highlight.js +19 -0
- package/conversation.js +114 -0
- package/git.js +59 -0
- package/inspector.js +27 -0
- package/links.d.ts +5 -0
- package/links.js +35 -0
- package/markdown.d.ts +20 -0
- package/markdown.js +80 -0
- package/message-attachments.js +14 -0
- package/metadata.js +23 -0
- package/navigation.js +30 -0
- package/package.json +48 -0
- package/rich-text.js +53 -0
- package/session-references.js +23 -0
- package/timeline-model.d.ts +13 -0
- package/timeline-model.js +81 -0
- package/timeline.js +71 -0
- package/tree.js +6 -0
- package/workbench.js +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 chldu2000
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @aibolabs/presentation-workbench
|
|
2
|
+
|
|
3
|
+
两套独立皮肤共用的纯数据工作台呈现模块。模块只将公开 Presentation 数据和
|
|
4
|
+
宿主动作目录转换成视觉树,不引用宿主代码、DOM、框架或业务执行端口。
|
|
5
|
+
|
|
6
|
+
当前提供 `renderNavigation`、`renderConversation`、`renderGit`、`renderInspector`,
|
|
7
|
+
参数均为 `(state, actions)`;`renderCapability(state, actions, renderSemantic)`
|
|
8
|
+
接收皮肤自己的语义 renderer。
|
|
9
|
+
调用方拥有样式;模块只生成语义结构、可访问性属性及当前目录中的动作绑定。
|
|
10
|
+
0.2.0 随双皮肤 0.3.0 交付导航、会话、Git、Inspector 与能力视图的完整工作台;当前版本 0.2.2 随双皮肤 0.4.1 交付。
|
|
11
|
+
执行权限、附件状态、诊断、会话绑定、三种布局、草稿以及跨默认呈现的焦点/滚动
|
|
12
|
+
恢复均由宿主提供数据与权限;包只决定呈现。macOS arm64 原生生命周期与
|
|
13
|
+
Chromium 浏览器交互已有验收记录,未宣称其他 OS 或所有屏幕阅读器通过认证。
|
|
14
|
+
|
|
15
|
+
`renderWorkbench(input, renderSemantic)` 装配三栏布局的语义结构,外层样式由皮肤
|
|
16
|
+
提供。`workbenchSource()` 使用 esbuild 打包本包固定入口与已安装依赖,不执行输入源码、
|
|
17
|
+
不读取宿主源码,也不在构建时下载依赖。生成自包含 Worker 脚本;安装本包时需安装其声明依赖。
|
|
18
|
+
|
|
19
|
+
`./markdown` 使用 Marked lexer 导出纯数据解析与目标提取,内置与外部呈现共享:
|
|
20
|
+
标题 1–6 级、有序起始编号、嵌套/任务列表、引用、分隔线、GFM 表格与列对齐、
|
|
21
|
+
粗体/斜体/删除线的嵌套、行内代码、自动/引用式链接,以及围栏/缩进代码块。
|
|
22
|
+
代码通过 lowlight 生成安全高亮节点;未知语言和超过 50,000 字符的块保留完整原文。
|
|
23
|
+
链接只接受显式 Markdown 目标:HTTP(S)、mailto,以及工作区文件(相对路径、绝对路径、`file://`,
|
|
24
|
+
可带 `#L10`、`#L10-L20` 或 `:10:5` 行列位置);普通文字不会被猜测为文件链接。
|
|
25
|
+
包内模块 `links.js`(不单独导出,供富文本绘制使用)的 `linkTarget` 负责分类,并提供网页、邮件、本地文件的类型图标(0.2.2 起)。
|
|
26
|
+
文件链接绑定宿主 `openLink` 动作,由宿主校验工作区边界后在预览面板打开。原始 HTML 作为文本显示,Markdown 图片作为命名链接,
|
|
27
|
+
不自动请求远程图片。公式和 Mermaid 暂不专用渲染。
|
|
28
|
+
块 index 按深度优先分配,引用与列表内部的代码和链接也绑定宿主动作;
|
|
29
|
+
流式未闭合代码块可显示,复制始终使用解析得到的原始代码而不是高亮节点。
|
|
30
|
+
`renderRichText` 只绘制安全视觉树,链接和复制代码按钮必须匹配当前宿主 token。
|
|
31
|
+
未提供 token 时按钮禁用,不回退为任意 URL 或包自有剪贴板调用。
|
|
32
|
+
|
|
33
|
+
工具消息以原文 `pre` 显示调用参数或输出,避免 Markdown 改写命令和 diff;
|
|
34
|
+
推理消息使用稳定消息键的原生折叠区域,状态标签保留生成中、失败和中断等区别。
|
|
35
|
+
连续工具按组显示数量与完成数;系统消息按宿主 `groupSystemItems` 提示分组,
|
|
36
|
+
推理、分支摘要和压缩摘要保持独立。内置呈现与插件共用 `./timeline-model` 的纯数据规则。
|
|
37
|
+
|
|
38
|
+
`renderConversation` 在消息与输入框之间渲染当前会话的待处理审批,只显示与 `state.session`
|
|
39
|
+
匹配的请求。按钮只绑定目录中的 `resolveApproval` token:带选项的请求按选项作答,
|
|
40
|
+
拒绝类在前;否则按 `availableDecisions` 显示允许/拒绝。没有 token 时不渲染按钮。
|
|
41
|
+
|
|
42
|
+
编辑器通过 `submitOnEnter` 使用 Enter 发送,运行时绑定宿主可用的排队发送;
|
|
43
|
+
Shift/Ctrl+Enter 换行。没有对应动作时回车不提交,输入法组合确认不触发发送。
|
|
44
|
+
|
|
45
|
+
可选 `data.layout`/`layoutActions` 让导航及辅助列消费宿主宽度,布局菜单提供
|
|
46
|
+
键盘/鼠标范围滑块。缺少合同的旧输入仍使用皮肤默认宽度。尺寸通过受限数值提示
|
|
47
|
+
交给皮肤 CSS;分栏边界支持鼠标拖动及方向键/Home/End 调整。
|
|
48
|
+
|
|
49
|
+
列宽、辅助面板开关及当前视图由宿主按窗口持久化,插件无需自行存储或恢复。
|
|
50
|
+
|
|
51
|
+
标准、专注和审阅布局消费宿主 mode;审阅交换两侧和拖动方向,专注仅保留内容。
|
|
52
|
+
模式菜单使用宿主选择令牌,沿用默认工作台的切换流程和模式持久化。
|
|
53
|
+
|
|
54
|
+
路径和斜杠命令建议支持上下键、Enter 确认、Escape 收起;路径另支持 Tab。
|
|
55
|
+
命令按名称、别名、描述过滤,确认后光标移到补全文本末尾。键盘引用已有宿主
|
|
56
|
+
动作;命令分类支持 Tab/Shift+Tab 和鼠标切换。分类是选项列表之外的筛选按钮,
|
|
57
|
+
编辑器关联到实际 listbox。会话历史有命名和焦点入口,支持键盘滚动。
|
|
58
|
+
|
|
59
|
+
建议列表打开时普通 Enter 优先确认选项,Shift/Ctrl+Enter 仍换行,与内置编辑器一致;鼠标选择
|
|
60
|
+
建议也会回到编辑器,光标位于补全文本末尾。
|
|
61
|
+
|
|
62
|
+
桌面会话历史与编辑区独立滚动,附件详情可展开;默认视图与外置视图按消息标识
|
|
63
|
+
恢复位置。外置核心语义不冒充内置专业阅读,存在专业偏好时显示通用降级提示。
|
package/bundle.mjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {build} from 'esbuild';
|
|
2
|
+
import {fileURLToPath} from 'node:url';
|
|
3
|
+
|
|
4
|
+
/** Bundle the fixed workbench entry and installed dependencies for the Worker. */
|
|
5
|
+
export async function workbenchSource() {
|
|
6
|
+
const result = await build({
|
|
7
|
+
stdin: {contents: "export {renderWorkbench} from './workbench.js';", resolveDir: fileURLToPath(new URL('.', import.meta.url))},
|
|
8
|
+
bundle: true, write: false, format: 'iife', globalName: 'aiboWorkbenchModule',
|
|
9
|
+
platform: 'browser', target: 'es2022', minify: true,
|
|
10
|
+
});
|
|
11
|
+
return result.outputFiles[0].text + '\nself.aiboWorkbench=aiboWorkbenchModule.renderWorkbench;\n';
|
|
12
|
+
}
|
package/capability.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {node,button,section,text,actionFor} from './tree.js';
|
|
2
|
+
/** The skin supplies its semantic renderer; only host-bound semantic actions are forwarded. */
|
|
3
|
+
export function renderCapability(state,actions,renderSemantic){
|
|
4
|
+
const find=(operation,...args)=>actionFor(actions,operation,...args);
|
|
5
|
+
const controls=[];
|
|
6
|
+
for(const [operation,label] of [['close','关闭能力视图'],['reload','重新加载'],['toggleLayout','切换布局'],['toggleReading',state.view.enhanced?'使用通用阅读':'尝试专业阅读']]){const action=find(operation);if(action)controls.push(button('capability:'+operation,label,action));}
|
|
7
|
+
const children=[node('nav','capability:catalog',null,state.catalog.map(item=>button('capability:open:'+item.installationId+':'+item.contributionId,item.title,find('open',item.installationId,item.contributionId),{title:item.issue??item.title})))];
|
|
8
|
+
if(state.selected){
|
|
9
|
+
children.push(node('h2','capability:selected-title',state.selected.title),node('nav','capability:controls',null,controls),text('capability:error',state.view.error),state.view.restoring?text('capability:restoring','正在恢复视图'):null);
|
|
10
|
+
if(state.view.snapshot){
|
|
11
|
+
const snapshot=state.view.snapshot;
|
|
12
|
+
if(snapshot.view.kind==='detail'&&state.view.enhanced)children.push(node('p','capability:reading-fallback','专业阅读界面不可用,已使用通用视图。',[],{role:'status'}));
|
|
13
|
+
const semanticActions=actions.filter(action=>action.operation==='semantic').map(entry=>{const action=JSON.parse(entry.args[0]);return {token:entry.token,action,label:snapshot.actions.find(item=>item.id===action.actionId)?.label??action.actionId};});
|
|
14
|
+
children.push(renderSemantic({snapshot,actions:semanticActions}));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return section('capability','能力视图',children);
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {common, createLowlight} from 'lowlight';
|
|
2
|
+
|
|
3
|
+
const highlighter = createLowlight(common);
|
|
4
|
+
const cache = new Map();
|
|
5
|
+
|
|
6
|
+
// Explicit languages only; large/unknown blocks remain complete plain text.
|
|
7
|
+
export function highlightCode(value, language) {
|
|
8
|
+
if (value.length > 50000 || !language || !highlighter.registered(language)) return [{value}];
|
|
9
|
+
const key = language + '\n' + value;
|
|
10
|
+
if (cache.has(key)) return cache.get(key);
|
|
11
|
+
const convert = nodes => nodes.map(node => node.type === 'text' ? {value: node.value}
|
|
12
|
+
: {className: (node.properties.className ?? []).join(' '), children: convert(node.children)});
|
|
13
|
+
let result;
|
|
14
|
+
try { result = convert(highlighter.highlight(language, value).children); }
|
|
15
|
+
catch { return [{value}]; }
|
|
16
|
+
if (cache.size >= 32) cache.delete(cache.keys().next().value);
|
|
17
|
+
cache.set(key, result);
|
|
18
|
+
return result;
|
|
19
|
+
}
|
package/conversation.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {splitMessageAttachments} from './message-attachments.js';
|
|
2
|
+
import {node,button,field,section,text,actionFor} from './tree.js';
|
|
3
|
+
import {renderExecutionProfile,renderMessageAttachment,renderSessionMetadata} from './metadata.js';
|
|
4
|
+
import {splitSessionReferences} from './session-references.js';
|
|
5
|
+
import {renderTimeline} from './timeline.js';
|
|
6
|
+
const labels={pauseGoal:'暂停目标',resumeGoal:'恢复目标',clearGoal:'清除目标',send:'发送',stop:'停止',retry:'重试',queueSteer:'立即发送',queueFollowUp:'排队发送',clearQueue:'清空队列',resumeQueue:'继续队列',addAttachments:'添加附件',addDirectory:'添加目录',loadOlder:'加载更早消息',fork:'分叉会话',loadModels:'刷新模型',compact:'压缩上下文',openTree:'会话树',closeTree:'关闭会话树',refreshTree:'刷新会话树',submitAnswers:'提交回答',cancelAnswers:'取消回答'};
|
|
7
|
+
export function renderConversation(state,actions){
|
|
8
|
+
const find=(operation,...args)=>actionFor(actions,operation,...args);
|
|
9
|
+
const controls=(operations)=>operations.flatMap(operation=>{const action=actions.find(a=>a.operation===operation&&!a.args.length);return action?[button('conversation:action:'+operation,labels[operation],action)]:[]});
|
|
10
|
+
const children=[node('header','conversation:header',null,[node('h1','conversation:title',state.session?.label??'选择或创建会话'),text('conversation:activity',state.activityLabel),node('nav','conversation:tools',null,controls(['fork','compact','openTree']))])];
|
|
11
|
+
if(state.session?.historyOnly)children.push(text('conversation:history-only','插件数据已清除,此会话仅保留历史。'));
|
|
12
|
+
children.push(renderSessionMetadata(state.session,'conversation:session-metadata'));
|
|
13
|
+
const messages=renderTimeline(state.timelineVisibleCount>0?state.timeline.slice(-state.timelineVisibleCount):[],actions,state.groupSystemItems===true,state.attachments);
|
|
14
|
+
children.push({...node('section','conversation:timeline',null,[...controls(['loadOlder']),...messages],{'aria-label':'会话消息'}),className:'timeline'});
|
|
15
|
+
if(state.retryReason||state.retryPrompt)children.push(section('conversation:retry','重试',[text('retry:reason',state.retryReason),text('retry:prompt',state.retryPrompt),...controls(['retry'])]));
|
|
16
|
+
for(const request of state.userInputRequests){
|
|
17
|
+
const questions=request.questions.map(question=>{
|
|
18
|
+
const prefix='question:'+request.requestId+':'+question.id;
|
|
19
|
+
const value=state.answerDrafts[JSON.stringify([request.sessionId,request.requestId,question.id,request.turnId])]??'';
|
|
20
|
+
const options=question.options.map(option=>{
|
|
21
|
+
const action=find('chooseAnswer',request.requestId,question.id,option.label);
|
|
22
|
+
return button(prefix+':option:'+option.label,option.label,action,{'aria-pressed':String(value===option.label),title:option.description??option.label});
|
|
23
|
+
});
|
|
24
|
+
return section(prefix,question.header??question.question,[question.header?text(prefix+':prompt',question.question):null,...options,(question.isOther||!question.options.length)?field(prefix+':answer',question.question,value,find('answer',request.requestId,question.id),true):null]);
|
|
25
|
+
});
|
|
26
|
+
children.push(section('request:'+request.requestId,'需要你的回答',[...questions,...['submitAnswers','cancelAnswers'].map(operation=>{const action=find(operation,request.requestId);return action?button('request:'+request.requestId+':'+operation,labels[operation],action):null;})]));
|
|
27
|
+
}
|
|
28
|
+
for(const request of (state.approvalRequests??[]).filter(request=>request.sessionId===state.session?.id)){
|
|
29
|
+
const prefix='approval:'+request.requestId;
|
|
30
|
+
// Reject kinds come first, matching the default workbench; decisions answer providers without options.
|
|
31
|
+
const choices=request.options.length?[...request.options].sort((a,b)=>a.kind===b.kind?0:a.kind==='reject'?-1:1).map(option=>['option',option.id,option.label??(option.kind==='allow'?'允许':'拒绝')]):request.availableDecisions.map(decision=>['decision',decision,decision==='accept'?'允许':'拒绝']);
|
|
32
|
+
children.push(section(prefix,'需要确认',[text(prefix+':kind',request.kind),request.command?text(prefix+':command',request.command):null,request.cwd?text(prefix+':cwd',request.cwd):null,
|
|
33
|
+
...choices.map(([kind,value,label])=>{const action=find('resolveApproval',request.requestId,kind,value);return action?button(prefix+':'+kind+':'+value,label,action):null;})]));
|
|
34
|
+
}
|
|
35
|
+
if(state.queue && (state.queue.items?.length || state.queue.steering.length || state.queue.followUp.length))children.push(section('conversation:queue','待处理消息',[
|
|
36
|
+
state.queue.paused?text('queue:paused','自动发送已暂停'):null,
|
|
37
|
+
...(state.queue.items?.length?state.queue.items.map(item=>section('queue:item:'+item.id,item.status==='sending'?'正在发送':item.status==='uncertain'?'投递结果未知':item.status==='failed'?'发送失败':'等待发送',[
|
|
38
|
+
text('queue:text:'+item.id,splitSessionReferences(item.text).body.split('[AIBO_CONTEXT_ATTACHMENTS]')[0].trim()),
|
|
39
|
+
...splitMessageAttachments(item.text,state.attachments).attachments.map(attachment=>renderMessageAttachment(attachment,'queue:'+item.id+':attachment:'+attachment.id)),
|
|
40
|
+
item.error?text('queue:error:'+item.id,item.error):null,
|
|
41
|
+
(!state.running || state.session?.capabilities.includes('queue.steer')) ? button('queue:send:'+item.id,'立即发送',find('sendQueuedMessage',item.id)) : null,
|
|
42
|
+
button('queue:remove:'+item.id,'删除',find('removeQueuedMessage',item.id)),
|
|
43
|
+
])):[...state.queue.steering.map((value,i)=>text('queue:steer:'+i,'引导:'+value)),...state.queue.followUp.map((value,i)=>text('queue:follow:'+i,'后续:'+value))]),
|
|
44
|
+
...controls(['resumeQueue','clearQueue'])]));
|
|
45
|
+
if(state.session){
|
|
46
|
+
const draftField=field('conversation:draft','消息',state.draft,find('draft'),true);
|
|
47
|
+
const submit=find(state.running?'queueFollowUp':'send');
|
|
48
|
+
draftField.children[1].submitOnEnter=true;
|
|
49
|
+
if(submit)draftField.children[1].primaryEnter=submit.token;
|
|
50
|
+
const goal = state.goal && state.goal.status !== 'cleared' ? {...node('details','conversation:goal',null,[
|
|
51
|
+
node('summary','goal:summary',state.goal.objective),
|
|
52
|
+
text('goal:status',state.goal.status === 'active' ? (state.running ? '目标进行中' : '目标待继续') : state.goal.status === 'paused' && state.running ? '目标已暂停,当前回合尚未结束' : ({paused:'目标已暂停',completed:'目标已完成',blocked:'目标受阻',usageLimited:'额度受限',budgetLimited:'目标预算已耗尽'})[state.goal.status] ?? '目标状态未知'),
|
|
53
|
+
text('goal:budget',state.goal.tokenBudget == null ? null : `Token:${state.goal.tokensUsed??0} / ${state.goal.tokenBudget}`),
|
|
54
|
+
...controls(['pauseGoal','resumeGoal','clearGoal']),
|
|
55
|
+
],{open:true,'aria-label':'当前目标'}),className:'goal-bar'} : null;
|
|
56
|
+
const composer=[goal,draftField,text('conversation:shortcut','Enter '+(state.running?'排队发送':'发送')+' · Shift/Ctrl+Enter 换行'),state.draftFailed?node('p','conversation:draft-error','草稿保存失败',[],{role:'alert'}):null];
|
|
57
|
+
|
|
58
|
+
composer.push(node('nav','conversation:composer-tools',null,controls(['addAttachments','addDirectory','send','stop','queueSteer','queueFollowUp'])));
|
|
59
|
+
const pendingAttachments=state.attachments.filter(item=>item.turnId===null && item.sessionId===state.session.id);
|
|
60
|
+
const attachmentList=node('ul','conversation:attachment-list',null,pendingAttachments.map(item=>node('li','attachment:'+item.id,null,[renderMessageAttachment(item,'composer:attachment:'+item.id),button('attachment:remove:'+item.id,'移除附件 '+item.path,find('removeAttachment',item.id))])));
|
|
61
|
+
if(pendingAttachments.length)composer.push(node('details','conversation:attachments',null,[node('summary','conversation:attachments:summary','附件 · '+pendingAttachments.length),attachmentList],{open:true}));
|
|
62
|
+
const mention=/(?:^|\s)@[^\s]*$/.test(state.draft);
|
|
63
|
+
const slash=state.draft.match(/^\/([^\s]*)$/);
|
|
64
|
+
const categoryOf=command=>command.category??(command.source==='skill'?'skill':command.source==='extension'||command.source==='prompt'?'extension':'agent');
|
|
65
|
+
const matches=slash?state.agentCommands.filter(command=>command.enabled!==false&&[command.name,...(command.aliases??[]),command.description??''].join(' ').toLocaleLowerCase().includes(slash[1].toLocaleLowerCase())):state.agentCommands;
|
|
66
|
+
const groups=slash?[['all','All'],['agent','Agent'],['skill','Skills'],['extension','Extension']].map(([id,label])=>({id,label,commands:matches.filter(command=>id==='all'||categoryOf(command)===id)})):[];
|
|
67
|
+
const commands=slash?[...new Map(groups.flatMap(group=>group.commands).map(command=>[command.name,command])).values()]:matches;
|
|
68
|
+
const pathButtons=state.workspacePathSuggestions.slice(0,24).map(item=>button('path:'+item.path,item.path+(item.isDirectory?'/':''),find('selectPath',item.path),{role:'option'}));
|
|
69
|
+
const commandButtons=commands.map(command=>button('command:'+command.name,'/'+command.name,find('selectCommand',command.name),{title:command.description??command.name,role:slash?'option':'button'}));
|
|
70
|
+
const sessionButtons=(state.sessionSuggestions??[]).map(item=>button('session-reference:'+item.id,'会话 · '+item.label+' · '+item.agent+(item.archived?' · 已归档':''),find('selectSessionReference',item.id),{role:'option'}));
|
|
71
|
+
const mentionGroups=mention?[['all','All'],['files','Files'],['folders','Folders'],['sessions','Sessions']].map(([id,label])=>({id,label,buttons:id==='sessions'?sessionButtons:id==='files'?pathButtons.filter((_,index)=>!state.workspacePathSuggestions[index].isDirectory):id==='folders'?pathButtons.filter((_,index)=>state.workspacePathSuggestions[index].isDirectory):[...sessionButtons,...pathButtons]})) : [];
|
|
72
|
+
const mentionButtons=mention?[...new Map(mentionGroups.flatMap(group=>group.buttons).map(item=>[item.key,item])).values()]:[];
|
|
73
|
+
if(mention)composer.push(node('div','conversation:mentions',null,[node('div','mentions:categories',null,mentionGroups.map(group=>node('button','mentions:category:'+group.id,group.label+' ('+group.buttons.length+')',[],{type:'button','aria-pressed':String(group.id==='all')})),{role:'group','aria-label':'引用分类'}),node('div','conversation:mention-options',null,mentionButtons,{role:'listbox','aria-label':'引用建议'})],{role:'group','aria-label':'引用补全'}));
|
|
74
|
+
if(slash||state.agentCommands.length)composer.push(slash?node('div','conversation:commands',null,[node('div','commands:categories',null,groups.map(group=>node('button','commands:category:'+group.id,group.label+' ('+group.commands.length+')',[],{type:'button','aria-pressed':String(group.id==='all')})),{role:'group','aria-label':'命令分类'}),node('div','conversation:command-options',null,commandButtons,{role:'listbox','aria-label':'命令建议'})],{role:'group','aria-label':'命令补全'}):node('details','conversation:commands',null,[node('summary','commands:title','命令'),...commandButtons]));
|
|
75
|
+
const suggestionButtons=mention?mentionButtons:slash?commandButtons:[];
|
|
76
|
+
if(suggestionButtons.length||mention||slash&&groups.length)draftField.children[1].suggestions={listKey:mention?'conversation:mentions':'conversation:commands',keys:suggestionButtons.filter(button=>button.events?.click).map(button=>button.key),confirmWithTab:false,confirmWithPrimary:mention,...(mention?{categories:mentionGroups.map(group=>({key:'mentions:category:'+group.id,options:group.buttons.filter(button=>button.events?.click).map(button=>button.key)}))}:slash?{categories:groups.map(group=>({key:'commands:category:'+group.id,options:group.commands.filter(command=>find('selectCommand',command.name)).map(command=>'command:'+command.name)}))}:{})};
|
|
77
|
+
const matrixModels=(state.modelCatalog?.models??[]).map(model=>section('model:'+model.reference,model.label,[text('model:description:'+model.reference,model.description),button('model:default:'+model.reference,'默认推理强度',find('selectModel',model.reference,null),{'aria-pressed':String(state.modelCatalog?.current?.reference===model.reference&&!state.modelConfiguration.selectedReasoningEffort)}),...model.reasoningEfforts.map(effort=>button('model:effort:'+model.reference+':'+effort.id,effort.label,find('selectModel',model.reference,effort.id),{'aria-pressed':String(state.modelCatalog?.current?.reference===model.reference&&state.modelConfiguration.selectedReasoningEffort===effort.id),title:effort.description??effort.label}))]));
|
|
78
|
+
const sequential=state.modelCatalog?.parameterScope==='current-model';
|
|
79
|
+
const currentModel=state.modelCatalog?.current;
|
|
80
|
+
const currentEfforts=currentModel?.reasoningEfforts??[];
|
|
81
|
+
const selectedEffort=currentEfforts.find(option=>option.id===state.modelConfiguration.currentReasoningEffort);
|
|
82
|
+
const models=sequential?[
|
|
83
|
+
section('models:selection','模型',(state.modelCatalog?.models??[]).map(model=>button('model:select:'+model.reference,model.label,find('selectModel',model.reference,null),{'aria-pressed':String(currentModel?.reference===model.reference),title:model.description??model.label}))),
|
|
84
|
+
section('models:reasoning','推理强度',currentEfforts.length?currentEfforts.map(effort=>button('model:effort:'+currentModel.reference+':'+effort.id,effort.label,find('selectModel',currentModel.reference,effort.id),{'aria-pressed':String(state.modelConfiguration.selectedReasoningEffort===effort.id),title:effort.description??effort.label})):[text('models:no-reasoning',state.modelCatalogLoading?'正在更新…':'此模型无可选推理强度')]),
|
|
85
|
+
]:matrixModels;
|
|
86
|
+
const fastTier=state.modelCatalog?.current?.serviceTiers?.find(tier=>tier.label?.trim().toLowerCase()==='fast');
|
|
87
|
+
const fastAction=fastTier?find('selectServiceTier',state.modelCatalog?.currentServiceTier===fastTier.id?'default':fastTier.id):null;
|
|
88
|
+
const contextAction=actions.find(action=>action.operation==='selectContextWindow');
|
|
89
|
+
const contextOptions=state.modelCatalog?.current?.contextWindows??[];
|
|
90
|
+
const contextCurrent=state.modelCatalog?.currentContextWindow;
|
|
91
|
+
const contextKnown=contextOptions.some(option=>option.id===contextCurrent);
|
|
92
|
+
const contextSelect=node('label','models:context-label',null,[text('models:context-title','上下文'),{
|
|
93
|
+
...node('select','models:context',null,[...(!contextKnown?[node('option','models:context:unknown',contextOptions.length?'未提供当前值':'不支持',[],{value:'',disabled:true,selected:true})]:[]),...contextOptions.map(option=>node('option','models:context:'+option.id,option.label,[],{value:option.id,selected:option.id===contextCurrent}))],{'aria-label':'模型上下文大小',disabled:!contextAction,value:contextKnown?contextCurrent:''}),
|
|
94
|
+
...(contextAction?{events:{change:contextAction.token}}:{}),
|
|
95
|
+
}]);
|
|
96
|
+
composer.push(node('details','conversation:models' ,null,[node('summary','models:title',(currentModel?.label??'模型')+(selectedEffort?' · '+selectedEffort.label:'')),...controls(['loadModels']),fastAction?button('models:fast','⚡ '+fastTier.label,fastAction,{'aria-pressed':String(state.modelCatalog?.currentServiceTier===fastTier.id),title:fastTier.description??fastTier.label}):null,contextSelect,state.modelCatalogLoading?text('models:loading','正在加载模型'):null,...models]));
|
|
97
|
+
composer.push(node('details','conversation:access',null,[node('summary','access:title','会话设置'),...actions.filter(a=>a.operation==='selectAccess').map(action=>button('access:'+action.args[0],state.executionProfile?.sessionControls?.find(option=>option.id===action.args[0])?.label??action.args[0],action)),renderExecutionProfile(state.executionProfile,'conversation:execution-profile')]));
|
|
98
|
+
if(state.usage){
|
|
99
|
+
const usage=state.usage;
|
|
100
|
+
const percent=usage.contextUsed!==null&&usage.contextLimit?Math.min(100,Math.round(usage.contextUsed/usage.contextLimit*100)):null;
|
|
101
|
+
const limits=(usage.limits??[]).map(limit=>(limit.label??(limit.windowMinutes?limit.windowMinutes+' 分钟':'套餐'))+'剩余 '+Math.max(0,100-Math.round(limit.usedPercent))+'%');
|
|
102
|
+
const credits=usage.credits?.unlimited?'Credits 不限量':usage.credits?.balance?'Credits '+usage.credits.balance:null;
|
|
103
|
+
composer.push(node('p','conversation:usage',[percent===null?usage.contextUsed===null?null:'上下文 '+usage.contextUsed:'上下文 '+percent+'%'+(usage.contextEstimated?'(估算)':''),usage.total===null?null:'Token '+usage.total,usage.plan?.toUpperCase(),...limits,credits].filter(Boolean).join(' · '),[],{'aria-label':'会话用量与套餐余量'}));
|
|
104
|
+
}
|
|
105
|
+
children.push(section('conversation:composer','撰写消息',composer));
|
|
106
|
+
}
|
|
107
|
+
if(state.treeOpen){
|
|
108
|
+
const branch=nodes=>nodes.map(item=>node('li','tree:'+item.id,null,[button('tree:select:'+item.id,item.label??item.summary??item.type,find('selectTreeNode',item.id),{'aria-current':state.tree?.leafId===item.id?'true':'false'}),node('ul','tree:children:'+item.id,null,branch(item.children))]));
|
|
109
|
+
children.push(section('conversation:tree','会话树',[...controls(['closeTree','refreshTree']),text('tree:status',state.treeNavigationStatus),node('ul','tree:roots',null,branch(state.tree?.tree??[]))]));
|
|
110
|
+
}
|
|
111
|
+
const composer=children.find(child=>child?.key==='conversation:composer');
|
|
112
|
+
const history={...node('section','conversation:history',null,children.filter(child=>child?.key!=='conversation:composer'),{'aria-label':'会话历史',tabindex:'0'}),className:'conversation-history'};
|
|
113
|
+
return {...node('main','conversation',null,[history,composer],{'aria-label':'会话'}),className:'conversation'};
|
|
114
|
+
}
|
package/git.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {node,button,field,section,text,actionFor} from './tree.js';
|
|
2
|
+
export function renderGit(state,actions){
|
|
3
|
+
const find=(operation,...args)=>actionFor(actions,operation,...args);
|
|
4
|
+
const act=(operation,label,...args)=>{const action=find(operation,...args);return action?button('git:action:'+operation+':'+JSON.stringify(args),label,action):null;};
|
|
5
|
+
const children=[];
|
|
6
|
+
const repositories=state.repositories;
|
|
7
|
+
const current=repositories?.find(repo=>repo.id===state.repositoryId);
|
|
8
|
+
if(repositories){
|
|
9
|
+
children.push(text('git:repository-target',current?`${current.name} · ${current.relativePath}${current.externalRoot?' · 仓库根目录位于工作区外':''}`:'所有仓库'));
|
|
10
|
+
if(repositories.length>1||state.repositoryId===null)children.push(node('details','git:repository-picker',null,[node('summary','git:repository-picker-title',current?'切换仓库':'选择仓库以查看历史、分支或提交'),field('git:repository-search','搜索仓库名称或路径',state.repositorySearch,find('repositorySearch')),act('selectRepository','所有仓库',null),...repositories.filter(repo=>`${repo.name} ${repo.relativePath}`.toLowerCase().includes((state.repositorySearch??'').toLowerCase())).map(repo=>act('selectRepository',`${repo.name} · ${repo.relativePath}`,repo.id))],state.repositoryPickerOpen?{open:true}:{}));
|
|
11
|
+
if(state.discoveryLimited)children.push(text('git:discovery-limited','发现范围受限'),act('continueDiscovery','继续扫描'));
|
|
12
|
+
for(const [index,warning] of (state.discoveryWarnings??[]).entries())children.push(text('git:discovery-warning:'+index,warning));
|
|
13
|
+
}
|
|
14
|
+
if(repositories&&state.repositoryId===null){
|
|
15
|
+
children.push(act('refresh','刷新仓库和变更'),act('selectSection','提交历史','history'),text('git:error',state.error));
|
|
16
|
+
if(state.loading)children.push(node('p','git:loading','正在读取仓库',[],{role:'status'}));
|
|
17
|
+
if(!repositories.length&&!state.loading)children.push(text('git:no-repositories','工作区内未发现 Git 仓库'));
|
|
18
|
+
const group=repo=>{
|
|
19
|
+
const prefix='git:repository:'+repo.id;
|
|
20
|
+
const groupAct=(...args)=>{const result=act(...args);return result?{...result,key:prefix+':'+result.key}:null;};
|
|
21
|
+
const files=repo.changes?.files??[];
|
|
22
|
+
const content=[groupAct('toggleRepository',`${repo.name} · ${repo.changes?.branch??'Detached HEAD'} · ${files.length}`,repo.id),text(prefix+':path',repo.relativePath+(repo.kind==='submodule'?' · 子模块':repo.kind==='worktree'?' · Worktree':'')+(repo.externalRoot?' · 仓库根目录位于工作区外':''))];
|
|
23
|
+
if(!(state.collapsedRepositories??[]).includes(repo.id)){
|
|
24
|
+
content.push(text(prefix+':error',repo.error??repo.changes?.captureError));
|
|
25
|
+
if(!repo.changes&&!repo.error)content.push(text(prefix+':loading','正在读取变更…'));
|
|
26
|
+
for(const [scope,title] of [['staged','已暂存的更改'],['unstaged','更改']]){
|
|
27
|
+
const entries=files.filter(file=>scope==='staged'?file.staged&&!file.conflicted:file.unstaged||file.untracked||file.conflicted);
|
|
28
|
+
if(entries.length)content.push(section(prefix+':'+scope,title,entries.map(file=>node('div',prefix+':'+scope+':'+file.path,null,[text(prefix+':'+scope+':path:'+file.path,`${file.untracked?'?':file.conflicted?'!':file.kind} ${file.path}`),groupAct('repositoryDiff','查看差异',repo.id,file.path,scope),groupAct(scope==='staged'?'repositoryUnstage':'repositoryStage',scope==='staged'?'取消暂存':'暂存',repo.id,file.path)]))));
|
|
29
|
+
}
|
|
30
|
+
content.push(groupAct('repositoryStageAll','全部暂存',repo.id),groupAct('repositoryUnstageAll','全部取消暂存',repo.id),groupAct('selectRepository',files.some(file=>file.staged)?'提交…':'打开仓库',repo.id));
|
|
31
|
+
}
|
|
32
|
+
return node('section',prefix,null,content,{'aria-label':`仓库 ${repo.name} ${repo.relativePath}`});
|
|
33
|
+
};
|
|
34
|
+
const clean=repositories.filter(repo=>!repo.error&&repo.changes?.captureStatus==='captured'&&!repo.changes.files.length);
|
|
35
|
+
children.push(...repositories.filter(repo=>!clean.includes(repo)).map(group));
|
|
36
|
+
if(clean.length)children.push(node('details','git:clean-repositories',null,[node('summary','git:clean-repositories-title',`干净的仓库(${clean.length})`),...clean.map(group)]));
|
|
37
|
+
}else{
|
|
38
|
+
const single=[node('nav','git:tools',null,[act('refresh','刷新变更'),act('refreshMetadata','刷新历史'),act('requestReview','请求审查'),act('selectSection','工作区变更','changes'),act('selectSection','提交历史','history')]),text('git:branch',state.changes?.branch),text('git:error',state.error),text('git:metadata-error',state.metadataError)];
|
|
39
|
+
if(state.changes?.captureStatus&&state.changes.captureStatus!=='captured')single.push(node('p','git:capture-error',state.changes.captureError??'当前工作区无法读取 Git 状态',[],{role:'status'}));
|
|
40
|
+
if(state.changes?.captureStatus==='captured'&&!state.changes.files.length)single.push(text('git:clean','工作区干净,没有待处理的更改'));
|
|
41
|
+
if(state.loading||state.metadataLoading)single.push(node('p','git:loading','正在读取 Git 信息',[],{role:'status'}));
|
|
42
|
+
if(state.draft.gitSection==='changes'){
|
|
43
|
+
single.push(node('ul','git:files',null,(state.changes?.files??[]).map(file=>node('li','git:file:'+file.path,null,[text('git:path:'+file.path,file.path),text('git:kind:'+file.path,`${file.kind}${file.conflicted?' · 冲突':''}${file.staged?' · 已暂存':''}${file.untracked?' · 未跟踪':''}`),act('openDiff','查看已暂存差异',file.path,'staged'),act('openDiff','查看工作区差异',file.path,'unstaged'),act('stageFile','暂存',file.path),act('unstageFile','取消暂存',file.path)]))));
|
|
44
|
+
single.push(act('stageAll','全部暂存'),act('unstageAll','全部取消暂存'),field('git:commit-message',current?`提交到 ${current.name} · ${state.changes?.branch??'Detached HEAD'}`:'提交说明',state.draft.commitMessage,find('commitMessage'),true),act('commit','提交'));
|
|
45
|
+
}else{
|
|
46
|
+
single.push(node('ul','git:history',null,state.history.map(commit=>node('li','git:commit:'+commit.hash,null,[act('selectCommit',`${commit.shortHash} ${commit.subject}`,commit.hash),text('git:author:'+commit.hash,`${commit.author} · ${commit.authoredAt}`)]))));
|
|
47
|
+
if(state.historyLoadMoreError)single.push(node('p','git:history-more-error',state.historyLoadMoreError,[],{role:'alert'}));
|
|
48
|
+
single.push(state.historyLoadingMore?text('git:history-more-loading','正在加载更多提交…'):state.historyHasMore?act('loadMoreHistory','加载更多提交'):null);
|
|
49
|
+
if(state.commitFiles)single.push(node('ul','git:commit-files',null,state.commitFiles.files.map(file=>node('li','git:commit-file:'+file.path,null,[act('openCommitDiff',file.path,state.commitFiles.commit,file.path)]))),act('loadMoreCommitFiles','加载更多文件',state.commitFiles.commit));
|
|
50
|
+
}
|
|
51
|
+
single.push(node('details','git:branches',null,[node('summary','git:branches-title','分支'),field('git:branch-draft','新分支名称',state.draft.branchDraft,find('branchDraft')),act('createBranch','创建分支'),...state.branches.map(branch=>node('p','git:branch:'+branch.name,null,[text('git:branch-name:'+branch.name,branch.name+(branch.current?' · 当前':'')),act('checkoutBranch','切换分支',branch.name)]))]));
|
|
52
|
+
if(state.remoteStatus)single.push(section('git:remote','远端',[text('git:upstream',state.remoteStatus.upstream),text('git:distance',`领先 ${state.remoteStatus.ahead} · 落后 ${state.remoteStatus.behind}`),act('fetch','获取'),act('pull','拉取'),act('push','推送')]));
|
|
53
|
+
single.push(node('details','git:stashes',null,[node('summary','git:stash-title','暂存栈'),act('saveStash','保存到暂存栈'),...state.stashes.map(stash=>node('p','git:stash:'+stash.reference,null,[text('git:stash-message:'+stash.reference,stash.message),act('applyStash','应用 '+stash.reference,stash.reference)]))]));
|
|
54
|
+
children.push(...single);
|
|
55
|
+
}
|
|
56
|
+
const preview=state.preview;
|
|
57
|
+
if(preview.selectedPath||preview.fileDiff||preview.error||preview.loading)children.push(section('git:preview',preview.contextLabel??preview.selectedPath??'差异预览',[act('closeDiff','关闭差异'),preview.loading?text('git:preview-loading','正在读取差异'):null,text('git:preview-error',preview.error),text('git:preview-reason',preview.fileDiff?.reason),preview.fileDiff?node('pre','git:preview-content',preview.fileDiff.diff,[],{tabindex:'0'}):null,preview.fileDiff?.truncated?text('git:preview-truncated','差异已截断'):null]));
|
|
58
|
+
return section('git','Git',children);
|
|
59
|
+
}
|
package/inspector.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {node,button,field,section,text,actionFor} from './tree.js';
|
|
2
|
+
import {renderExecutionProfile,renderAttachment,renderSessionMetadata} from './metadata.js';
|
|
3
|
+
export function renderInspector(state,actions){
|
|
4
|
+
const find=(operation,...args)=>actionFor(actions,operation,...args);
|
|
5
|
+
const act=(operation,label,...args)=>{const action=find(operation,...args);return action?button('inspector:action:'+operation+':'+JSON.stringify(args),label,action):null;};
|
|
6
|
+
const children=[node('nav','inspector:tools',null,[act('refresh','刷新上下文'),act('syncThreads','同步会话')])];
|
|
7
|
+
children.push(section('inspector:diagnostics','Agent 状态',state.diagnostics.map(item=>section('diagnostic:'+item.agent,item.label,[text('diagnostic:status:'+item.agent,`${item.status} · ${item.authState}`),text('diagnostic:version:'+item.agent,item.version),text('diagnostic:executable:'+item.agent,item.executable),text('diagnostic:message:'+item.agent,item.message),...(item.capabilities??[]).map((value,i)=>text('diagnostic:capability:'+item.agent+':'+i,value))]))));
|
|
8
|
+
children.push(renderSessionMetadata(state.session,'inspector:session-metadata'),renderExecutionProfile(state.executionProfile,'inspector:execution-profile'),section('inspector:attachments','上下文附件',state.attachments.length?state.attachments.map(item=>renderAttachment(item,'inspector:attachment:'+item.id)):[text('inspector:no-attachments','发送前可在输入框添加文件或目录')]));
|
|
9
|
+
const inventory=state.workspaceCapabilities;
|
|
10
|
+
if(inventory)for(const [key,label] of [['instructions','指令'],['skills','技能'],['tools','工具'],['mcpServers','MCP 服务']])children.push(section('inventory:'+key,label,inventory[key].map((item,i)=>text('inventory:'+key+':'+i,`${item.name} · ${item.source}`))));
|
|
11
|
+
if(inventory?.warnings.length)children.push(section('inventory:warnings','检查提示',inventory.warnings.map((value,i)=>text('inventory:warning:'+i,value))));
|
|
12
|
+
children.push(section('inspector:threads','线程',state.threads.map(item=>text('thread:'+item.id,`${item.title??item.id} · ${item.status??''}${item.cwd?' · '+item.cwd:''}`))));
|
|
13
|
+
children.push(section('inspector:artifacts','产物',state.artifacts.map(item=>node('article','artifact:'+item.id,null,[text('artifact:metadata:'+item.id,`${item.source} · ${item.mediaType} · ${item.size} 字节`),text('artifact:scope:'+item.id,item.turnId?'本轮 '+item.turnId:'会话级'),text('artifact:hash:'+item.id,item.contentHash?'内容摘要:'+item.contentHash:null),act('toggleArtifact',state.artifactPreview.artifactId===item.id?(state.artifactPreview.error?'重试产物':'收起产物'):'查看产物 '+item.source,item.id)]))));
|
|
14
|
+
const preview=state.artifactPreview;
|
|
15
|
+
if(preview.artifactId)children.push(section('inspector:preview','产物预览',[act('closeArtifact','关闭产物'),preview.loading?text('artifact:loading','正在读取产物'):null,text('artifact:error',preview.error),preview.content?node('pre','artifact:content',preview.content.content,[],{tabindex:'0'}):null,preview.content?.truncated?text('artifact:truncated','产物已截断'):null]));
|
|
16
|
+
children.push(section('inspector:project-actions','工程动作',[act('newProjectAction','新建工程动作'),...state.projectActions.map(item=>node('article','project:'+item.id,null,[text('project:name:'+item.id,item.name),text('project:command:'+item.id,[item.program,...item.args].join(' ')),text('project:cwd:'+item.id,item.cwd),text('project:enabled:'+item.id,item.enabled?'已启用':'已禁用'),act('editProjectAction','编辑 '+item.name,item.id),act('deleteProjectAction','删除 '+item.name,item.id),act('runProjectAction','运行 '+item.name,item.id)]))]));
|
|
17
|
+
const editor=state.projectEditor;
|
|
18
|
+
if(editor.open)children.push(section('inspector:editor','编辑工程动作',[...['name','program','args','cwd'].map((key,i)=>field('project:field:'+key,['名称','程序','参数(JSON 数组)','工作目录'][i],editor[key],find('projectField',key),key==='args')),node('nav','project:kinds',null,['test','lint','build','custom'].map((kind,i)=>{const action=find('projectKind',kind);return button('project:kind:'+kind,['测试','检查','构建','自定义'][i],action,{'aria-pressed':String(editor.kind===kind)});})),text('project:save-error',editor.error),editor.saving?text('project:saving','正在保存'):null,act('saveProjectAction','保存工程动作'),act('closeProjectEditor','关闭编辑器')]));
|
|
19
|
+
children.push(section('inspector:runs','运行记录',state.projectActionRuns.map(run=>node('article','run:'+run.id,null,[text('run:label:'+run.id,`${run.actionName??run.actionId} · ${run.status} · 退出码 ${run.exitCode??'—'}`),node('pre','run:output:'+run.id,run.output,[],{tabindex:'0'}),act('cancelProjectAction','取消运行',run.id)]))));
|
|
20
|
+
const change=state.changeSet;
|
|
21
|
+
if(change){
|
|
22
|
+
children.push(section('inspector:changes','本轮变更',[text('change:status',`${change.attribution} · ${change.captureStatus}`),text('change:error',change.captureError),act('restoreTurn','恢复整轮',change.turnId),...change.files.map(file=>node('article','change:file:'+file.path,null,[text('change:path:'+file.path,file.path),act('showDiff','查看差异',change.turnId,file.path),...['stage','unstage','revert'].map((operation,i)=>act('fileAction',['暂存文件','取消暂存文件','恢复文件'][i],change.turnId,file.path,operation))])),...change.commands.map(command=>section('change:command:'+command.id,command.command??command.toolName??'命令',[text('change:command-status:'+command.id,command.status),node('pre','change:command-output:'+command.id,command.output)])),...change.verification.map(item=>section('change:verification:'+item.id,'验证 '+item.status,[node('pre','change:verification-output:'+item.id,item.output)]))]));
|
|
23
|
+
if(state.fileDiff)children.push(section('inspector:diff',state.fileDiff.path,[text('change:diff-reason',state.fileDiff.reason),node('pre','change:diff-content',state.fileDiff.diff,[],{tabindex:'0'}),...state.fileDiff.hunks.map(hunk=>section('change:hunk:'+hunk.index,hunk.header,[node('pre','change:hunk-content:'+hunk.index,hunk.content),...['stage','unstage','revert'].map((operation,i)=>act('hunkAction',['暂存片段','取消暂存片段','恢复片段'][i],change.turnId,state.fileDiff.path,String(hunk.index),operation))]))]));
|
|
24
|
+
}
|
|
25
|
+
children.push(text('inspector:diff-error',state.fileDiffError),state.fileDiffLoading?text('inspector:diff-loading','正在读取差异'):null,section('inspector:checkpoints','检查点',state.checkpoints.map(item=>text('checkpoint:'+item.id,`${item.path} · ${item.available?'可用':item.reason??'不可用'}${item.baselineDirty?' · 基线已有修改':''}`))),section('inspector:restores','恢复记录',state.restoreOperations.map(item=>section('restore:'+item.id,item.status,[text('restore:restored:'+item.id,'已恢复:'+item.restored.join('、')),text('restore:conflicts:'+item.id,'冲突:'+item.conflicts.join('、')),text('restore:unsupported:'+item.id,'不支持:'+item.unsupported.join('、'))]))));
|
|
26
|
+
return section('inspector','上下文',children);
|
|
27
|
+
}
|
package/links.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type LinkTarget = {kind:'web'|'email'} | {kind:'file';path:string;line:number|null;column:number|null};
|
|
2
|
+
export function linkTarget(href:string): LinkTarget|null;
|
|
3
|
+
export const linkIcons: {web:'globe';email:'mail';file:'file'};
|
|
4
|
+
export const linkLabels: Record<LinkTarget['kind'],string>;
|
|
5
|
+
export const linkIconPaths: Record<LinkTarget['kind'],string>;
|
package/links.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Classify explicit Markdown destinations; never turn arbitrary prose into file links. */
|
|
2
|
+
export function linkTarget(href) {
|
|
3
|
+
if (!href || /[\u0000-\u001f\u007f]/.test(href)) return null;
|
|
4
|
+
if (/^https?:\/\//i.test(href)) return /\s/.test(href) ? null : {kind:'web'};
|
|
5
|
+
if (/^mailto:/i.test(href)) return /\s/.test(href) ? null : {kind:'email'};
|
|
6
|
+
if (/^(?:#|\/\/|\\\\)/.test(href)) return null;
|
|
7
|
+
let path = href, line = null, column = null;
|
|
8
|
+
const location = /#L([1-9]\d*)(?:-L?([1-9]\d*))?$/.exec(path) ?? /:([1-9]\d*)(?::([1-9]\d*))?$/.exec(path);
|
|
9
|
+
if (location) {
|
|
10
|
+
line = Number(location[1]);
|
|
11
|
+
column = location[0][0] === ':' && location[2] ? Number(location[2]) : null;
|
|
12
|
+
if (!Number.isSafeInteger(line) || line > 2147483647 || (column !== null && (!Number.isSafeInteger(column) || column > 2147483647))) return null;
|
|
13
|
+
path = path.slice(0, location.index);
|
|
14
|
+
}
|
|
15
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(href) && !/^file:\/\//i.test(href) && !/^[a-z]:[/\\]/i.test(href) && !(location && path.includes('.'))) return null;
|
|
16
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(path) && !/^file:\/\//i.test(path) && !/^[a-z]:[/\\]/i.test(path)) return null;
|
|
17
|
+
try {
|
|
18
|
+
if (/^file:\/\//i.test(path)) {
|
|
19
|
+
const url = new URL(path);
|
|
20
|
+
if (url.hostname && url.hostname !== 'localhost' || url.search || url.hash) return null;
|
|
21
|
+
path = decodeURIComponent(url.pathname);
|
|
22
|
+
if (/^\/[a-z]:\//i.test(path)) path = path.slice(1);
|
|
23
|
+
} else path = decodeURIComponent(path);
|
|
24
|
+
} catch { return null; }
|
|
25
|
+
if (!path || /[\u0000-\u001f\u007f]/.test(path) || /^(?:\/\/|\\\\)/.test(path)) return null;
|
|
26
|
+
return {kind:'file',path,line,column};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const linkIcons = {web:'globe',email:'mail',file:'file'};
|
|
30
|
+
export const linkLabels = {web:'网页',email:'邮件',file:'本地文件'};
|
|
31
|
+
export const linkIconPaths = {
|
|
32
|
+
web:'M21 12a9 9 0 1 1-18 0 9 9 0 1 1 18 0M3 12h18M12 3c-5 5-5 13 0 18 5-5 5-13 0-18Z',
|
|
33
|
+
email:'M3 5h18v14H3ZM3 5l9 7 9-7',
|
|
34
|
+
file:'M5 3h9l5 5v13H5ZM14 3v6h5',
|
|
35
|
+
};
|
package/markdown.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type InlineSegment = {
|
|
2
|
+
kind: 'text' | 'code' | 'strong' | 'em' | 'del' | 'link';
|
|
3
|
+
value: string;
|
|
4
|
+
href?: string;
|
|
5
|
+
children?: InlineSegment[];
|
|
6
|
+
};
|
|
7
|
+
type BlockBase = {index: number; lines: string[]};
|
|
8
|
+
export type MarkdownBlock = BlockBase & (
|
|
9
|
+
| {kind: 'paragraph'; segments: InlineSegment[]}
|
|
10
|
+
| {kind: 'heading'; level: number; segments: InlineSegment[]}
|
|
11
|
+
| {kind: 'code'; language: string}
|
|
12
|
+
| {kind: 'rule'}
|
|
13
|
+
| {kind: 'quote'; blocks: MarkdownBlock[]}
|
|
14
|
+
| {kind: 'list'; ordered: boolean; start: number; items: {checked: boolean | null; blocks: MarkdownBlock[]}[]}
|
|
15
|
+
| {kind: 'table'; align: ('left' | 'center' | 'right' | null)[]; header: InlineSegment[][]; rows: InlineSegment[][][]}
|
|
16
|
+
);
|
|
17
|
+
export function parseMarkdown(value: string): MarkdownBlock[];
|
|
18
|
+
export function inlineSegments(value: string): InlineSegment[];
|
|
19
|
+
export function displayMarkdown(value: string): string;
|
|
20
|
+
export function markdownTargets(value: string): {kind: 'code' | 'link'; index: number; value: string}[];
|
package/markdown.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {Lexer} from 'marked';
|
|
2
|
+
import {decodeHTML} from 'entities';
|
|
3
|
+
import {linkTarget} from './links.js';
|
|
4
|
+
|
|
5
|
+
const options = {gfm: true, breaks: true};
|
|
6
|
+
|
|
7
|
+
// Only data leaves the lexer. HTML is displayed literally, never interpreted.
|
|
8
|
+
function segments(tokens) {
|
|
9
|
+
return tokens.filter(token => token.type !== 'checkbox').map(token => {
|
|
10
|
+
if (token.type === 'html') return {kind: 'text', value: token.raw};
|
|
11
|
+
if (token.type === 'br') return {kind: 'text', value: '\n'};
|
|
12
|
+
if (token.type === 'codespan') return {kind: 'code', value: token.text};
|
|
13
|
+
if (['strong', 'em', 'del'].includes(token.type)) {
|
|
14
|
+
return {kind: token.type, value: token.text, children: segments(token.tokens)};
|
|
15
|
+
}
|
|
16
|
+
if (token.type === 'link' || token.type === 'image') {
|
|
17
|
+
const href = decodeHTML(token.href);
|
|
18
|
+
if (!linkTarget(href)) return {kind: 'text', value: token.raw};
|
|
19
|
+
// Images stay explicit links: remote fetching is not part of Markdown rendering.
|
|
20
|
+
const children = token.type === 'image'
|
|
21
|
+
? [{kind: 'text', value: `图片:${decodeHTML(token.text) || href}`}]
|
|
22
|
+
: segments(token.tokens);
|
|
23
|
+
return {kind: 'link', value: decodeHTML(token.text), href, children};
|
|
24
|
+
}
|
|
25
|
+
return {kind: 'text', value: decodeHTML(token.text ?? token.raw)};
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function inlineSegments(value) {
|
|
30
|
+
return segments(Lexer.lexInline(value, options));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parseMarkdown(value) {
|
|
34
|
+
let index = 0;
|
|
35
|
+
function blocks(tokens) {
|
|
36
|
+
return tokens.filter(token => !['space', 'def', 'checkbox'].includes(token.type)).map(token => {
|
|
37
|
+
const base = {index: index++, lines: [token.text ?? token.raw]};
|
|
38
|
+
if (token.type === 'code') return {...base, kind: 'code', lines: token.text.split('\n'), language: (token.lang ?? '').split(/\s+/)[0]};
|
|
39
|
+
if (token.type === 'heading') return {...base, kind: 'heading', level: token.depth, segments: segments(token.tokens)};
|
|
40
|
+
if (token.type === 'hr') return {...base, kind: 'rule'};
|
|
41
|
+
if (token.type === 'blockquote') return {...base, kind: 'quote', blocks: blocks(token.tokens)};
|
|
42
|
+
if (token.type === 'list') return {...base, kind: 'list', ordered: token.ordered, start: token.ordered ? token.start : 1,
|
|
43
|
+
lines: token.items.map(item => item.text),
|
|
44
|
+
items: token.items.map(item => ({checked: item.task ? Boolean(item.checked) : null, blocks: blocks(item.tokens)}))};
|
|
45
|
+
if (token.type === 'table') return {...base, kind: 'table', align: token.align,
|
|
46
|
+
header: token.header.map(cell => segments(cell.tokens)), rows: token.rows.map(row => row.map(cell => segments(cell.tokens)))};
|
|
47
|
+
return {...base, kind: 'paragraph', segments: token.type === 'html'
|
|
48
|
+
? [{kind: 'text', value: token.raw}] : segments(token.tokens ?? [{type: 'text', text: token.text ?? token.raw}])};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return blocks(Lexer.lex(value.replace(/\r\n?/g, '\n'), options));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function displayMarkdown(content) {
|
|
55
|
+
return content.replace(/\n?\[AIBO_CONTEXT_ATTACHMENTS\][\s\S]*?\[\/AIBO_CONTEXT_ATTACHMENTS\]/g, '').trimEnd();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function markdownTargets(content) {
|
|
59
|
+
const targets = [], links = new Set();
|
|
60
|
+
function visitInline(values, index) {
|
|
61
|
+
for (const segment of values) {
|
|
62
|
+
if (segment.kind === 'link' && !links.has(segment.href)) {
|
|
63
|
+
links.add(segment.href);
|
|
64
|
+
targets.push({kind: 'link', index, value: segment.href});
|
|
65
|
+
}
|
|
66
|
+
if (segment.children) visitInline(segment.children, index);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function visit(blocks) {
|
|
70
|
+
for (const block of blocks) {
|
|
71
|
+
if (block.kind === 'code') targets.push({kind: 'code', index: block.index, value: block.lines.join('\n')});
|
|
72
|
+
if (block.segments) visitInline(block.segments, block.index);
|
|
73
|
+
if (block.kind === 'table') for (const row of [block.header, ...block.rows]) for (const cell of row) visitInline(cell, block.index);
|
|
74
|
+
if (block.blocks) visit(block.blocks);
|
|
75
|
+
if (block.items) for (const item of block.items) visit(item.blocks);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
visit(parseMarkdown(displayMarkdown(content)));
|
|
79
|
+
return targets;
|
|
80
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Match explicit attachment IDs, never every attachment sharing a turn (steering shares turns). */
|
|
2
|
+
export function splitMessageAttachments(content, attachments = []) {
|
|
3
|
+
const found=[];
|
|
4
|
+
const body=content.replace(/\n?\[AIBO_CONTEXT_ATTACHMENTS\]([\s\S]*?)\[\/AIBO_CONTEXT_ATTACHMENTS\]/g,(_,block)=>{
|
|
5
|
+
for(const line of block.split('\n')) {
|
|
6
|
+
const match=line.match(/^- (.*?) \[attachment:([^\]\r\n]+)\]\s*$/);
|
|
7
|
+
if(!match || found.some(item=>item.id===match[2]))continue;
|
|
8
|
+
const existing=attachments.find(item=>item.id===match[2]);
|
|
9
|
+
found.push(existing??{id:match[2],path:match[1].replace(/ \([^)]*\)$/,''),mediaType:'',size:null});
|
|
10
|
+
}
|
|
11
|
+
return '';
|
|
12
|
+
});
|
|
13
|
+
return {body:body.trimEnd(),attachments:found};
|
|
14
|
+
}
|
package/metadata.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {node,section,text} from './tree.js';
|
|
2
|
+
const labels={interactionMode:{ask:'问答',plan:'计划',edit:'编辑'},filesystemPolicy:{'agent-managed':'Agent 原生权限','read-only':'只读','workspace-write':'工作区可写','danger-full-access':'完整文件访问'},commandPolicy:{'agent-managed':'Agent 原生权限',disabled:'禁用',approved:'需审批',trusted:'自动执行'},networkPolicy:{disabled:'禁用','agent-managed':'Agent 管理'},approvalPolicy:{never:'从不请求审批',untrusted:'未信任操作需审批','on-request':'按请求审批',trusted:'信任模式'},approvalReviewer:{user:'用户','auto-review':'自动审核',none:'无'}};
|
|
3
|
+
export function renderExecutionProfile(profile,key){
|
|
4
|
+
if(!profile)return null;
|
|
5
|
+
const fields=[['interactionMode','模式'],['filesystemPolicy','文件'],['commandPolicy','命令'],['networkPolicy','网络'],['approvalPolicy','审批'],['approvalReviewer','审核者'],['model','模型'],['reasoningEffort','推理强度']];
|
|
6
|
+
const value=(field,value)=>value==null?'默认':labels[field]?.[value]??value;
|
|
7
|
+
return section(key,'执行权限',[text(key+':sandbox',profile.agentManagedPermissions?'权限由 Agent 管理;aibo 转发审批,不提供进程沙箱':profile.nativeSandbox?'原生沙箱':'无原生沙箱'),node('table',key+':table',null,[node('thead',key+':head',null,[node('tr',key+':columns',null,['设置','请求值','实际生效'].map((label,i)=>node('th',key+':column:'+i,label)))]),node('tbody',key+':body',null,fields.map(([field,label])=>node('tr',key+':row:'+field,null,[node('th',key+':label:'+field,label),node('td',key+':requested:'+field,value(field,profile.requested[field])),node('td',key+':enforced:'+field,value(field,profile.enforced[field]))])))]),...(profile.unsupported??[]).map((item,i)=>node('p',key+':unsupported:'+i,'未启用:'+item,[],{role:'status'})),text(key+':resolved',profile.resolvedAt?'更新于 '+profile.resolvedAt:null),section(key+':capabilities','Agent 能力',(profile.adapterCapabilities??[]).length?profile.adapterCapabilities.map((item,i)=>text(key+':capability:'+i,item)):[text(key+':no-capabilities','未报告额外能力')])]);
|
|
8
|
+
}
|
|
9
|
+
export function renderAttachment(item,key){
|
|
10
|
+
return node('div',key,null,[text(key+':path',item.path),text(key+':state',`${item.turnId?'已发送':'待发送'} · ${item.sendStrategy==='reference'?'工作区引用':item.sendStrategy==='inline'?'内联':item.sendStrategy??'发送方式未提供'}${item.size==null?'':` · ${item.size} 字节`}`),text(key+':media',item.mediaType),text(key+':source',item.source?'来源:'+item.source:null)]);
|
|
11
|
+
}
|
|
12
|
+
export function renderSessionMetadata(session,key){
|
|
13
|
+
if(!session)return null;
|
|
14
|
+
return node('details',key,null,[node('summary',key+':summary','会话信息'),text(key+':id','会话 ID:'+session.id),text(key+':external',session.externalSessionId?'远端绑定:'+session.externalSessionId:null),text(key+':updated',session.updatedAt?'更新于 '+session.updatedAt:null),text(key+':archived',session.archived?'已归档':null)]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function renderMessageAttachment(item,key){
|
|
18
|
+
const name=(item.path??item.id).split(/[\\/]/).pop()||item.id;
|
|
19
|
+
return {...node('div',key,null,[
|
|
20
|
+
item.mediaType?.startsWith('image/')?{...node('img',key+':preview',null,[],{alt:name,width:'160',height:'112'}),resource:'attachment:'+item.id}:null,
|
|
21
|
+
text(key+':name',name),
|
|
22
|
+
],{title:item.path??item.id}),className:'message-attachment'};
|
|
23
|
+
}
|
package/navigation.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {node,button,field,text} from './tree.js';
|
|
2
|
+
const labels={toggleSearch:'搜索会话',toggleFilter:'筛选会话',applyFilters:'应用筛选',addWorkspace:'添加工作区',toggleSessionCreator:'新建会话',toggleTrust:'更改信任',removeWorkspace:'移除工作区',openWorkspace:'打开目录',createCodex:'新建 Codex 会话',createPi:'新建 Pi 会话',unarchiveSession:'取消归档',archiveSession:'归档',syncSession:'同步',renameSession:'重命名',saveRename:'保存名称',cancelRename:'取消重命名'};
|
|
3
|
+
const states={active:'未归档',all:'全部',archived:'已归档',created:'已创建',starting:'启动中',running:'运行中',waiting_approval:'等待审批',waiting_user:'等待回答',compacting:'压缩中',idle:'空闲',interrupted:'已中断',failed:'失败',closed:'已关闭'};
|
|
4
|
+
export function renderNavigation(state,actions){
|
|
5
|
+
const find=(operation,targetId)=>actions.find(action=>action.operation===operation&&action.targetId===targetId);
|
|
6
|
+
const buttons=(operations,targetId)=>operations.flatMap(operation=>{const action=find(operation,targetId);return action?[button('navigation:'+operation+':'+(targetId??''),labels[operation],action)]:[]});
|
|
7
|
+
const children=[node('h2','navigation:title','工作区'),node('nav','navigation:tools',null,buttons(['addWorkspace','toggleFilter']))];
|
|
8
|
+
if(state.sessionFilterOpen){const action=find('filter');children.push(node('label','navigation:filter-label',null,[text('navigation:filter-text','会话状态'),{...node('select','navigation:filter',null,(action?.options??[]).map(value=>node('option','navigation:filter:'+value,states[value]??value,[],{value,selected:state.sessionFilter===value})),{'aria-label':'会话状态',value:state.sessionFilter}),...(action?{events:{change:action.token}}:{})}]),...buttons(['applyFilters']));}
|
|
9
|
+
for(const workspace of state.workspaces){
|
|
10
|
+
const expanded=state.expandedWorkspaceIds.includes(workspace.id);
|
|
11
|
+
const rows=[button('workspace:'+workspace.id,workspace.label,find('selectWorkspace',workspace.id),{'aria-expanded':String(expanded),'aria-current':state.selectedWorkspaceId===workspace.id?'page':'false',title:workspace.path}),text('workspace:trust:'+workspace.id,workspace.trust==='trusted'?'已信任':'未信任'),node('nav','workspace:tools:'+workspace.id,null,buttons(['toggleSessionCreator','openWorkspace','toggleTrust','removeWorkspace'],workspace.id))];
|
|
12
|
+
if(state.createSessionWorkspaceId===workspace.id)rows.push(node('nav','workspace:create:'+workspace.id,null,(state.agentChoices??[]).map(choice=>{
|
|
13
|
+
const key='workspace:create:'+workspace.id+':'+choice.id;
|
|
14
|
+
const action=actions.find(action=>action.operation==='createAgent'&&action.targetId===workspace.id&&action.choiceId===choice.id);
|
|
15
|
+
const control=button(key,null,action,{'aria-label':'使用 '+choice.label+' 创建会话',title:choice.label});
|
|
16
|
+
control.children=[node('svg',key+':icon',null,[node('path',key+':path',null,[],{d:choice.icon?.path??'M12 2 22 12 12 22 2 12Z',fill:'currentColor'})],{viewBox:'0 0 24 24',width:'16',height:'16','aria-hidden':'true'}),node('span',key+':label',choice.label)];
|
|
17
|
+
return control;
|
|
18
|
+
})));
|
|
19
|
+
if(expanded){
|
|
20
|
+
if(state.sessionsLoadingWorkspaceIds.includes(workspace.id))rows.push(node('p','workspace:loading:'+workspace.id,'正在加载会话',[],{role:'status'}));
|
|
21
|
+
for(const session of state.sessionsByWorkspace[workspace.id]??[]){
|
|
22
|
+
const parts=[button('session:'+session.id,session.label,find('selectSession',session.id),{'aria-current':state.selectedSessionId===session.id?'page':'false'}),text('session:state:'+session.id,`${session.agent} · ${states[session.state]??session.state}${session.archived?' · 已归档':''}`),node('nav','session:tools:'+session.id,null,buttons(['syncSession','renameSession','archiveSession','unarchiveSession'],session.id))];
|
|
23
|
+
if(state.renamingSessionId===session.id)parts.push(field('session:rename:'+session.id,'会话名称',state.sessionLabelDraft,find('renameDraft',session.id)),...buttons(['saveRename','cancelRename'],session.id));
|
|
24
|
+
rows.push(node('article','session:row:'+session.id,null,parts));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
children.push(node('section','workspace:section:'+workspace.id,null,rows));
|
|
28
|
+
}
|
|
29
|
+
return {...node('aside','navigation',null,children,{'aria-label':'工作区与会话'}),className:'navigation'};
|
|
30
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aibolabs/presentation-workbench",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/chldu2000/aibo.git",
|
|
8
|
+
"directory": "packages/presentation-workbench"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
"./git": "./git.js",
|
|
13
|
+
"./inspector": "./inspector.js",
|
|
14
|
+
"./capability": "./capability.js",
|
|
15
|
+
"./navigation": "./navigation.js",
|
|
16
|
+
"./conversation": "./conversation.js",
|
|
17
|
+
"./bundle": "./bundle.mjs",
|
|
18
|
+
".": "./workbench.js",
|
|
19
|
+
"./markdown": {
|
|
20
|
+
"types": "./markdown.d.ts",
|
|
21
|
+
"default": "./markdown.js"
|
|
22
|
+
},
|
|
23
|
+
"./rich-text": "./rich-text.js",
|
|
24
|
+
"./timeline-model": {
|
|
25
|
+
"types": "./timeline-model.d.ts",
|
|
26
|
+
"default": "./timeline-model.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"*.js",
|
|
31
|
+
"README.md",
|
|
32
|
+
"bundle.mjs",
|
|
33
|
+
"markdown.d.ts",
|
|
34
|
+
"links.d.ts",
|
|
35
|
+
"timeline-model.d.ts",
|
|
36
|
+
"code-highlight.d.ts"
|
|
37
|
+
],
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"marked": "^18.0.14",
|
|
40
|
+
"entities": "^8.1.0",
|
|
41
|
+
"lowlight": "^3.3.0",
|
|
42
|
+
"esbuild": "^0.28.2"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public",
|
|
46
|
+
"registry": "https://registry.npmjs.org/"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/rich-text.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {node,button,actionFor} from './tree.js';
|
|
2
|
+
import {parseMarkdown,displayMarkdown} from './markdown.js';
|
|
3
|
+
import {highlightCode} from './code-highlight.js';
|
|
4
|
+
import {linkTarget,linkLabels,linkIconPaths} from './links.js';
|
|
5
|
+
|
|
6
|
+
export function renderRichText(content,key,entryId,actions) {
|
|
7
|
+
const inline = (segments,prefix) => segments.map((segment,index) => {
|
|
8
|
+
const id = prefix+':'+index;
|
|
9
|
+
if (segment.kind === 'link') {
|
|
10
|
+
const action = actions.find(action => action.operation === 'openLink' && action.args[0] === entryId && action.args[2] === segment.href);
|
|
11
|
+
const kind = linkTarget(segment.href).kind;
|
|
12
|
+
const icon = {...node('svg',id+':icon',null,[node('path',id+':icon:path',null,[],{d:linkIconPaths[kind]})],{viewBox:'0 0 24 24',width:'13',height:'13',fill:'none',stroke:'currentColor','stroke-width':'1.6','aria-hidden':'true'}),className:'markdown-link-icon'};
|
|
13
|
+
return {...button(id,null,action,{role:'link',title:linkLabels[kind]+':'+segment.href}),children:[icon,...inline(segment.children,id+':label')],className:'markdown-link'};
|
|
14
|
+
}
|
|
15
|
+
const tag = {code:'code',strong:'strong',em:'em',del:'del'}[segment.kind] ?? 'span';
|
|
16
|
+
return {...node(tag,id,segment.children ? null : segment.value,segment.children ? inline(segment.children,id) : []),
|
|
17
|
+
...(segment.kind === 'del' ? {className:'markdown-deleted'} : {})};
|
|
18
|
+
});
|
|
19
|
+
const highlighted = (segments,prefix) => segments.map((segment,index) => ({
|
|
20
|
+
...node('span',prefix+':'+index,segment.value,segment.children ? highlighted(segment.children,prefix+':'+index) : []),
|
|
21
|
+
...(segment.className ? {className:segment.className} : {}),
|
|
22
|
+
}));
|
|
23
|
+
const blocks = values => values.map(block => {
|
|
24
|
+
const id = key+':block:'+block.index;
|
|
25
|
+
if (block.kind === 'code') return {...node('section',id,null,[
|
|
26
|
+
node('header',id+':toolbar',null,[node('span',id+':language',block.language),button(id+':copy','复制代码',actionFor(actions,'copyCode',entryId,String(block.index)))]),
|
|
27
|
+
node('pre',id+':pre',null,[node('code',id+':code',null,highlighted(highlightCode(block.lines.join('\n'),block.language),id+':highlight'))],{tabindex:'0','aria-label':'代码块'}),
|
|
28
|
+
]),className:'markdown-code-block'};
|
|
29
|
+
if (block.kind === 'list') return {...node(block.ordered ? 'ol' : 'ul',id,null,block.items.map((item,i) => {
|
|
30
|
+
const itemId = id+':item:'+i;
|
|
31
|
+
return {...node('li',itemId,null,[
|
|
32
|
+
item.checked !== null ? {...node('span',itemId+':check',item.checked ? '☑' : '☐',[],{role:'img','aria-label':item.checked ? '已完成' : '未完成'}),className:'markdown-task-check'} : null,
|
|
33
|
+
{...node('div',itemId+':content',null,blocks(item.blocks)),className:'markdown-list-content'},
|
|
34
|
+
]),className:item.checked !== null ? 'markdown-task' : ''};
|
|
35
|
+
}),block.ordered ? {start:String(block.start)} : {}),className:'markdown-list'};
|
|
36
|
+
if (block.kind === 'quote') return {...node('blockquote',id,null,blocks(block.blocks)),className:'markdown-quote'};
|
|
37
|
+
if (block.kind === 'rule') return {...node('hr',id),className:'markdown-rule'};
|
|
38
|
+
if (block.kind === 'table') {
|
|
39
|
+
const row = (cells,rowId,tag) => node('tr',rowId,null,cells.map((cell,column) => ({
|
|
40
|
+
...node(tag,rowId+':'+column,null,inline(cell,rowId+':'+column+':inline'),tag === 'th' ? {scope:'col'} : {}),
|
|
41
|
+
className:'markdown-align-'+(block.align[column] ?? 'left'),
|
|
42
|
+
})));
|
|
43
|
+
return {...node('div',id,null,[{...node('table',id+':table',null,[
|
|
44
|
+
node('thead',id+':head',null,[row(block.header,id+':header','th')]),
|
|
45
|
+
node('tbody',id+':body',null,block.rows.map((cells,i) => row(cells,id+':row:'+i,'td'))),
|
|
46
|
+
]),className:'markdown-table'}],{role:'region',tabindex:'0','aria-label':'表格,可横向滚动'}),className:'markdown-table-scroll'};
|
|
47
|
+
}
|
|
48
|
+
const heading = block.kind === 'heading';
|
|
49
|
+
return {...node(heading ? 'h'+block.level : 'p',id,null,inline(block.segments,id+':inline')),
|
|
50
|
+
className:heading ? 'markdown-heading markdown-heading-'+block.level : 'markdown-paragraph'};
|
|
51
|
+
});
|
|
52
|
+
return {...node('div',key,null,blocks(parseMarkdown(displayMarkdown(content)))),className:'markdown-content'};
|
|
53
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Extract only recognized, valid reference envelopes; malformed prose stays visible. */
|
|
2
|
+
export function splitSessionReferences(content) {
|
|
3
|
+
const references=[];
|
|
4
|
+
const body=content.replace(/\[AIBO_SESSION_REFERENCES\]\r?\n[^\n]*\r?\n(\[[^\n]*\])\r?\n\[\/AIBO_SESSION_REFERENCES\]/g,(block,payload)=>{
|
|
5
|
+
try {
|
|
6
|
+
const entries=JSON.parse(payload);
|
|
7
|
+
if(!Array.isArray(entries)||!entries.length||entries.some(entry=>!entry?.snapshot||typeof entry.snapshot.sourceSessionId!=='string'))return block;
|
|
8
|
+
for(const entry of entries){
|
|
9
|
+
const snapshot=entry.snapshot;
|
|
10
|
+
const messages=Array.isArray(snapshot.messages)?snapshot.messages:[];
|
|
11
|
+
const complete=snapshot.schema==='aibo.session-reference/v3';
|
|
12
|
+
const selected=messages.filter(item=>item&&(item.role==='user'||item.role==='assistant')&&typeof item.content==='string');
|
|
13
|
+
const excerpts=(complete?selected:selected.slice(-12))
|
|
14
|
+
.map(item=>({role:item.role,text:complete?item.content:item.content.split('[AIBO_SESSION_REFERENCES]')[0].split('[AIBO_CONTEXT_ATTACHMENTS]')[0].slice(0,1500),truncated:item.truncated===true||(!complete&&item.content.length>1500)}));
|
|
15
|
+
references.push({id:String(entry.snapshotId??snapshot.snapshotId??snapshot.sourceSessionId),title:typeof snapshot.sourceLabel==='string'?snapshot.sourceLabel:'引用会话',agent:typeof snapshot.sourceAgent==='string'?snapshot.sourceAgent:'',excerpts,
|
|
16
|
+
note:complete?`${snapshot.messageLimit===null?'全部用户 / Agent 消息':`最近 ${snapshot.messageLimit} 条用户 / Agent 消息`} · 工具输出未提供`:snapshot.contextMode==='conversation-excerpts'?'对话摘录 · 工具输出未提供':'历史引用 · 仅展示对话摘录',
|
|
17
|
+
omitted:typeof snapshot.omittedMessageCount==='number'?snapshot.omittedMessageCount:null});
|
|
18
|
+
}
|
|
19
|
+
return '';
|
|
20
|
+
}catch{return block;}
|
|
21
|
+
});
|
|
22
|
+
return {body:body.trim(),references};
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface TimelineEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
role: string;
|
|
4
|
+
toolName: string | null;
|
|
5
|
+
entryType: string | null;
|
|
6
|
+
content: string;
|
|
7
|
+
}
|
|
8
|
+
export type TimelineRenderItem<T extends TimelineEntry = TimelineEntry> =
|
|
9
|
+
| { kind: 'entry'; id: string; item: T }
|
|
10
|
+
| { kind: 'tool-group' | 'system-group'; id: string; items: T[] };
|
|
11
|
+
export function groupTimelineItems<T extends TimelineEntry>(items: T[], groupSystemItems?: boolean): TimelineRenderItem<T>[];
|
|
12
|
+
export function toolLabel(item: TimelineEntry): string;
|
|
13
|
+
export function isDiffContent(content: string): boolean;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export function groupTimelineItems(items, groupSystemItems = false) {
|
|
2
|
+
const grouped = [];
|
|
3
|
+
let toolItems = [];
|
|
4
|
+
let systemItems = [];
|
|
5
|
+
|
|
6
|
+
const flushTools = () => {
|
|
7
|
+
if (toolItems.length === 1) {
|
|
8
|
+
grouped.push({ kind: 'entry', id: toolItems[0].id, item: toolItems[0] });
|
|
9
|
+
} else if (toolItems.length > 1) {
|
|
10
|
+
grouped.push({ kind: 'tool-group', id: `tool-group-${toolItems[0].id}`, items: toolItems });
|
|
11
|
+
}
|
|
12
|
+
toolItems = [];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const flushSystems = () => {
|
|
16
|
+
if (systemItems.length === 1) {
|
|
17
|
+
grouped.push({ kind: 'entry', id: systemItems[0].id, item: systemItems[0] });
|
|
18
|
+
} else if (systemItems.length > 1) {
|
|
19
|
+
grouped.push({ kind: 'system-group', id: `system-group-${systemItems[0].id}`, items: systemItems });
|
|
20
|
+
}
|
|
21
|
+
systemItems = [];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
for (const item of items) {
|
|
25
|
+
if (item.toolName === 'subagent') {
|
|
26
|
+
flushTools(); flushSystems();
|
|
27
|
+
grouped.push({ kind: 'entry', id: item.id, item });
|
|
28
|
+
} else if (item.role === 'tool') {
|
|
29
|
+
flushSystems();
|
|
30
|
+
toolItems.push(item);
|
|
31
|
+
} else if (groupSystemItems && isGroupableSystemItem(item)) {
|
|
32
|
+
flushTools();
|
|
33
|
+
systemItems.push(item);
|
|
34
|
+
} else {
|
|
35
|
+
flushTools();
|
|
36
|
+
flushSystems();
|
|
37
|
+
grouped.push({ kind: 'entry', id: item.id, item });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
flushTools();
|
|
41
|
+
flushSystems();
|
|
42
|
+
return grouped;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isGroupableSystemItem(item) {
|
|
46
|
+
return item.role === 'system'
|
|
47
|
+
&& item.toolName !== 'reasoning'
|
|
48
|
+
&& item.entryType !== null
|
|
49
|
+
&& item.entryType !== 'branch_summary'
|
|
50
|
+
&& item.entryType !== 'compaction';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function toolLabel(item) {
|
|
54
|
+
const explicitName = item.toolName?.trim();
|
|
55
|
+
const firstLine = item.content
|
|
56
|
+
.split('\n')
|
|
57
|
+
.map((line) => line.trim())
|
|
58
|
+
.find((line) => line.length > 0);
|
|
59
|
+
if (!explicitName) return truncateToolLabel(firstLine ?? '工具操作');
|
|
60
|
+
|
|
61
|
+
const friendlyName = {
|
|
62
|
+
commandExecution: '命令执行',
|
|
63
|
+
fileRead: '读取文件',
|
|
64
|
+
fileChange: '修改文件',
|
|
65
|
+
mcpToolCall: 'MCP 工具',
|
|
66
|
+
webSearch: '网页搜索',
|
|
67
|
+
};
|
|
68
|
+
const label = friendlyName[explicitName] ?? explicitName.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
69
|
+
if (firstLine && firstLine !== explicitName && !firstLine.startsWith('{') && firstLine.length <= 64) {
|
|
70
|
+
return truncateToolLabel(`${label} · ${firstLine}`);
|
|
71
|
+
}
|
|
72
|
+
return truncateToolLabel(label);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isDiffContent(content) {
|
|
76
|
+
return /(^diff --git |^@@ |^\+\+\+ |^--- )/m.test(content);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function truncateToolLabel(value) {
|
|
80
|
+
return value.length > 88 ? `${value.slice(0, 85)}…` : value;
|
|
81
|
+
}
|
package/timeline.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {splitMessageAttachments} from './message-attachments.js';
|
|
2
|
+
import {renderMessageAttachment} from './metadata.js';
|
|
3
|
+
import {splitSessionReferences} from './session-references.js';
|
|
4
|
+
import {node,button,text,actionFor} from './tree.js';
|
|
5
|
+
import {renderRichText} from './rich-text.js';
|
|
6
|
+
import {groupTimelineItems} from './timeline-model.js';
|
|
7
|
+
|
|
8
|
+
export function renderTimeline(entries,actions,groupSystemItems=false,attachments=[]){
|
|
9
|
+
return groupTimelineItems(entries,groupSystemItems).map(group=>{
|
|
10
|
+
if(group.kind==='entry')return renderTimelineEntry(group.item,actions,attachments);
|
|
11
|
+
const key='message-group:'+group.id;
|
|
12
|
+
const tool=group.kind==='tool-group';
|
|
13
|
+
const completed=group.items.filter(item=>item.status==='completed').length;
|
|
14
|
+
return node('details',key,null,[
|
|
15
|
+
node('summary',key+':summary',tool?`工具调用 · ${group.items.length} 项 · ${completed}/${group.items.length} 完成`:`系统消息 · ${group.items.length} 项`),
|
|
16
|
+
...group.items.map(entry=>{
|
|
17
|
+
if(tool)return renderTimelineEntry(entry,actions,attachments);
|
|
18
|
+
return node('details','message:'+entry.id+':disclosure',null,[
|
|
19
|
+
node('summary','message:'+entry.id+':summary',(entry.content.split('\n')[0]||'系统消息')+' · 查看详情'),
|
|
20
|
+
renderTimelineEntry(entry,actions,attachments),
|
|
21
|
+
]);
|
|
22
|
+
}),
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const timelineStatusLabels={streaming:'生成中',completed:'完成',failed:'失败',queued:'排队中',interrupted:'已中断'};
|
|
28
|
+
const timelineToolLabels={commandExecution:'命令执行',fileRead:'读取文件',fileChange:'修改文件',mcpToolCall:'MCP 工具',webSearch:'网页搜索'};
|
|
29
|
+
|
|
30
|
+
/** Tool payloads are literal text; only conversational prose uses Markdown. */
|
|
31
|
+
export function renderTimelineEntry(entry,actions,attachments=[]){
|
|
32
|
+
const key='message:'+entry.id;
|
|
33
|
+
if(entry.toolName==='subagent') {
|
|
34
|
+
try {
|
|
35
|
+
const child=JSON.parse(entry.content);
|
|
36
|
+
const labels={pending:'正在启动',running:'运行中',waiting:'等待输入',completed:'已完成',failed:'失败',interrupted:'已中断',closed:'已关闭',unavailable:'过程暂不可用'};
|
|
37
|
+
return node('article',key,null,[node('strong',key+':name',child.name),text(key+':status',labels[child.status]??child.status),text(key+':task',child.task),text(key+':activity',child.activity),button(key+':open','查看工作过程',actionFor(actions,'openSubagent',child.id))]);
|
|
38
|
+
} catch { /* Retain a readable fallback for older malformed history. */ }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const reasoning=entry.role==='system'&&entry.toolName==='reasoning';
|
|
42
|
+
const header=node('header','message:header:'+entry.id,null,[
|
|
43
|
+
node('strong','message:role:'+entry.id,reasoning?'THINKING':entry.role.toUpperCase()),
|
|
44
|
+
text('message:tool:'+entry.id,entry.toolName),text('message:type:'+entry.id,entry.entryType),
|
|
45
|
+
text('message:status:'+entry.id,timelineStatusLabels[entry.status]??entry.status),
|
|
46
|
+
]);
|
|
47
|
+
let content;
|
|
48
|
+
if(entry.role==='tool'){
|
|
49
|
+
const diff=/(^diff --git |^@@ |^\+\+\+ |^--- )/m.test(entry.content);
|
|
50
|
+
const name=entry.toolName?.trim();
|
|
51
|
+
const label=timelineToolLabels[name]??name??'工具操作';
|
|
52
|
+
const hint=entry.entryType==='tool_call'?'查看调用参数':diff?'查看 diff':'查看工具输出';
|
|
53
|
+
content=node('details',key+':disclosure',null,[node('summary',key+':summary',`${label} · ${hint}`),
|
|
54
|
+
{...node('pre','message:content:'+entry.id,entry.content||'…'),className:diff?'tool-output diff-content':'tool-output'},
|
|
55
|
+
]);
|
|
56
|
+
}else{
|
|
57
|
+
const attached=entry.role==='user'?splitMessageAttachments(entry.content,attachments):{body:entry.content,attachments:[]};
|
|
58
|
+
const message=entry.role==='user'?splitSessionReferences(attached.body):{body:entry.content,references:[]};
|
|
59
|
+
content=node('div',key+':body',null,[message.body?renderRichText(message.body,'message:content:'+entry.id,entry.id,actions):message.references.length||attached.attachments.length?null:text(key+':empty','…'),
|
|
60
|
+
...attached.attachments.map(item=>renderMessageAttachment(item,key+':attachment:'+item.id)),
|
|
61
|
+
...message.references.map((reference,index)=>{const refKey=key+':reference:'+index;return node('details',refKey,null,[
|
|
62
|
+
node('summary',refKey+':title','引用会话 · '+reference.title),
|
|
63
|
+
text(refKey+':note',reference.agent+' · '+reference.note+(reference.omitted===null?'':' · 已省略 '+reference.omitted+' 条消息')),
|
|
64
|
+
...reference.excerpts.flatMap((excerpt,i)=>[text(refKey+':role:'+i,(excerpt.role==='user'?'用户':'助手')+(excerpt.truncated?' · 已截取':'')),node('pre',refKey+':text:'+i,excerpt.text)]),
|
|
65
|
+
]);}),
|
|
66
|
+
]);
|
|
67
|
+
if(reasoning)content=node('details',key+':disclosure',null,[node('summary',key+':summary','思考 · 查看详情'),content]);
|
|
68
|
+
}
|
|
69
|
+
const fork=entry.role==='assistant'&&entry.turnId&&actionFor(actions,'fork',entry.turnId);
|
|
70
|
+
return node('article',key,null,[header,content,fork?button('message:fork:'+entry.id,'从此处分叉',fork):null]);
|
|
71
|
+
}
|
package/tree.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const node=(tag,key,text,children=[],attrs={})=>({tag,key,...(text==null?{}:{text:String(text)}),children:children.filter(Boolean),attrs});
|
|
2
|
+
export const button=(key,label,action,attrs={})=>({...node('button',key,label,[],{type:'button',disabled:!action,...attrs}),...(action?{events:{[action.event]:action.token}}:{})});
|
|
3
|
+
export const field=(key,label,value,action,multiline=false)=>node('label',key,null,[node('span',key+':label',label),{...node(multiline?'textarea':'input',key+':input',null,[],{'aria-label':label,value:value??'',disabled:!action,...(multiline?{rows:'4'}:{type:'text'})}),...(action?{events:{[action.event]:action.token}}:{})}]);
|
|
4
|
+
export const section=(key,title,children)=>({...node('section',key,null,[node('h2',key+':title',title),...children]),className:key});
|
|
5
|
+
export const text=(key,value)=>value==null||value===''?null:node('p',key,value);
|
|
6
|
+
export const actionFor=(actions,operation,...args)=>actions.find(action=>action.operation===operation&&args.every((value,index)=>action.args?.[index]===value));
|
package/workbench.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {node,button,actionFor} from './tree.js';
|
|
2
|
+
import {renderNavigation} from './navigation.js';
|
|
3
|
+
import {renderConversation} from './conversation.js';
|
|
4
|
+
import {renderGit} from './git.js';
|
|
5
|
+
import {renderInspector} from './inspector.js';
|
|
6
|
+
import {renderCapability} from './capability.js';
|
|
7
|
+
export function renderWorkbench(input,renderSemantic){
|
|
8
|
+
const data=input.data;
|
|
9
|
+
if(!data?.navigation||!data?.conversation||!data?.git||!data?.inspector||!data?.capability)return node('main','workbench:empty','正在准备工作台',[],{role:'status'});
|
|
10
|
+
const gitActions=data.gitActions??[],inspectorActions=data.inspectorActions??[];
|
|
11
|
+
const toggle=actionFor(gitActions,'togglePanel');
|
|
12
|
+
const view=kind=>actionFor(gitActions,'selectView',kind);
|
|
13
|
+
const toolbar=node('nav','workbench:toolbar',null,[button('workbench:toggle','侧边面板',toggle,{'aria-expanded':String(data.inspector.open&&data.layout?.mode!=='focus')}),button('workbench:context','上下文',view('context'),{'aria-pressed':String(data.inspector.activeView==='context')}),button('workbench:git','Git',view('git'),{'aria-pressed':String(data.inspector.activeView==='git')})]);
|
|
14
|
+
if(data.layout){
|
|
15
|
+
const controls=(data.layout.mode==='focus'?[]:['navigation',...(data.layout.auxiliaryOpen?['auxiliary']:[])]).map(target=>{
|
|
16
|
+
const bounds=data.layout[target],action=actionFor(data.layoutActions??[],'resize',target);
|
|
17
|
+
const label=target==='navigation'?'导航宽度':'侧边面板宽度';
|
|
18
|
+
return node('label','layout:'+target,null,[node('span','layout:'+target+':label',`${label} ${Math.round(bounds.width)} 像素`),
|
|
19
|
+
{...node('input','layout:'+target+':input',null,[],{type:'range','aria-label':label,min:String(bounds.min),max:String(bounds.max),step:'1',value:String(bounds.width),disabled:!action}),...(action?{events:{input:action.token}}:{})},
|
|
20
|
+
]);
|
|
21
|
+
});
|
|
22
|
+
controls.unshift(...[['standard','标准布局'],['focus','专注会话'],['review','审阅布局']].map(([mode,label])=>button('layout:mode:'+mode,label,actionFor(data.layoutActions??[],'selectMode',mode),{'aria-pressed':String((data.layout.mode??'standard')===mode)})));
|
|
23
|
+
toolbar.children.push(node('details','workbench:layout',null,[node('summary','workbench:layout:summary','布局'),...controls]));
|
|
24
|
+
}
|
|
25
|
+
const content=data.capability.selected?renderCapability(data.capability,data.capabilityActions??[],renderSemantic):renderConversation(data.conversation,data.conversationActions??[]);
|
|
26
|
+
const main={...node('section','workbench:center',null,[toolbar,content,!data.capability.selected?node('details','workbench:capabilities',null,[node('summary','workbench:capabilities-title','能力插件'),renderCapability(data.capability,data.capabilityActions??[],renderSemantic)]):null]),className:'workbench-center'+(!data.capability.selected?' conversation-center':'')};
|
|
27
|
+
const navigation=renderNavigation(data.navigation,data.navigationActions??[]);
|
|
28
|
+
if(data.layout)navigation.inlineSize=data.layout.navigation.width;
|
|
29
|
+
const splitter=target=>{
|
|
30
|
+
const bounds=data.layout?.[target],action=actionFor(data.layoutActions??[],'resize',target);
|
|
31
|
+
if(!bounds||!action)return null;
|
|
32
|
+
return {...node('button','workbench:splitter:'+target,null,[],{type:'button',role:'separator','aria-orientation':'vertical','aria-label':target==='navigation'?'调整导航宽度':'调整侧边面板宽度','aria-valuenow':String(bounds.width),'aria-valuemin':String(bounds.min),'aria-valuemax':String(bounds.max)}),className:'workbench-splitter',resize:{token:action.token,value:bounds.width,min:bounds.min,max:bounds.max,direction:(target==='navigation'?1:-1)*(data.layout.mode==='review'?-1:1)}};
|
|
33
|
+
};
|
|
34
|
+
const children=[navigation,splitter('navigation'),main];
|
|
35
|
+
if(data.inspector.open)children.push(splitter('auxiliary'));
|
|
36
|
+
if(data.inspector.open)children.push({...node('aside','workbench:inspector',null,[data.inspector.activeView==='git'?renderGit(data.git,gitActions):renderInspector(data.inspector,inspectorActions)]),className:'workbench-inspector',...(data.layout?{inlineSize:data.layout.auxiliary.width}:{})});
|
|
37
|
+
const ordered=data.layout?.mode==='focus'?[main]:data.layout?.mode==='review'?[...children].reverse():children;
|
|
38
|
+
return {...node('div','workbench',null,ordered),className:'workbench'+(data.layout?.mode==='review'?' workbench-review':'')};
|
|
39
|
+
}
|