@aibolabs/presentation-workbench 0.2.2 → 0.2.4

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/i18n.js ADDED
@@ -0,0 +1,7 @@
1
+ import {translate} from './i18n.generated.js';
2
+
3
+ /** Legacy hosts omitted locale and rendered Chinese; no global mutable language state. */
4
+ export function presentationTranslator(locale = 'zh-CN') {
5
+ const language = locale === 'en' ? 'en' : 'zh-CN';
6
+ return (key, params) => translate(language, key, params);
7
+ }
package/inspector.js CHANGED
@@ -1,27 +1,30 @@
1
+ import {presentationTranslator} from './i18n.js';
2
+ import {formatNumber} from './i18n.generated.js';
1
3
  import {node,button,field,section,text,actionFor} from './tree.js';
2
4
  import {renderExecutionProfile,renderAttachment,renderSessionMetadata} from './metadata.js';
3
- export function renderInspector(state,actions){
5
+ export function renderInspector(state,actions,locale='zh-CN'){
6
+ const t=presentationTranslator(locale);
4
7
  const find=(operation,...args)=>actionFor(actions,operation,...args);
5
8
  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 children=[node('nav','inspector:tools',null,[act('refresh',t('inspector.refresh')),act('syncThreads',t('external.syncSessions'))])];
10
+ children.push(section('inspector:diagnostics',t('diagnostics.agents'),state.diagnostics.map(item=>section('diagnostic:'+item.agent,item.label,[text('diagnostic:status:'+item.agent,`${({ready:t('inspector.available'),missing:t('inspector.notInstalled'),error:t('inspector.error')})[item.status]??item.status} · ${({delegated:t('diagnostics.systemCredentials'),not_required:t('diagnostics.authNotRequired'),unknown:t('diagnostics.authUnknown')})[item.authState]??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))]))));
11
+ children.push(renderSessionMetadata(state.session,'inspector:session-metadata',locale),renderExecutionProfile(state.executionProfile,'inspector:execution-profile',locale),section('inspector:attachments',t('inspector.attachments'),state.attachments.length?state.attachments.map(item=>renderAttachment(item,'inspector:attachment:'+item.id,locale)):[text('inspector:no-attachments',t('external.attachmentHint'))]));
9
12
  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)]))));
13
+ if(inventory)for(const [key,label] of [['instructions',t('inspector.instructions')],['skills',t('composer.category.skill')],['tools',t('role.tool')],['mcpServers',t('external.mcpServers')]])children.push(section('inventory:'+key,label,inventory[key].map((item,i)=>text('inventory:'+key+':'+i,`${item.name} · ${item.source}`))));
14
+ if(inventory?.warnings.length)children.push(section('inventory:warnings',t('external.inspectionNotes'),inventory.warnings.map((value,i)=>text('inventory:warning:'+i,value))));
15
+ children.push(section('inspector:threads',t('external.threads'),state.threads.map(item=>text('thread:'+item.id,`${item.title??item.id} · ${item.status??''}${item.cwd?' · '+item.cwd:''}`))));
16
+ children.push(section('inspector:artifacts',t('search.kind.artifact'),state.artifacts.map(item=>node('article','artifact:'+item.id,null,[text('artifact:metadata:'+item.id,t('external.artifactMetadata',{source:item.source,mediaType:item.mediaType,size:formatNumber(locale,item.size)})),text('artifact:scope:'+item.id,item.turnId?t('external.turnScope',{turnId:item.turnId}):t('inspector.sessionLevel')),text('artifact:hash:'+item.id,item.contentHash?t('external.contentHash',{hash:item.contentHash}):null),act('toggleArtifact',state.artifactPreview.artifactId===item.id?(state.artifactPreview.error?t('external.retryArtifact'):t('external.collapseArtifact')):t('external.viewArtifact',{source:item.source}),item.id)]))));
14
17
  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)]))]));
18
+ if(preview.artifactId)children.push(section('inspector:preview',t('external.artifactPreview'),[act('closeArtifact',t('external.closeArtifact')),preview.loading?text('artifact:loading',t('external.loadingArtifact')):null,text('artifact:error',preview.error),preview.content?node('pre','artifact:content',preview.content.content,[],{tabindex:'0'}):null,preview.content?.truncated?text('artifact:truncated',t('external.artifactTruncated')):null]));
19
+ children.push(section('inspector:project-actions',t('project.title'),[act('newProjectAction',t('external.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?t('app.enabled'):t('app.disabled')),act('editProjectAction',t('external.editNamed',{name:item.name}),item.id),act('deleteProjectAction',t('external.deleteNamed',{name:item.name}),item.id),act('runProjectAction',t('external.runNamed',{name:item.name}),item.id)]))]));
17
20
  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)]))));
21
+ if(editor.open)children.push(section('inspector:editor',t('external.editProjectAction'),[...['name','program','args','cwd'].map((key,i)=>field('project:field:'+key,[t('external.name'),t('external.program'),t('external.jsonArguments'),t('external.workingDirectory')][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,[t('external.test'),t('external.lint'),t('external.build'),t('external.custom')][i],action,{'aria-pressed':String(editor.kind===kind)});})),text('project:save-error',editor.error),editor.saving?text('project:saving',t('external.saving')):null,act('saveProjectAction',t('external.saveProjectAction')),act('closeProjectEditor',t('external.closeEditor'))]));
22
+ children.push(section('inspector:runs',t('external.runHistory'),state.projectActionRuns.map(run=>node('article','run:'+run.id,null,[text('run:label:'+run.id,t('external.projectRunLabel',{name:run.actionName??run.actionId,status:run.status,exitCode:run.exitCode??'—'})),node('pre','run:output:'+run.id,run.output,[],{tabindex:'0'}),act('cancelProjectAction',t('external.cancelRun'),run.id)]))));
20
23
  const change=state.changeSet;
21
24
  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))]))]));
25
+ children.push(section('inspector:changes',t('inspector.turnChanges'),[text('change:status',`${change.attribution} · ${change.captureStatus}`),text('change:error',change.captureError),act('restoreTurn',t('external.restoreTurn'),change.turnId),...change.files.map(file=>node('article','change:file:'+file.path,null,[text('change:path:'+file.path,file.path),act('showDiff',t('semantic.inspect'),change.turnId,file.path),...['stage','unstage','revert'].map((operation,i)=>act('fileAction',[t('external.stageFile'),t('external.unstageFile'),t('external.restoreFile')][i],change.turnId,file.path,operation))])),...change.commands.map(command=>section('change:command:'+command.id,command.command??command.toolName??t('inspector.command'),[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,t('external.verificationStatus',{status:item.status}),[node('pre','change:verification-output:'+item.id,item.output)]))]));
26
+ 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',[t('external.stageHunk'),t('external.unstageHunk'),t('external.restoreHunk')][i],change.turnId,state.fileDiff.path,String(hunk.index),operation))]))]));
24
27
  }
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);
28
+ children.push(text('inspector:diff-error',state.fileDiffError),state.fileDiffLoading?text('inspector:diff-loading',t('external.loadingDiff')):null,section('inspector:checkpoints',t('external.checkpoints'),state.checkpoints.map(item=>text('checkpoint:'+item.id,`${item.path} · ${item.available?t('inspector.available'):item.reason??t('inspector.unavailable')}${item.baselineDirty?t('external.baselineDirtySuffix'):''}`))),section('inspector:restores',t('inspector.restoreHistory'),state.restoreOperations.map(item=>section('restore:'+item.id,item.status==='completed'?t('inspector.restored'):item.status==='blocked'?t('inspector.restoreBlocked'):item.status==='failed'?t('inspector.restoreFailed'):item.status,[text('restore:restored:'+item.id,t('external.restoredPaths',{paths:new Intl.ListFormat(locale).format(item.restored)})),text('restore:conflicts:'+item.id,t('external.conflictPaths',{paths:new Intl.ListFormat(locale).format(item.conflicts)})),text('restore:unsupported:'+item.id,t('external.unsupportedPaths',{paths:new Intl.ListFormat(locale).format(item.unsupported)}))]))));
29
+ return section('inspector',t('side.context'),children);
27
30
  }
package/markdown.d.ts CHANGED
@@ -14,6 +14,7 @@ export type MarkdownBlock = BlockBase & (
14
14
  | {kind: 'list'; ordered: boolean; start: number; items: {checked: boolean | null; blocks: MarkdownBlock[]}[]}
15
15
  | {kind: 'table'; align: ('left' | 'center' | 'right' | null)[]; header: InlineSegment[][]; rows: InlineSegment[][][]}
16
16
  );
17
+ /** Cached by source text with bounded retention; consumers must not mutate the returned blocks. */
17
18
  export function parseMarkdown(value: string): MarkdownBlock[];
18
19
  export function inlineSegments(value: string): InlineSegment[];
19
20
  export function displayMarkdown(value: string): string;
package/markdown.js CHANGED
@@ -3,6 +3,12 @@ import {decodeHTML} from 'entities';
3
3
  import {linkTarget} from './links.js';
4
4
 
5
5
  const options = {gfm: true, breaks: true};
6
+ // Shared by rendering and action discovery. Bound both entry count and source
7
+ // size so streaming revisions and sessions cannot retain unbounded history.
8
+ const parsed = new Map();
9
+ const maxEntries = 256;
10
+ const maxSourceLength = 512 * 1024;
11
+ let sourceLength = 0;
6
12
 
7
13
  // Only data leaves the lexer. HTML is displayed literally, never interpreted.
8
14
  function segments(tokens) {
@@ -31,6 +37,12 @@ export function inlineSegments(value) {
31
37
  }
32
38
 
33
39
  export function parseMarkdown(value) {
40
+ const cached = parsed.get(value);
41
+ if (cached) {
42
+ parsed.delete(value);
43
+ parsed.set(value, cached);
44
+ return cached;
45
+ }
34
46
  let index = 0;
35
47
  function blocks(tokens) {
36
48
  return tokens.filter(token => !['space', 'def', 'checkbox'].includes(token.type)).map(token => {
@@ -48,7 +60,17 @@ export function parseMarkdown(value) {
48
60
  ? [{kind: 'text', value: token.raw}] : segments(token.tokens ?? [{type: 'text', text: token.text ?? token.raw}])};
49
61
  });
50
62
  }
51
- return blocks(Lexer.lex(value.replace(/\r\n?/g, '\n'), options));
63
+ const result = blocks(Lexer.lex(value.replace(/\r\n?/g, '\n'), options));
64
+ if (value.length <= maxSourceLength) {
65
+ parsed.set(value, result);
66
+ sourceLength += value.length;
67
+ while (parsed.size > maxEntries || sourceLength > maxSourceLength) {
68
+ const oldest = parsed.keys().next().value;
69
+ parsed.delete(oldest);
70
+ sourceLength -= oldest.length;
71
+ }
72
+ }
73
+ return result;
52
74
  }
53
75
 
54
76
  export function displayMarkdown(content) {
package/metadata.js CHANGED
@@ -1,23 +1,28 @@
1
+ import {presentationTranslator} from './i18n.js';
2
+ import {formatDateTime} from './i18n.generated.js';
1
3
  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
+ export function renderExecutionProfile(profile,key,locale='zh-CN'){
5
+ const t=presentationTranslator(locale);
6
+ const labels={interactionMode:{ask:t('inspector.ask'),plan:t('inspector.plan'),edit:t('common.edit')},filesystemPolicy:{'agent-managed':t('inspector.nativePermissions'),'read-only':t('git.readOnly'),'workspace-write':t('inspector.workspaceWrite'),'danger-full-access':t('external.fullFileAccess')},commandPolicy:{'agent-managed':t('inspector.nativePermissions'),disabled:t('inspector.disabled'),approved:t('inspector.approvalRequired'),trusted:t('inspector.automatic')},networkPolicy:{disabled:t('inspector.disabled'),'agent-managed':t('inspector.agentManaged')},approvalPolicy:{never:t('external.neverApprove'),untrusted:t('external.approveUntrusted'),'on-request':t('external.approveOnRequest'),trusted:t('external.trustMode')},approvalReviewer:{user:t('role.user'),'auto-review':t('external.autoReview'),none:t('external.none')}};
4
7
  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
+ const fields=[['interactionMode',t('inspector.mode')],['filesystemPolicy',t('inspector.file')],['commandPolicy',t('inspector.command')],['networkPolicy',t('inspector.network')],['approvalPolicy',t('inspector.approval')],['approvalReviewer',t('inspector.reviewer')],['model',t('composer.model')],['reasoningEffort',t('composer.reasoning')]];
9
+ const value=(field,value)=>value==null?t('app.defaultModel'):labels[field]?.[value]??value;
10
+ return section(key,t('external.executionPermissions'),[text(key+':sandbox',profile.agentManagedPermissions?t('external.agentPermissions'):profile.nativeSandbox?t('inspector.nativeSandbox'):t('inspector.noNativeSandbox')),node('table',key+':table',null,[node('thead',key+':head',null,[node('tr',key+':columns',null,[t('search.kind.setting'),t('external.requestedValue'),t('external.enforcedValue')].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,t('external.profileUnsupported',{message:item}),[],{role:'status'})),text(key+':resolved',profile.resolvedAt?t('external.updatedAt',{date:formatDateTime(locale,profile.resolvedAt,{dateStyle:'medium',timeStyle:'short'})}):null),section(key+':capabilities',t('inspector.agentCapabilities'),(profile.adapterCapabilities??[]).length?profile.adapterCapabilities.map((item,i)=>text(key+':capability:'+i,item)):[text(key+':no-capabilities',t('external.noExtraCapabilities'))])]);
8
11
  }
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)]);
12
+ export function renderAttachment(item,key,locale='zh-CN'){
13
+ const t=presentationTranslator(locale);
14
+ return node('div',key,null,[text(key+':path',item.displayName??item.path),text(key+':state',t('external.attachmentState',{state:item.turnId?t('inspector.sent'):t('inspector.pending'),strategy:item.sendStrategy==='reference'?t('external.workspaceReference'):item.sendStrategy==='inline'?t('inspector.inline'):item.sendStrategy??t('external.missingSendStrategy'),size:item.size==null?'':t('external.bytesSuffix',{count:item.size})})),text(key+':media',item.mediaType),text(key+':source',item.source?t('external.source',{source:item.source}):null)]);
11
15
  }
12
- export function renderSessionMetadata(session,key){
16
+ export function renderSessionMetadata(session,key,locale='zh-CN'){
17
+ const t=presentationTranslator(locale);
13
18
  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)]);
19
+ return node('details',key,null,[node('summary',key+':summary',t('external.sessionInformation')),text(key+':id',t('external.sessionId',{id:session.id})),text(key+':external',session.externalSessionId?t('external.remoteBinding',{id:session.externalSessionId}):null),text(key+':updated',session.updatedAt?t('external.updatedAt',{date:formatDateTime(locale,session.updatedAt,{dateStyle:'medium',timeStyle:'short'})}):null),text(key+':archived',session.archived?t('session.status.archived'):null)]);
15
20
  }
16
21
 
17
- export function renderMessageAttachment(item,key){
18
- const name=(item.path??item.id).split(/[\\/]/).pop()||item.id;
22
+ export function renderMessageAttachment(item,key,locale='zh-CN'){
23
+ const name=item.displayName??((item.path??item.id).split(/[\\/]/).pop()||item.id);
19
24
  return {...node('div',key,null,[
20
25
  item.mediaType?.startsWith('image/')?{...node('img',key+':preview',null,[],{alt:name,width:'160',height:'112'}),resource:'attachment:'+item.id}:null,
21
26
  text(key+':name',name),
22
- ],{title:item.path??item.id}),className:'message-attachment'};
27
+ ],{title:item.displayName??item.path??item.id}),className:'message-attachment'};
23
28
  }
package/navigation.js CHANGED
@@ -1,30 +1,32 @@
1
+ import {presentationTranslator} from './i18n.js';
1
2
  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){
3
+ export function renderNavigation(state,actions,locale='zh-CN'){
4
+ const t=presentationTranslator(locale);
5
+ const labels={toggleSearch:t('external.searchSessions'),toggleFilter:t('sidebar.filter'),applyFilters:t('external.applyFilters'),addWorkspace:t('sidebar.addWorkspace'),toggleSessionCreator:t('search.newSession'),toggleTrust:t('external.changeTrust'),removeWorkspace:t('sidebar.removeWorkspace'),openWorkspace:t('external.openDirectory'),unarchiveSession:t('sidebar.unarchive'),archiveSession:t('sidebar.archiveHint'),syncSession:t('external.sync'),renameSession:t('external.rename'),saveRename:t('external.saveName'),cancelRename:t('external.cancelRename')};
6
+ const states={active:t('external.unarchived'),all:t('composer.category.all'),archived:t('session.status.archived'),created:t('external.created'),starting:t('session.status.starting'),running:t('subagent.status.running'),waiting_approval:t('external.awaitApproval'),waiting_user:t('external.awaitAnswer'),compacting:t('session.status.compacting'),idle:t('session.status.idle'),interrupted:t('timeline.interrupted'),failed:t('timeline.failed'),closed:t('subagent.status.closed')};
5
7
  const find=(operation,targetId)=>actions.find(action=>action.operation===operation&&action.targetId===targetId);
6
8
  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
+ const children=[node('h2','navigation:title',t('external.workspaces')),node('nav','navigation:tools',null,buttons(['addWorkspace','toggleFilter']))];
10
+ if(state.sessionFilterOpen){const action=find('filter');children.push(node('label','navigation:filter-label',null,[text('navigation:filter-text',t('external.sessionState')),{...node('select','navigation:filter',null,(action?.options??[]).map(value=>node('option','navigation:filter:'+value,states[value]??value,[],{value,selected:state.sessionFilter===value})),{'aria-label':t('external.sessionState'),value:state.sessionFilter}),...(action?{events:{change:action.token}}:{})}]),...buttons(['applyFilters']));}
9
11
  for(const workspace of state.workspaces){
10
12
  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))];
13
+ 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'?t('external.trusted'):t('external.untrusted')),node('nav','workspace:tools:'+workspace.id,null,buttons(['toggleSessionCreator','openWorkspace','toggleTrust','removeWorkspace'],workspace.id))];
12
14
  if(state.createSessionWorkspaceId===workspace.id)rows.push(node('nav','workspace:create:'+workspace.id,null,(state.agentChoices??[]).map(choice=>{
13
15
  const key='workspace:create:'+workspace.id+':'+choice.id;
14
16
  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});
17
+ const control=button(key,null,action,{'aria-label':t('external.createWithAgent',{name:choice.label}),title:choice.label});
16
18
  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
19
  return control;
18
20
  })));
19
21
  if(expanded){
20
- if(state.sessionsLoadingWorkspaceIds.includes(workspace.id))rows.push(node('p','workspace:loading:'+workspace.id,'正在加载会话',[],{role:'status'}));
22
+ if(state.sessionsLoadingWorkspaceIds.includes(workspace.id))rows.push(node('p','workspace:loading:'+workspace.id,t('external.loadingSessions'),[],{role:'status'}));
21
23
  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
+ 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,t('external.sessionStateLine',{agent:session.agent,state:states[session.state]??session.state,archived:session.archived?t('external.archivedSuffix'):''})),node('nav','session:tools:'+session.id,null,buttons(['syncSession','renameSession','archiveSession','unarchiveSession'],session.id))];
25
+ if(state.renamingSessionId===session.id)parts.push(field('session:rename:'+session.id,t('sidebar.sessionName'),state.sessionLabelDraft,find('renameDraft',session.id)),...buttons(['saveRename','cancelRename'],session.id));
24
26
  rows.push(node('article','session:row:'+session.id,null,parts));
25
27
  }
26
28
  }
27
29
  children.push(node('section','workspace:section:'+workspace.id,null,rows));
28
30
  }
29
- return {...node('aside','navigation',null,children,{'aria-label':'工作区与会话'}),className:'navigation'};
31
+ return {...node('aside','navigation',null,children,{'aria-label':t('sidebar.browse')}),className:'navigation'};
30
32
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aibolabs/presentation-workbench",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/rich-text.js CHANGED
@@ -1,16 +1,18 @@
1
+ import {presentationTranslator} from './i18n.js';
1
2
  import {node,button,actionFor} from './tree.js';
2
3
  import {parseMarkdown,displayMarkdown} from './markdown.js';
3
4
  import {highlightCode} from './code-highlight.js';
4
- import {linkTarget,linkLabels,linkIconPaths} from './links.js';
5
+ import {linkTarget,linkIconPaths} from './links.js';
5
6
 
6
- export function renderRichText(content,key,entryId,actions) {
7
+ export function renderRichText(content,key,entryId,actions,locale='zh-CN') {
8
+ const t=presentationTranslator(locale);
7
9
  const inline = (segments,prefix) => segments.map((segment,index) => {
8
10
  const id = prefix+':'+index;
9
11
  if (segment.kind === 'link') {
10
12
  const action = actions.find(action => action.operation === 'openLink' && action.args[0] === entryId && action.args[2] === segment.href);
11
13
  const kind = linkTarget(segment.href).kind;
12
14
  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'};
15
+ return {...button(id,null,action,{role:'link',title:t('markdown.link.'+kind,{href:segment.href})}),children:[icon,...inline(segment.children,id+':label')],className:'markdown-link'};
14
16
  }
15
17
  const tag = {code:'code',strong:'strong',em:'em',del:'del'}[segment.kind] ?? 'span';
16
18
  return {...node(tag,id,segment.children ? null : segment.value,segment.children ? inline(segment.children,id) : []),
@@ -23,13 +25,13 @@ export function renderRichText(content,key,entryId,actions) {
23
25
  const blocks = values => values.map(block => {
24
26
  const id = key+':block:'+block.index;
25
27
  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
+ node('header',id+':toolbar',null,[node('span',id+':language',block.language),button(id+':copy',t('external.copyCode'),actionFor(actions,'copyCode',entryId,String(block.index)))]),
29
+ node('pre',id+':pre',null,[node('code',id+':code',null,highlighted(highlightCode(block.lines.join('\n'),block.language),id+':highlight'))],{tabindex:'0','aria-label':t('markdown.code')}),
28
30
  ]),className:'markdown-code-block'};
29
31
  if (block.kind === 'list') return {...node(block.ordered ? 'ol' : 'ul',id,null,block.items.map((item,i) => {
30
32
  const itemId = id+':item:'+i;
31
33
  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,
34
+ item.checked !== null ? {...node('span',itemId+':check',item.checked ? '☑' : '☐',[],{role:'img','aria-label':item.checked ? t('markdown.completed') : t('markdown.pending')}),className:'markdown-task-check'} : null,
33
35
  {...node('div',itemId+':content',null,blocks(item.blocks)),className:'markdown-list-content'},
34
36
  ]),className:item.checked !== null ? 'markdown-task' : ''};
35
37
  }),block.ordered ? {start:String(block.start)} : {}),className:'markdown-list'};
@@ -43,7 +45,7 @@ export function renderRichText(content,key,entryId,actions) {
43
45
  return {...node('div',id,null,[{...node('table',id+':table',null,[
44
46
  node('thead',id+':head',null,[row(block.header,id+':header','th')]),
45
47
  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'};
48
+ ]),className:'markdown-table'}],{role:'region',tabindex:'0','aria-label':t('markdown.table')}),className:'markdown-table-scroll'};
47
49
  }
48
50
  const heading = block.kind === 'heading';
49
51
  return {...node(heading ? 'h'+block.level : 'p',id,null,inline(block.segments,id+':inline')),
@@ -9,5 +9,5 @@ export type TimelineRenderItem<T extends TimelineEntry = TimelineEntry> =
9
9
  | { kind: 'entry'; id: string; item: T }
10
10
  | { kind: 'tool-group' | 'system-group'; id: string; items: T[] };
11
11
  export function groupTimelineItems<T extends TimelineEntry>(items: T[], groupSystemItems?: boolean): TimelineRenderItem<T>[];
12
- export function toolLabel(item: TimelineEntry): string;
12
+ export function toolLabel(item: TimelineEntry, locale?: 'zh-CN' | 'en'): string;
13
13
  export function isDiffContent(content: string): boolean;
package/timeline-model.js CHANGED
@@ -1,3 +1,4 @@
1
+ import {presentationTranslator} from './i18n.js';
1
2
  export function groupTimelineItems(items, groupSystemItems = false) {
2
3
  const grouped = [];
3
4
  let toolItems = [];
@@ -22,7 +23,7 @@ export function groupTimelineItems(items, groupSystemItems = false) {
22
23
  };
23
24
 
24
25
  for (const item of items) {
25
- if (item.toolName === 'subagent') {
26
+ if (['subagent','background_task'].includes(item.toolName)) {
26
27
  flushTools(); flushSystems();
27
28
  grouped.push({ kind: 'entry', id: item.id, item });
28
29
  } else if (item.role === 'tool') {
@@ -50,20 +51,21 @@ function isGroupableSystemItem(item) {
50
51
  && item.entryType !== 'compaction';
51
52
  }
52
53
 
53
- export function toolLabel(item) {
54
+ export function toolLabel(item, locale='zh-CN') {
55
+ const t=presentationTranslator(locale);
54
56
  const explicitName = item.toolName?.trim();
55
57
  const firstLine = item.content
56
58
  .split('\n')
57
59
  .map((line) => line.trim())
58
60
  .find((line) => line.length > 0);
59
- if (!explicitName) return truncateToolLabel(firstLine ?? '工具操作');
61
+ if (!explicitName) return truncateToolLabel(firstLine ?? t('external.toolOperation'));
60
62
 
61
63
  const friendlyName = {
62
- commandExecution: '命令执行',
63
- fileRead: '读取文件',
64
- fileChange: '修改文件',
65
- mcpToolCall: 'MCP 工具',
66
- webSearch: '网页搜索',
64
+ commandExecution: t('external.toolCommand'),
65
+ fileRead: t('external.toolRead'),
66
+ fileChange: t('external.toolChange'),
67
+ mcpToolCall: t('external.toolMcp'),
68
+ webSearch: t('external.toolWeb'),
67
69
  };
68
70
  const label = friendlyName[explicitName] ?? explicitName.replace(/([a-z])([A-Z])/g, '$1 $2');
69
71
  if (firstLine && firstLine !== explicitName && !firstLine.startsWith('{') && firstLine.length <= 64) {
package/timeline.js CHANGED
@@ -1,3 +1,5 @@
1
+ import {presentationTranslator} from './i18n.js';
2
+ import {parseBackgroundTask} from './background-tasks.js';
1
3
  import {splitMessageAttachments} from './message-attachments.js';
2
4
  import {renderMessageAttachment} from './metadata.js';
3
5
  import {splitSessionReferences} from './session-references.js';
@@ -5,36 +7,40 @@ import {node,button,text,actionFor} from './tree.js';
5
7
  import {renderRichText} from './rich-text.js';
6
8
  import {groupTimelineItems} from './timeline-model.js';
7
9
 
8
- export function renderTimeline(entries,actions,groupSystemItems=false,attachments=[]){
10
+ export function renderTimeline(entries,actions,groupSystemItems=false,attachments=[],locale='zh-CN'){
11
+ const t=presentationTranslator(locale);
9
12
  return groupTimelineItems(entries,groupSystemItems).map(group=>{
10
- if(group.kind==='entry')return renderTimelineEntry(group.item,actions,attachments);
13
+ if(group.kind==='entry')return renderTimelineEntry(group.item,actions,attachments,locale);
11
14
  const key='message-group:'+group.id;
12
15
  const tool=group.kind==='tool-group';
13
16
  const completed=group.items.filter(item=>item.status==='completed').length;
14
17
  return node('details',key,null,[
15
- node('summary',key+':summary',tool?`工具调用 · ${group.items.length} 项 · ${completed}/${group.items.length} 完成`:`系统消息 · ${group.items.length} 项`),
18
+ node('summary',key+':summary',tool?t('external.toolGroup',{count:group.items.length,completed}):t('external.systemGroup',{count:group.items.length})),
16
19
  ...group.items.map(entry=>{
17
- if(tool)return renderTimelineEntry(entry,actions,attachments);
20
+ if(tool)return renderTimelineEntry(entry,actions,attachments,locale);
18
21
  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),
22
+ node('summary','message:'+entry.id+':summary',t('external.viewDetails',{title:entry.content.split('\n')[0]||t('timeline.systemMessage')})),
23
+ renderTimelineEntry(entry,actions,attachments,locale),
21
24
  ]);
22
25
  }),
23
26
  ]);
24
27
  });
25
28
  }
26
29
 
27
- const timelineStatusLabels={streaming:'生成中',completed:'完成',failed:'失败',queued:'排队中',interrupted:'已中断'};
28
- const timelineToolLabels={commandExecution:'命令执行',fileRead:'读取文件',fileChange:'修改文件',mcpToolCall:'MCP 工具',webSearch:'网页搜索'};
29
30
 
30
31
  /** Tool payloads are literal text; only conversational prose uses Markdown. */
31
- export function renderTimelineEntry(entry,actions,attachments=[]){
32
+ export function renderTimelineEntry(entry,actions,attachments=[],locale='zh-CN'){
33
+ const t=presentationTranslator(locale);
34
+ const timelineStatusLabels={streaming:t('timeline.streaming'),completed:t('timeline.completed'),failed:t('timeline.failed'),queued:t('timeline.queued'),interrupted:t('timeline.interrupted')};
35
+ const timelineToolLabels={commandExecution:t('external.toolCommand'),fileRead:t('external.toolRead'),fileChange:t('external.toolChange'),mcpToolCall:t('external.toolMcp'),webSearch:t('external.toolWeb')};
32
36
  const key='message:'+entry.id;
37
+ const background = parseBackgroundTask(entry,locale);
38
+ if(background)return node('article',key,null,[node('strong',key+':name',t('external.backgroundName',{name:background.name})),text(key+':status',t('background.status.'+background.status)),node('pre',key+':command',background.command),node('pre',key+':activity',background.activity),text(key+':id',t('external.taskId',{id:background.id})),background.exitCode!=null?text(key+':exit',t('external.exitCode',{code:background.exitCode})):null,background.outputPath?node('pre',key+':output',background.outputPath):null]);
33
39
  if(entry.toolName==='subagent') {
34
40
  try {
35
41
  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))]);
42
+ const labels={pending:t('subagent.status.pending'),running:t('subagent.status.running'),waiting:t('subagent.status.waiting'),completed:t('markdown.completed'),failed:t('timeline.failed'),interrupted:t('timeline.interrupted'),closed:t('subagent.status.closed'),unavailable:t('subagent.status.unavailable')};
43
+ 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',t('subagent.viewProcess'),actionFor(actions,'openSubagent',child.id))]);
38
44
  } catch { /* Retain a readable fallback for older malformed history. */ }
39
45
  }
40
46
 
@@ -48,24 +54,24 @@ export function renderTimelineEntry(entry,actions,attachments=[]){
48
54
  if(entry.role==='tool'){
49
55
  const diff=/(^diff --git |^@@ |^\+\+\+ |^--- )/m.test(entry.content);
50
56
  const name=entry.toolName?.trim();
51
- const label=timelineToolLabels[name]??name??'工具操作';
52
- const hint=entry.entryType==='tool_call'?'查看调用参数':diff?'查看 diff':'查看工具输出';
57
+ const label=timelineToolLabels[name]??name??t('external.toolOperation');
58
+ const hint=entry.entryType==='tool_call'?t('timeline.parameters'):diff?t('timeline.viewDiff'):t('subagent.viewTool');
53
59
  content=node('details',key+':disclosure',null,[node('summary',key+':summary',`${label} · ${hint}`),
54
60
  {...node('pre','message:content:'+entry.id,entry.content||'…'),className:diff?'tool-output diff-content':'tool-output'},
55
61
  ]);
56
62
  }else{
57
63
  const attached=entry.role==='user'?splitMessageAttachments(entry.content,attachments):{body:entry.content,attachments:[]};
58
64
  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','…'),
65
+ content=node('div',key+':body',null,[message.body?renderRichText(message.body,'message:content:'+entry.id,entry.id,actions,locale):message.references.length||attached.attachments.length?null:text(key+':empty','…'),
60
66
  ...attached.attachments.map(item=>renderMessageAttachment(item,key+':attachment:'+item.id)),
61
67
  ...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)]),
68
+ node('summary',refKey+':title',t('external.referenceTitle',{title:reference.title})),
69
+ text(refKey+':note',reference.omitted===null?reference.agent+' · '+reference.note:t('external.referenceOmitted',{agent:reference.agent,note:reference.note,count:reference.omitted})),
70
+ ...reference.excerpts.flatMap((excerpt,i)=>[text(refKey+':role:'+i,(excerpt.role==='user'?t('role.user'):t('role.assistant'))+(excerpt.truncated?t('timeline.excerptTruncated'):'')),node('pre',refKey+':text:'+i,excerpt.text)]),
65
71
  ]);}),
66
72
  ]);
67
- if(reasoning)content=node('details',key+':disclosure',null,[node('summary',key+':summary','思考 · 查看详情'),content]);
73
+ if(reasoning)content=node('details',key+':disclosure',null,[node('summary',key+':summary',t('timeline.reasoningDetails')),content]);
68
74
  }
69
75
  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]);
76
+ return node('article',key,null,[header,content,fork?button('message:fork:'+entry.id,t('external.forkFromHere'),fork):null]);
71
77
  }
package/workbench.js CHANGED
@@ -1,3 +1,4 @@
1
+ import {presentationTranslator} from './i18n.js';
1
2
  import {node,button,actionFor} from './tree.js';
2
3
  import {renderNavigation} from './navigation.js';
3
4
  import {renderConversation} from './conversation.js';
@@ -5,35 +6,36 @@ import {renderGit} from './git.js';
5
6
  import {renderInspector} from './inspector.js';
6
7
  import {renderCapability} from './capability.js';
7
8
  export function renderWorkbench(input,renderSemantic){
9
+ const locale=input.locale??'zh-CN',t=presentationTranslator(locale);
8
10
  const data=input.data;
9
- if(!data?.navigation||!data?.conversation||!data?.git||!data?.inspector||!data?.capability)return node('main','workbench:empty','正在准备工作台',[],{role:'status'});
11
+ if(!data?.navigation||!data?.conversation||!data?.git||!data?.inspector||!data?.capability)return node('main','workbench:empty',t('external.preparing'),[],{role:'status'});
10
12
  const gitActions=data.gitActions??[],inspectorActions=data.inspectorActions??[];
11
13
  const toggle=actionFor(gitActions,'togglePanel');
12
14
  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')})]);
15
+ const toolbar=node('nav','workbench:toolbar',null,[button('workbench:toggle',t('external.sidePanel'),toggle,{'aria-expanded':String(data.inspector.open&&data.layout?.mode!=='focus')}),button('workbench:context',t('side.context'),view('context'),{'aria-pressed':String(data.inspector.activeView==='context')}),button('workbench:git','Git',view('git'),{'aria-pressed':String(data.inspector.activeView==='git')})]);
14
16
  if(data.layout){
15
17
  const controls=(data.layout.mode==='focus'?[]:['navigation',...(data.layout.auxiliaryOpen?['auxiliary']:[])]).map(target=>{
16
18
  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
+ const label=target==='navigation'?t('external.navigationWidth'):t('external.sidePanelWidth');
20
+ return node('label','layout:'+target,null,[node('span','layout:'+target+':label',t('external.widthPixels',{label,width:Math.round(bounds.width)})),
19
21
  {...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
22
  ]);
21
23
  });
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
+ controls.unshift(...[['standard',t('workbench.standard')],['focus',t('workbench.focus')],['review',t('external.reviewLayout')]].map(([mode,label])=>button('layout:mode:'+mode,label,actionFor(data.layoutActions??[],'selectMode',mode),{'aria-pressed':String((data.layout.mode??'standard')===mode)})));
25
+ toolbar.children.push(node('details','workbench:layout',null,[node('summary','workbench:layout:summary',t('settings.layout')),...controls]));
24
26
  }
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??[]);
27
+ const content=data.capability.selected?renderCapability(data.capability,data.capabilityActions??[],renderSemantic,locale):renderConversation(data.conversation,data.conversationActions??[],locale);
28
+ const main={...node('section','workbench:center',null,[toolbar,content,!data.capability.selected?node('details','workbench:capabilities',null,[node('summary','workbench:capabilities-title',t('external.capabilityPlugins')),renderCapability(data.capability,data.capabilityActions??[],renderSemantic,locale)]):null]),className:'workbench-center'+(!data.capability.selected?' conversation-center':'')};
29
+ const navigation=renderNavigation(data.navigation,data.navigationActions??[],locale);
28
30
  if(data.layout)navigation.inlineSize=data.layout.navigation.width;
29
31
  const splitter=target=>{
30
32
  const bounds=data.layout?.[target],action=actionFor(data.layoutActions??[],'resize',target);
31
33
  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)}};
34
+ return {...node('button','workbench:splitter:'+target,null,[],{type:'button',role:'separator','aria-orientation':'vertical','aria-label':target==='navigation'?t('external.resizeNavigation'):t('external.resizeSidePanel'),'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
35
  };
34
36
  const children=[navigation,splitter('navigation'),main];
35
37
  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}:{})});
38
+ if(data.inspector.open)children.push({...node('aside','workbench:inspector',null,[data.inspector.activeView==='git'?renderGit(data.git,gitActions,locale):renderInspector(data.inspector,inspectorActions,locale)]),className:'workbench-inspector',...(data.layout?{inlineSize:data.layout.auxiliary.width}:{})});
37
39
  const ordered=data.layout?.mode==='focus'?[main]:data.layout?.mode==='review'?[...children].reverse():children;
38
40
  return {...node('div','workbench',null,ordered),className:'workbench'+(data.layout?.mode==='review'?' workbench-review':'')};
39
41
  }