@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/README.md CHANGED
@@ -4,10 +4,11 @@
4
4
  宿主动作目录转换成视觉树,不引用宿主代码、DOM、框架或业务执行端口。
5
5
 
6
6
  当前提供 `renderNavigation`、`renderConversation`、`renderGit`、`renderInspector`,
7
- 参数均为 `(state, actions)`;`renderCapability(state, actions, renderSemantic)`
7
+ 参数为 `(state, actions, locale?)`。
8
+ `renderCapability(state, actions, renderSemantic, locale?)`
8
9
  接收皮肤自己的语义 renderer。
9
10
  调用方拥有样式;模块只生成语义结构、可访问性属性及当前目录中的动作绑定。
10
- 0.2.0 随双皮肤 0.3.0 交付导航、会话、Git、Inspector 与能力视图的完整工作台;当前版本 0.2.2 随双皮肤 0.4.1 交付。
11
+ 0.2.0 随双皮肤 0.3.0 交付导航、会话、Git、Inspector 与能力视图的完整工作台;当前版本 0.2.4 随双皮肤 0.4.3 交付。
11
12
  执行权限、附件状态、诊断、会话绑定、三种布局、草稿以及跨默认呈现的焦点/滚动
12
13
  恢复均由宿主提供数据与权限;包只决定呈现。macOS arm64 原生生命周期与
13
14
  Chromium 浏览器交互已有验收记录,未宣称其他 OS 或所有屏幕阅读器通过认证。
@@ -61,3 +62,12 @@ Shift/Ctrl+Enter 换行。没有对应动作时回车不提交,输入法组合
61
62
 
62
63
  桌面会话历史与编辑区独立滚动,附件详情可展开;默认视图与外置视图按消息标识
63
64
  恢复位置。外置核心语义不冒充内置专业阅读,存在专业偏好时显示通用降级提示。
65
+
66
+ 界面语言由宿主 `PresentationInput.locale` 提供,缺省按简体中文兼容旧宿主。
67
+ 导航、布局、时间线、执行权限与 Markdown 操作显式接收语言参数;原始用户、Agent、
68
+ 路径、代码和插件字段保持原文。语言不会改变 key、动作 token 或上下文。
69
+ 独立皮肤的控件与语义视图同样使用这个字段。会话 Composer、Git、Inspector 与能力页也接入相同词条及语言参数。
70
+
71
+ `i18n.generated.js` 从仓库的规范词条和纯翻译器打包生成,使 npm tarball 无需读取
72
+ 仓库其他目录;不是手工维护的第二份词条。修改规范词条后运行
73
+ `node scripts/build-presentation-i18n.mjs`,测试检查生成产物同步。
@@ -0,0 +1,11 @@
1
+ import {presentationTranslator} from './i18n.js';
2
+ export const backgroundTaskLabels = {running:'运行中',completed:'已完成',failed:'失败',stopped:'已停止',unknown:'状态未知'};
3
+ export function parseBackgroundTask(item,locale='zh-CN') {
4
+ if (item.toolName !== 'background_task') return null;
5
+ try {
6
+ const task = JSON.parse(item.content);
7
+ if (!task || !['id','rootTurnId','name','command','activity'].every(key => typeof task[key] === 'string') || !Object.hasOwn(backgroundTaskLabels, task.status)) return null;
8
+ if (task.status === 'running' && item.status !== 'streaming') return {...task,status:'unknown',activity:presentationTranslator(locale)('background.disconnected')};
9
+ return task;
10
+ } catch { return null; }
11
+ }
package/bundle.mjs CHANGED
@@ -4,9 +4,9 @@ import {fileURLToPath} from 'node:url';
4
4
  /** Bundle the fixed workbench entry and installed dependencies for the Worker. */
5
5
  export async function workbenchSource() {
6
6
  const result = await build({
7
- stdin: {contents: "export {renderWorkbench} from './workbench.js';", resolveDir: fileURLToPath(new URL('.', import.meta.url))},
7
+ stdin: {contents: "export {renderWorkbench} from './workbench.js';export {presentationTranslator} from './i18n.js';", resolveDir: fileURLToPath(new URL('.', import.meta.url))},
8
8
  bundle: true, write: false, format: 'iife', globalName: 'aiboWorkbenchModule',
9
9
  platform: 'browser', target: 'es2022', minify: true,
10
10
  });
11
- return result.outputFiles[0].text + '\nself.aiboWorkbench=aiboWorkbenchModule.renderWorkbench;\n';
11
+ return result.outputFiles[0].text + '\nself.aiboWorkbench=aiboWorkbenchModule.renderWorkbench;self.aiboTranslate=aiboWorkbenchModule.presentationTranslator;\n';
12
12
  }
package/capability.js CHANGED
@@ -1,18 +1,20 @@
1
+ import {presentationTranslator} from './i18n.js';
1
2
  import {node,button,section,text,actionFor} from './tree.js';
2
3
  /** The skin supplies its semantic renderer; only host-bound semantic actions are forwarded. */
3
- export function renderCapability(state,actions,renderSemantic){
4
+ export function renderCapability(state,actions,renderSemantic,locale='zh-CN'){
5
+ const t=presentationTranslator(locale);
4
6
  const find=(operation,...args)=>actionFor(actions,operation,...args);
5
7
  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));}
8
+ for(const [operation,label] of [['close',t('external.closeCapabilityView')],['reload',t('workbench.reload')],['toggleLayout',t('workbench.changeLayout')],['toggleReading',state.view.enhanced?t('workbench.genericReading'):t('external.trySpecialized')]]){const action=find(operation);if(action)controls.push(button('capability:'+operation,label,action));}
7
9
  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
10
  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);
11
+ 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',t('external.restoringView')):null);
10
12
  if(state.view.snapshot){
11
13
  const snapshot=state.view.snapshot;
12
- if(snapshot.view.kind==='detail'&&state.view.enhanced)children.push(node('p','capability:reading-fallback','专业阅读界面不可用,已使用通用视图。',[],{role:'status'}));
14
+ if(snapshot.view.kind==='detail'&&state.view.enhanced)children.push(node('p','capability:reading-fallback',t('workbench.genericFallback'),[],{role:'status'}));
13
15
  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
16
  children.push(renderSemantic({snapshot,actions:semanticActions}));
15
17
  }
16
18
  }
17
- return section('capability','能力视图',children);
19
+ return section('capability',t('external.capabilityView'),children);
18
20
  }
package/conversation.js CHANGED
@@ -1,18 +1,21 @@
1
+ import {presentationTranslator} from './i18n.js';
2
+ import {formatNumber} from './i18n.generated.js';
1
3
  import {splitMessageAttachments} from './message-attachments.js';
2
4
  import {node,button,field,section,text,actionFor} from './tree.js';
3
5
  import {renderExecutionProfile,renderMessageAttachment,renderSessionMetadata} from './metadata.js';
4
6
  import {splitSessionReferences} from './session-references.js';
5
7
  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
+ export function renderConversation(state,actions,locale='zh-CN'){
9
+ const t=presentationTranslator(locale);
10
+ const labels={pauseGoal:t('goal.pause'),resumeGoal:t('goal.resume'),clearGoal:t('goal.clear'),send:t('composer.send'),stop:t('history.stop'),retry:t('app.retry'),queueSteer:t('composer.sendNow'),queueFollowUp:t('composer.queue'),clearQueue:t('external.clearQueue'),resumeQueue:t('queue.resume'),addAttachments:t('external.addAttachments'),addDirectory:t('composer.addFolder'),loadOlder:t('external.loadOlderMessages'),fork:t('external.forkSession'),loadModels:t('external.refreshModels'),compact:t('commands.compact'),openTree:t('tree.title'),closeTree:t('tree.close'),refreshTree:t('external.refreshTree'),submitAnswers:t('input.submit'),cancelAnswers:t('external.cancelAnswers')};
8
11
  const find=(operation,...args)=>actionFor(actions,operation,...args);
9
12
  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'])]));
13
+ const children=[node('header','conversation:header',null,[node('h1','conversation:title',state.session?.label??t('external.selectSession')),text('conversation:activity',state.activityLabel),node('nav','conversation:tools',null,controls(['fork','compact','openTree']))])];
14
+ if(state.session?.historyOnly)children.push(text('conversation:history-only',t('composer.historyOnly')));
15
+ children.push(renderSessionMetadata(state.session,'conversation:session-metadata',locale));
16
+ const messages=renderTimeline(state.timelineVisibleCount>0?state.timeline.slice(-state.timelineVisibleCount):[],actions,state.groupSystemItems===true,state.attachments,locale);
17
+ children.push({...node('section','conversation:timeline',null,[...controls(['loadOlder']),...messages],{'aria-label':t('external.sessionMessages')}),className:'timeline'});
18
+ if(state.retryReason||state.retryPrompt)children.push(section('conversation:retry',t('app.retry'),[text('retry:reason',state.retryReason),text('retry:prompt',state.retryPrompt),...controls(['retry'])]));
16
19
  for(const request of state.userInputRequests){
17
20
  const questions=request.questions.map(question=>{
18
21
  const prefix='question:'+request.requestId+':'+question.id;
@@ -23,65 +26,65 @@ export function renderConversation(state,actions){
23
26
  });
24
27
  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
28
  });
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;})]));
29
+ children.push(section('request:'+request.requestId,t('external.needAnswers'),[...questions,...['submitAnswers','cancelAnswers'].map(operation=>{const action=find(operation,request.requestId);return action?button('request:'+request.requestId+':'+operation,labels[operation],action):null;})]));
27
30
  }
28
31
  for(const request of (state.approvalRequests??[]).filter(request=>request.sessionId===state.session?.id)){
29
32
  const prefix='approval:'+request.requestId;
30
33
  // 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,
34
+ 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'?t('approval.allow'):t('approval.reject'))]):request.availableDecisions.map(decision=>['decision',decision,decision==='accept'?t('approval.allow'):t('approval.reject')]);
35
+ children.push(section(prefix,t('approval.required'),[text(prefix+':kind',request.kind),request.command?text(prefix+':command',request.command):null,request.cwd?text(prefix+':cwd',request.cwd):null,
33
36
  ...choices.map(([kind,value,label])=>{const action=find('resolveApproval',request.requestId,kind,value);return action?button(prefix+':'+kind+':'+value,label,action):null;})]));
34
37
  }
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
+ if(state.queue && (state.queue.items?.length || state.queue.steering.length || state.queue.followUp.length))children.push(section('conversation:queue',t('external.pendingMessages'),[
39
+ state.queue.paused?text('queue:paused',t('queue.paused')):null,
40
+ ...(state.queue.items?.length?state.queue.items.map(item=>section('queue:item:'+item.id,item.status==='sending'?t('external.sendingMessage'):item.status==='uncertain'?t('external.deliveryUnknown'):item.status==='failed'?t('queue.failed'):t('external.waitingToSend'),[
38
41
  text('queue:text:'+item.id,splitSessionReferences(item.text).body.split('[AIBO_CONTEXT_ATTACHMENTS]')[0].trim()),
39
42
  ...splitMessageAttachments(item.text,state.attachments).attachments.map(attachment=>renderMessageAttachment(attachment,'queue:'+item.id+':attachment:'+attachment.id)),
40
43
  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
+ (!state.running || state.session?.capabilities.includes('queue.steer')) ? button('queue:send:'+item.id,t('composer.sendNow'),find('sendQueuedMessage',item.id)) : null,
45
+ button('queue:remove:'+item.id,t('git.deleted'),find('removeQueuedMessage',item.id)),
46
+ ])):[...state.queue.steering.map((value,i)=>text('queue:steer:'+i,t('external.steeringMessage',{text:value}))),...state.queue.followUp.map((value,i)=>text('queue:follow:'+i,t('external.followUpMessage',{text:value})))]),
44
47
  ...controls(['resumeQueue','clearQueue'])]));
45
48
  if(state.session){
46
- const draftField=field('conversation:draft','消息',state.draft,find('draft'),true);
49
+ const draftField=field('conversation:draft',t('external.message'),state.draft,find('draft'),true);
47
50
  const submit=find(state.running?'queueFollowUp':'send');
48
51
  draftField.children[1].submitOnEnter=true;
49
52
  if(submit)draftField.children[1].primaryEnter=submit.token;
50
53
  const goal = state.goal && state.goal.status !== 'cleared' ? {...node('details','conversation:goal',null,[
51
54
  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}`),
55
+ text('goal:status',state.goal.status === 'active' ? (state.running ? t('goal.status.activeRunning') : t('goal.status.activeIdle')) : state.goal.status === 'paused' && state.running ? t('goal.status.pausedRunning') : ({paused:t('goal.status.paused'),completed:t('goal.status.completed'),blocked:t('goal.status.blocked'),usageLimited:t('goal.status.usageLimited'),budgetLimited:t('goal.status.budgetLimited')})[state.goal.status] ?? t('goal.status.unknown')),
56
+ text('goal:budget',state.goal.tokenBudget == null ? null : t('external.goalBudget',{used:formatNumber(locale,state.goal.tokensUsed??0),budget:formatNumber(locale,state.goal.tokenBudget)})),
54
57
  ...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];
58
+ ],{open:true,'aria-label':t('goal.current')}),className:'goal-bar'} : null;
59
+ const composer=[goal,draftField,text('conversation:shortcut',t('external.composerShortcut',{action:state.running?t('composer.queue'):t('composer.send')})),state.draftFailed?node('p','conversation:draft-error',t('external.draftSaveFailed'),[],{role:'alert'}):null];
57
60
 
58
61
  composer.push(node('nav','conversation:composer-tools',null,controls(['addAttachments','addDirectory','send','stop','queueSteer','queueFollowUp'])));
59
62
  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}));
63
+ 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,t('external.removeAttachment',{name:item.displayName??item.path}),find('removeAttachment',item.id))])));
64
+ if(pendingAttachments.length)composer.push(node('details','conversation:attachments',null,[node('summary','conversation:attachments:summary',t('external.attachmentsCount',{count:pendingAttachments.length})),attachmentList],{open:true}));
62
65
  const mention=/(?:^|\s)@[^\s]*$/.test(state.draft);
63
66
  const slash=state.draft.match(/^\/([^\s]*)$/);
64
67
  const categoryOf=command=>command.category??(command.source==='skill'?'skill':command.source==='extension'||command.source==='prompt'?'extension':'agent');
65
68
  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)})):[];
69
+ const groups=slash?['all','agent','skill','extension'].map(id=>[id,t('composer.category.'+id)]).map(([id,label])=>({id,label,commands:matches.filter(command=>id==='all'||categoryOf(command)===id)})):[];
67
70
  const commands=slash?[...new Map(groups.flatMap(group=>group.commands).map(command=>[command.name,command])).values()]:matches;
68
71
  const pathButtons=state.workspacePathSuggestions.slice(0,24).map(item=>button('path:'+item.path,item.path+(item.isDirectory?'/':''),find('selectPath',item.path),{role:'option'}));
69
72
  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]})) : [];
73
+ const sessionButtons=(state.sessionSuggestions??[]).map(item=>button('session-reference:'+item.id,t('external.sessionSuggestion',{label:item.label,agent:item.agent,archived:item.archived?t('external.archivedSuffix'):''}),find('selectSessionReference',item.id),{role:'option'}));
74
+ const mentionGroups=mention?['all','files','folders','sessions'].map(id=>[id,t('composer.category.'+id)]).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
75
  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]));
76
+ 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':t('composer.referenceCategories')}),node('div','conversation:mention-options',null,mentionButtons,{role:'listbox','aria-label':t('composer.referenceSuggestions')})],{role:'group','aria-label':t('external.mentionCompletion')}));
77
+ 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':t('composer.commandCategories')}),node('div','conversation:command-options',null,commandButtons,{role:'listbox','aria-label':t('composer.commandSuggestions')})],{role:'group','aria-label':t('external.commandCompletion')}):node('details','conversation:commands',null,[node('summary','commands:title',t('inspector.command')),...commandButtons]));
75
78
  const suggestionButtons=mention?mentionButtons:slash?commandButtons:[];
76
79
  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}))]));
80
+ 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,t('external.defaultReasoning'),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
81
  const sequential=state.modelCatalog?.parameterScope==='current-model';
79
82
  const currentModel=state.modelCatalog?.current;
80
83
  const currentEfforts=currentModel?.reasoningEfforts??[];
81
84
  const selectedEffort=currentEfforts.find(option=>option.id===state.modelConfiguration.currentReasoningEffort);
82
85
  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?'正在更新…':'此模型无可选推理强度')]),
86
+ section('models:selection',t('composer.model'),(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}))),
87
+ section('models:reasoning',t('composer.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?t('composer.updating'):t('composer.noReasoning'))]),
85
88
  ]:matrixModels;
86
89
  const fastTier=state.modelCatalog?.current?.serviceTiers?.find(tier=>tier.label?.trim().toLowerCase()==='fast');
87
90
  const fastAction=fastTier?find('selectServiceTier',state.modelCatalog?.currentServiceTier===fastTier.id?'default':fastTier.id):null;
@@ -89,26 +92,26 @@ export function renderConversation(state,actions){
89
92
  const contextOptions=state.modelCatalog?.current?.contextWindows??[];
90
93
  const contextCurrent=state.modelCatalog?.currentContextWindow;
91
94
  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:''}),
95
+ const contextSelect=node('label','models:context-label',null,[text('models:context-title',t('side.context')),{
96
+ ...node('select','models:context',null,[...(!contextKnown?[node('option','models:context:unknown',contextOptions.length?t('context.noCurrent'):t('context.unsupported'),[],{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':t('context.size'),disabled:!contextAction,value:contextKnown?contextCurrent:''}),
94
97
  ...(contextAction?{events:{change:contextAction.token}}:{}),
95
98
  }]);
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')]));
99
+ composer.push(node('details','conversation:models' ,null,[node('summary','models:title',(currentModel?.label??t('composer.model'))+(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',t('external.loadingModels')):null,...models]));
100
+ composer.push(node('details','conversation:access',null,[node('summary','access:title',t('composer.sessionSettings')),...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',locale)]));
98
101
  if(state.usage){
99
102
  const usage=state.usage;
100
103
  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
+ const limits=[...(usage.limits??[]),...(usage.unknownLimits??[]).map(limit=>({...limit,usedPercent:null}))].map(limit=>t('external.quotaLine',{label:limit.label??(limit.windowMinutes?t('external.minutes',{count:limit.windowMinutes}):t('usage.plan')),amount:limit.usedPercent===null?t('usage.unknownQuota'):t('external.quotaRemaining',{percent:Math.max(0,100-Math.round(limit.usedPercent))}),observation:limit.observedAt!==undefined?t(limit.resetsAt===null?'external.lastObservedUnknownReset':'external.lastObserved'):''}));
105
+ const credits=usage.credits?.unlimited?t('usage.unlimitedCredits'):usage.credits?.balance?'Credits '+usage.credits.balance:null;
106
+ composer.push(node('p','conversation:usage',[percent===null?usage.contextUsed===null?null:t('external.contextTokens',{count:formatNumber(locale,usage.contextUsed)}):t('external.contextPercent',{percent,estimated:usage.contextEstimated?t('usage.estimatedDetail'):''}),usage.total===null?null:'Token '+usage.total,usage.plan?.toUpperCase(),...limits,credits].filter(Boolean).join(' · '),[],{'aria-label':t('usage.accessibility')}));
104
107
  }
105
- children.push(section('conversation:composer','撰写消息',composer));
108
+ children.push(section('conversation:composer',t('external.composeMessage'),composer));
106
109
  }
107
110
  if(state.treeOpen){
108
111
  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??[]))]));
112
+ children.push(section('conversation:tree',t('tree.title'),[...controls(['closeTree','refreshTree']),text('tree:status',state.treeNavigationStatus),node('ul','tree:roots',null,branch(state.tree?.tree??[]))]));
110
113
  }
111
114
  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'};
115
+ const history={...node('section','conversation:history',null,children.filter(child=>child?.key!=='conversation:composer'),{'aria-label':t('search.sessionHistory'),tabindex:'0'}),className:'conversation-history'};
116
+ return {...node('main','conversation',null,[history,composer],{'aria-label':t('scope.session')}),className:'conversation'};
114
117
  }
package/git.js CHANGED
@@ -1,59 +1,64 @@
1
+ import {presentationTranslator} from './i18n.js';
2
+ import {formatDateTime} from './i18n.generated.js';
1
3
  import {node,button,field,section,text,actionFor} from './tree.js';
2
- export function renderGit(state,actions){
4
+ export function renderGit(state,actions,locale='zh-CN'){
5
+ const t=presentationTranslator(locale);
6
+ const kinds={added:t('git.added'),modified:t('git.modified'),deleted:t('git.deleted'),renamed:t('git.renamed')};
7
+ const kindLabel=kind=>Object.hasOwn(kinds,kind)?kinds[kind]:kind;
3
8
  const find=(operation,...args)=>actionFor(actions,operation,...args);
4
9
  const act=(operation,label,...args)=>{const action=find(operation,...args);return action?button('git:action:'+operation+':'+JSON.stringify(args),label,action):null;};
5
10
  const children=[];
6
11
  const repositories=state.repositories;
7
12
  const current=repositories?.find(repo=>repo.id===state.repositoryId);
8
13
  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','继续扫描'));
14
+ children.push(text('git:repository-target',current?`${current.name} · ${current.relativePath}${current.externalRoot?t('git.externalRootSuffix'):''}`:t('repository.all')));
15
+ if(repositories.length>1||state.repositoryId===null)children.push(node('details','git:repository-picker',null,[node('summary','git:repository-picker-title',current?t('external.switchRepository'):t('external.selectRepositoryHint')),field('git:repository-search',t('external.searchRepositories'),state.repositorySearch,find('repositorySearch')),act('selectRepository',t('repository.all'),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}:{}));
16
+ if(state.discoveryLimited)children.push(text('git:discovery-limited',t('git.discoveryLimited')),act('continueDiscovery',t('git.continueDiscovery')));
12
17
  for(const [index,warning] of (state.discoveryWarnings??[]).entries())children.push(text('git:discovery-warning:'+index,warning));
13
18
  }
14
19
  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 仓库'));
20
+ children.push(act('refresh',t('external.refreshRepositories')),act('selectSection',t('git.history'),'history'),text('git:error',state.error));
21
+ if(state.loading)children.push(node('p','git:loading',t('external.loadingRepositories'),[],{role:'status'}));
22
+ if(!repositories.length&&!state.loading)children.push(text('git:no-repositories',t('external.noRepositories')));
18
23
  const group=repo=>{
19
24
  const prefix='git:repository:'+repo.id;
20
25
  const groupAct=(...args)=>{const result=act(...args);return result?{...result,key:prefix+':'+result.key}:null;};
21
26
  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?' · 仓库根目录位于工作区外':''))];
27
+ const content=[groupAct('toggleRepository',`${repo.name} · ${repo.changes?.branch??t('git.detachedHead')} · ${files.length}`,repo.id),text(prefix+':path',repo.relativePath+(repo.kind==='submodule'?t('git.submoduleSuffix'):repo.kind==='worktree'?t('git.worktreeSuffix'):'')+(repo.externalRoot?t('git.externalRootSuffix'):''))];
23
28
  if(!(state.collapsedRepositories??[]).includes(repo.id)){
24
29
  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','更改']]){
30
+ if(!repo.changes&&!repo.error)content.push(text(prefix+':loading',t('app.loadingChanges')));
31
+ for(const [scope,title] of [['staged',t('external.stagedChanges')],['unstaged',t('git.changed')]]){
27
32
  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)]))));
33
+ 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?'!':kindLabel(file.kind)} ${file.path}`),groupAct('repositoryDiff',t('semantic.inspect'),repo.id,file.path,scope),groupAct(scope==='staged'?'repositoryUnstage':'repositoryStage',scope==='staged'?t('git.unstage'):t('git.stage'),repo.id,file.path)]))));
29
34
  }
30
- content.push(groupAct('repositoryStageAll','全部暂存',repo.id),groupAct('repositoryUnstageAll','全部取消暂存',repo.id),groupAct('selectRepository',files.some(file=>file.staged)?'提交…':'打开仓库',repo.id));
35
+ content.push(groupAct('repositoryStageAll',t('git.stageAll'),repo.id),groupAct('repositoryUnstageAll',t('git.unstageEverything'),repo.id),groupAct('selectRepository',files.some(file=>file.staged)?t('git.commitEllipsis'):t('git.openRepository'),repo.id));
31
36
  }
32
- return node('section',prefix,null,content,{'aria-label':`仓库 ${repo.name} ${repo.relativePath}`});
37
+ return node('section',prefix,null,content,{'aria-label':t('external.repositoryLabel',{name:repo.name,path:repo.relativePath})});
33
38
  };
34
39
  const clean=repositories.filter(repo=>!repo.error&&repo.changes?.captureStatus==='captured'&&!repo.changes.files.length);
35
40
  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)]));
41
+ if(clean.length)children.push(node('details','git:clean-repositories',null,[node('summary','git:clean-repositories-title',t('external.cleanRepositories',{count:clean.length})),...clean.map(group)]));
37
42
  }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'}));
43
+ const single=[node('nav','git:tools',null,[act('refresh',t('app.refreshChanges')),act('refreshMetadata',t('external.refreshHistory')),act('requestReview',t('external.requestReview')),act('selectSection',t('app.workspaceChanges'),'changes'),act('selectSection',t('git.history'),'history')]),text('git:branch',state.changes?.branch),text('git:error',state.error),text('git:metadata-error',state.metadataError)];
44
+ if(state.changes?.captureStatus&&state.changes.captureStatus!=='captured')single.push(node('p','git:capture-error',state.changes.captureError??t('external.gitStateUnavailable'),[],{role:'status'}));
45
+ if(state.changes?.captureStatus==='captured'&&!state.changes.files.length)single.push(text('git:clean',t('external.cleanWorkspace')));
46
+ if(state.loading||state.metadataLoading)single.push(node('p','git:loading',t('external.loadingGit'),[],{role:'status'}));
42
47
  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','提交'));
48
+ 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,`${kindLabel(file.kind)}${file.conflicted?t('external.conflictSuffix'):''}${file.staged?t('external.stagedSuffix'):''}${file.untracked?t('external.untrackedSuffix'):''}`),act('openDiff',t('external.viewStagedDiff'),file.path,'staged'),act('openDiff',t('external.viewWorkingDiff'),file.path,'unstaged'),act('stageFile',t('git.stage'),file.path),act('unstageFile',t('git.unstage'),file.path)]))));
49
+ single.push(act('stageAll',t('git.stageAll')),act('unstageAll',t('git.unstageEverything')),field('git:commit-message',current?t('external.commitTarget',{name:current.name,branch:state.changes?.branch??t('git.detachedHead')}):t('external.commitMessage'),state.draft.commitMessage,find('commitMessage'),true),act('commit',t('execution.operation.git.commit')));
45
50
  }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}`)]))));
51
+ 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} · ${formatDateTime(locale,commit.authoredAt,{dateStyle:'medium',timeStyle:'short'})}`)]))));
47
52
  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));
53
+ single.push(state.historyLoadingMore?text('git:history-more-loading',t('git.loadingMoreCommits')):state.historyHasMore?act('loadMoreHistory',t('git.loadMoreCommits')):null);
54
+ 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',t('external.loadMoreFiles'),state.commitFiles.commit));
50
55
  }
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)]))]));
56
+ single.push(node('details','git:branches',null,[node('summary','git:branches-title',t('external.branches')),field('git:branch-draft',t('git.newBranch'),state.draft.branchDraft,find('branchDraft')),act('createBranch',t('execution.operation.git.create-branch')),...state.branches.map(branch=>node('p','git:branch:'+branch.name,null,[text('git:branch-name:'+branch.name,branch.name+(branch.current?t('external.currentSuffix'):'')),act('checkoutBranch',t('execution.operation.git.checkout'),branch.name)]))]));
57
+ if(state.remoteStatus)single.push(section('git:remote',t('external.remote'),[text('git:upstream',state.remoteStatus.upstream),text('git:distance',t('external.remoteDistance',{ahead:state.remoteStatus.ahead,behind:state.remoteStatus.behind})),act('fetch',t('external.fetch')),act('pull',t('git.pull')),act('push',t('git.push'))]));
58
+ single.push(node('details','git:stashes',null,[node('summary','git:stash-title',t('git.stash')),act('saveStash',t('external.saveStash')),...state.stashes.map(stash=>node('p','git:stash:'+stash.reference,null,[text('git:stash-message:'+stash.reference,stash.message),act('applyStash',t('external.applyStash',{reference:stash.reference}),stash.reference)]))]));
54
59
  children.push(...single);
55
60
  }
56
61
  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]));
62
+ if(preview.selectedPath||preview.fileDiff||preview.error||preview.loading)children.push(section('git:preview',preview.contextLabel??preview.selectedPath??t('external.diffPreview'),[act('closeDiff',t('external.closeDiff')),preview.loading?text('git:preview-loading',t('external.loadingDiff')):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',t('external.diffTruncated')):null]));
58
63
  return section('git','Git',children);
59
64
  }