diff --git a/.changeset/agent-card-fgbg-badge.md b/.changeset/agent-card-fgbg-badge.md new file mode 100644 index 0000000000..7cb8e2a23d --- /dev/null +++ b/.changeset/agent-card-fgbg-badge.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Label inline subagent cards in the message stream with their foreground or background mode. diff --git a/.changeset/copy-server-info.md b/.changeset/copy-server-info.md new file mode 100644 index 0000000000..c07b85beb5 --- /dev/null +++ b/.changeset/copy-server-info.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Add copy buttons next to the server version and server address in settings. diff --git a/.changeset/fix-empty-workspace-group-legacy-sidebar.md b/.changeset/fix-empty-workspace-group-legacy-sidebar.md new file mode 100644 index 0000000000..6e1a31912f --- /dev/null +++ b/.changeset/fix-empty-workspace-group-legacy-sidebar.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Keep empty workspace groups visible in the legacy sidebar after their last session is archived. diff --git a/.changeset/fix-menu-tooltip-stuck.md b/.changeset/fix-menu-tooltip-stuck.md new file mode 100644 index 0000000000..e52dbea0e8 --- /dev/null +++ b/.changeset/fix-menu-tooltip-stuck.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Hide button hover tooltips outside a menu while the menu is open. diff --git a/.changeset/fix-model-menu-viewport.md b/.changeset/fix-model-menu-viewport.md new file mode 100644 index 0000000000..c73955c7cb --- /dev/null +++ b/.changeset/fix-model-menu-viewport.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Keep the model picker menu on the workspace home within the viewport. diff --git a/.changeset/fix-search-dialog-workspaces-label.md b/.changeset/fix-search-dialog-workspaces-label.md new file mode 100644 index 0000000000..f3c22ddc90 --- /dev/null +++ b/.changeset/fix-search-dialog-workspaces-label.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Fix the workspace group title showing untranslated text in the search dialog. diff --git a/.changeset/fix-settings-select-teleport.md b/.changeset/fix-settings-select-teleport.md new file mode 100644 index 0000000000..3d3a855ca9 --- /dev/null +++ b/.changeset/fix-settings-select-teleport.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Fix the settings dialog dropdown list being clipped by the scroll area, and lock the content behind it while the list is open. diff --git a/.changeset/fix-slash-mention-menu-viewport.md b/.changeset/fix-slash-mention-menu-viewport.md new file mode 100644 index 0000000000..a0747af7d0 --- /dev/null +++ b/.changeset/fix-slash-mention-menu-viewport.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Keep the slash command and @ mention panels on the workspace home within the viewport. diff --git a/.changeset/fix-user-menu-text-select.md b/.changeset/fix-user-menu-text-select.md new file mode 100644 index 0000000000..7cf40aee56 --- /dev/null +++ b/.changeset/fix-user-menu-text-select.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Prevent text selection in the sidebar user menu and its plan usage submenu. diff --git a/.changeset/grouped-session-list-load.md b/.changeset/grouped-session-list-load.md new file mode 100644 index 0000000000..f81e586c60 --- /dev/null +++ b/.changeset/grouped-session-list-load.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Fix slow session list loading when there are many workspaces. diff --git a/.changeset/login-wake-and-waiting-page.md b/.changeset/login-wake-and-waiting-page.md new file mode 100644 index 0000000000..6644811ee8 --- /dev/null +++ b/.changeset/login-wake-and-waiting-page.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Auto-open the browser authorization page after choosing a login region, redesign the authorization waiting page, and refresh the login state as soon as the window regains focus instead of waiting for the poll. diff --git a/.changeset/mention-menu-fs-suggest.md b/.changeset/mention-menu-fs-suggest.md new file mode 100644 index 0000000000..1bd6d1ca86 --- /dev/null +++ b/.changeset/mention-menu-fs-suggest.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Upgrade the @ mention menu: file and skill candidates are merged and ranked by match quality, file search is faster, with path-fragment matching and hit highlighting. diff --git a/.changeset/menu-radius-concentric.md b/.changeset/menu-radius-concentric.md new file mode 100644 index 0000000000..a598eadf55 --- /dev/null +++ b/.changeset/menu-radius-concentric.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Round menu items concentric with their menu frames. diff --git a/.changeset/pin-session-from-chat-header.md b/.changeset/pin-session-from-chat-header.md new file mode 100644 index 0000000000..fb5bbbe542 --- /dev/null +++ b/.changeset/pin-session-from-chat-header.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Add a Pin action to the chat header more-menu to pin the current session to the sidebar pinned section. diff --git a/.changeset/pinned-section-resize.md b/.changeset/pinned-section-resize.md new file mode 100644 index 0000000000..7c8d8aae36 --- /dev/null +++ b/.changeset/pinned-section-resize.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Allow dragging the divider between the pinned section and the session list to resize both areas, with fade hints at the edges when the pinned section scrolls. diff --git a/.changeset/queue-interaction.md b/.changeset/queue-interaction.md new file mode 100644 index 0000000000..f1026bb0bd --- /dev/null +++ b/.changeset/queue-interaction.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Improve the prompt queue interaction, with per-row steer and send. diff --git a/.changeset/region-login-entries.md b/.changeset/region-login-entries.md new file mode 100644 index 0000000000..db9fc72bee --- /dev/null +++ b/.changeset/region-login-entries.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Add kimi.com and kimi.ai OAuth login entries, and switch update and help links to the site matching the current login. diff --git a/.changeset/remote-session-archive-sync.md b/.changeset/remote-session-archive-sync.md new file mode 100644 index 0000000000..15a6726b86 --- /dev/null +++ b/.changeset/remote-session-archive-sync.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Remove sessions archived from another client from the session list immediately, without a manual refresh. diff --git a/.changeset/session-menu-last-active.md b/.changeset/session-menu-last-active.md new file mode 100644 index 0000000000..1a3e8f1f3c --- /dev/null +++ b/.changeset/session-menu-last-active.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Label the timestamp at the bottom of the session menu as last active and tighten that row's padding. diff --git a/.changeset/sidebar-action-button-alignment.md b/.changeset/sidebar-action-button-alignment.md new file mode 100644 index 0000000000..984cb5a9ba --- /dev/null +++ b/.changeset/sidebar-action-button-alignment.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Fix misaligned action buttons between the sidebar section headers and the session rows. diff --git a/.changeset/skill-activation-card.md b/.changeset/skill-activation-card.md new file mode 100644 index 0000000000..78cedd46db --- /dev/null +++ b/.changeset/skill-activation-card.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Remove the skill-activated card from skill activation messages. diff --git a/.changeset/skill-turn-undo.md b/.changeset/skill-turn-undo.md new file mode 100644 index 0000000000..09556731c2 --- /dev/null +++ b/.changeset/skill-turn-undo.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Make skill-activation turns undoable so they can be withdrawn and resent. diff --git a/.changeset/user-menu-density.md b/.changeset/user-menu-density.md new file mode 100644 index 0000000000..c948592f7c --- /dev/null +++ b/.changeset/user-menu-density.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Tighten the row height and spacing of the account menu and its submenus to match the standard menu density. diff --git a/.changeset/waitfor-tool-card.md b/.changeset/waitfor-tool-card.md new file mode 100644 index 0000000000..e16b80625a --- /dev/null +++ b/.changeset/waitfor-tool-card.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Give WaitFor tool calls a dedicated quiet-line display showing completed tasks, wait timeouts, and how many tasks are still running. diff --git a/apps/kimi-code/dist-web/assets/CodeBlockNode-EUGAgT_h.js b/apps/kimi-code/dist-web/assets/CodeBlockNode-h5qcRgnj.js similarity index 99% rename from apps/kimi-code/dist-web/assets/CodeBlockNode-EUGAgT_h.js rename to apps/kimi-code/dist-web/assets/CodeBlockNode-h5qcRgnj.js index 57790a8221..ab0a42de7d 100644 --- a/apps/kimi-code/dist-web/assets/CodeBlockNode-EUGAgT_h.js +++ b/apps/kimi-code/dist-web/assets/CodeBlockNode-h5qcRgnj.js @@ -1,5 +1,5 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index-DXophRCW.js","assets/index-yKYHPeXU.js","assets/index-DBtc5buz.css"])))=>i.map(i=>d[i]); -import{bR as xi,cb as Si,bQ as Ho,M as pl,bl as Ci,af as fl,bY as Mi,cc as Bi,b$ as hl,aU as $,c0 as Ei,c1 as Fi,c2 as Pi,a0 as Mo,aD as No,bE as ue,az as Li,cd as wn,c8 as mt,aI as To,aL as Y,s as Sn,aw as Vt,au as pt,bk as J,ce as Bo,u as ie,I as Ro,A as Oi,bJ as Gt,aY as ht,bL as vl,v as w,t as we,bB as ml,bb as Be,q as b,b7 as $i,cf as zi,cg as bn,ch as Hi,ci as Ni,cj as Ti,ck as Ri,as as A,cl as Eo,cm as Di,cn as Ai,co as Wt,cp as Fo,bO as Do,T as ji,G as qi,F as Po,g as Wi,c7 as _i,b_ as Ii}from"./index-yKYHPeXU.js";import{i as fe,t as kn}from"./safeRaf-DGuzXxDK.js";var xn=(k,M,y)=>new Promise((te,ne)=>{var be=P=>{try{r(y.next(P))}catch(H){ne(H)}},ke=P=>{try{r(y.throw(P))}catch(H){ne(H)}},r=P=>P.done?te(P.value):Promise.resolve(P.value).then(be,ke);r((y=y.apply(k,M)).next())});let Lo=!1,_t=null,It=null,Ut=null;function Ui(){return xn(this,null,function*(){if(Ut)return Ut;Ut=xn(null,null,function*(){if(!It)try{if(It=(function(k){const M=k;if(typeof M?.useMonaco=="function")return M;const y=k?.default;return typeof y?.useMonaco=="function"?y:null})(yield xi(()=>import("./index-DXophRCW.js"),__vite__mapDeps([0,1,2]))),!It)return null}catch{return null}try{return yield(function(k){return xn(this,null,function*(){return Lo?void 0:_t||(_t=xn(null,null,function*(){const y=globalThis?.MonacoEnvironment;y&&(typeof y.getWorker=="function"||typeof y.getWorkerUrl=="function")||typeof k?.preloadMonacoWorkers!="function"||(yield k.preloadMonacoWorkers()),Lo=!0}).finally(()=>{_t=null}),_t)})})(It),Si(),It}catch{return null}});try{return yield Ut}finally{Ut=null}})}var Vi=Object.defineProperty,Gi=Object.defineProperties,Ji=Object.getOwnPropertyDescriptors,Oo=Object.getOwnPropertySymbols,Yi=Object.prototype.hasOwnProperty,Qi=Object.prototype.propertyIsEnumerable,$o=(k,M,y)=>M in k?Vi(k,M,{enumerable:!0,configurable:!0,writable:!0,value:y}):k[M]=y,U=(k,M)=>{for(var y in M||(M={}))Yi.call(M,y)&&$o(k,y,M[y]);if(Oo)for(var y of Oo(M))Qi.call(M,y)&&$o(k,y,M[y]);return k},Me=(k,M)=>Gi(k,Ji(M)),j=(k,M,y)=>new Promise((te,ne)=>{var be=P=>{try{r(y.next(P))}catch(H){ne(H)}},ke=P=>{try{r(y.throw(P))}catch(H){ne(H)}},r=P=>P.done?te(P.value):Promise.resolve(P.value).then(be,ke);r((y=y.apply(k,M)).next())});const Xi={key:0,class:"code-block-header flex justify-between items-center border-b px-[var(--ms-inset-panel-x)] py-[var(--ms-inset-panel-y)] border-[var(--code-border)] bg-[var(--code-header-bg)] text-[var(--code-fg)]"},Ki={class:"flex items-center gap-0.5"},Zi=["aria-label"],er={class:"code-diff-stat removed"},tr={class:"code-diff-stat added"},nr=["aria-label"],lr={key:0,xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",width:"1em",height:"1em",viewBox:"0 0 24 24",class:"action-icon"},or={key:1,xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",width:"1em",height:"1em",viewBox:"0 0 24 24",class:"action-icon"},ir=["aria-pressed"],rr={key:3,class:"relative"},ar=["aria-expanded"],ur=["disabled"],sr=["disabled"],dr=["disabled"],cr={key:0,xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",width:"1em",height:"1em",viewBox:"0 0 24 24",class:"action-icon"},fr={key:1,xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",width:"1em",height:"1em",viewBox:"0 0 24 24",class:"action-icon"},vr={class:"code-loading-placeholder"},mr={class:"sr-only","aria-live":"polite",role:"status"},pr=pl({__name:"CodeBlockShell",props:{showHeader:{type:Boolean,default:!0},showCollapseButton:{type:Boolean,default:!0},showFontSizeButtons:{type:Boolean,default:!0},enableFontSizeControl:{type:Boolean,default:!0},showCopyButton:{type:Boolean,default:!0},showExpandButton:{type:Boolean,default:!0},showPreviewButton:{type:Boolean,default:!0},showTooltips:{type:Boolean,default:!0},isDark:{type:Boolean,default:!1},loading:{type:Boolean,default:!1},stream:{type:Boolean,default:!1},isCollapsed:{type:Boolean,default:!1},isExpanded:{type:Boolean,default:!1},copyText:{type:Boolean,default:!1},isPreviewable:{type:Boolean,default:!1},codeFontSize:{},codeFontMin:{},codeFontMax:{},defaultCodeFontSize:{},fontBaselineReady:{type:Boolean,default:!1},diffStats:{},diffStatsAriaLabel:{}},emits:["toggleCollapse","decreaseFont","resetFont","increaseFont","copy","toggleExpand","preview"],setup(k,{emit:M}){const y=k,te=M,ne=$(!1),be=$(null),ke=$(null);function r(){mt(!0),ne.value=!ne.value,ne.value&&document.addEventListener("click",H,{once:!0,capture:!0})}function P(){mt(!0),ne.value=!1}function H(Q){var v,z;const wt=Q.target;(v=be.value)!=null&&v.contains(wt)||(z=ke.value)!=null&&z.contains(wt)?document.addEventListener("click",H,{once:!0,capture:!0}):P()}const re=b(()=>y.showFontSizeButtons&&y.enableFontSizeControl||y.showExpandButton||y.isPreviewable&&y.showPreviewButton),{t:T}=hl(),gt=b(()=>y.showTooltips!==!1);function Ge(Q,v){gt.value&&_i(Q.currentTarget,v,"top",!1,void 0,y.isDark)}function Ee(){gt.value&&mt()}function yt(Q){Ge(Q,y.copyText?T("common.copied")||"Copied":T("common.copy")||"Copy")}const gl=b(()=>{var Q,v;return!!Number.isFinite(y.codeFontSize)&&((Q=y.codeFontSize)!=null?Q:0)<=((v=y.codeFontMin)!=null?v:0)}),Jt=b(()=>!y.fontBaselineReady||y.codeFontSize===y.defaultCodeFontSize),Yt=b(()=>{var Q,v;return!!Number.isFinite(y.codeFontSize)&&((Q=y.codeFontSize)!=null?Q:0)>=((v=y.codeFontMax)!=null?v:100)});return(Q,v)=>(Y(),ie(Po,null,[y.showHeader?(Y(),ie("div",Xi,[ht(Q.$slots,"header-left"),ht(Q.$slots,"header-right",{},()=>[w("div",Ki,[k.diffStats?(Y(),ie("div",{key:0,class:"code-diff-stats","aria-label":k.diffStatsAriaLabel},[w("span",er,"-"+Be(k.diffStats.removed),1),w("span",tr,"+"+Be(k.diffStats.added),1)],8,Zi)):we("",!0),y.showCopyButton?(Y(),ie("button",{key:1,type:"button",class:"code-action-btn inline-flex items-center justify-center p-[var(--ms-action-btn-padding)] rounded leading-none shrink-0 cursor-pointer text-[var(--code-action-fg)] hover:bg-[var(--code-action-hover-bg)] hover:text-[var(--code-action-hover-fg)] active:scale-[0.96] disabled:opacity-40 disabled:cursor-not-allowed transition-colors","aria-label":k.copyText?J(T)("common.copied")||"Copied":J(T)("common.copy")||"Copy",onClick:v[0]||(v[0]=z=>te("copy")),onMouseenter:v[1]||(v[1]=z=>yt(z)),onFocus:v[2]||(v[2]=z=>yt(z)),onMouseleave:Ee,onBlur:Ee},[k.copyText?(Y(),ie("svg",or,[...v[14]||(v[14]=[w("path",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M20 6L9 17l-5-5"},null,-1)])])):(Y(),ie("svg",lr,[...v[13]||(v[13]=[w("g",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2"},[w("rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}),w("path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"})],-1)])]))],40,nr)):we("",!0),y.showCollapseButton?(Y(),ie("button",{key:2,type:"button",class:"code-action-btn inline-flex items-center justify-center p-[var(--ms-action-btn-padding)] rounded leading-none shrink-0 cursor-pointer text-[var(--code-action-fg)] hover:bg-[var(--code-action-hover-bg)] hover:text-[var(--code-action-hover-fg)] active:scale-[0.96] disabled:opacity-40 disabled:cursor-not-allowed transition-colors","aria-pressed":k.isCollapsed,onClick:v[3]||(v[3]=z=>te("toggleCollapse")),onMouseenter:v[4]||(v[4]=z=>Ge(z,k.isCollapsed?J(T)("common.expand")||"Expand":J(T)("common.collapse")||"Collapse")),onFocus:v[5]||(v[5]=z=>Ge(z,k.isCollapsed?J(T)("common.expand")||"Expand":J(T)("common.collapse")||"Collapse")),onMouseleave:Ee,onBlur:Ee},[(Y(),ie("svg",{style:Vt({rotate:k.isCollapsed?"0deg":"90deg"}),xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",width:"1em",height:"1em",viewBox:"0 0 24 24",class:"action-icon"},[...v[15]||(v[15]=[w("path",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"m9 18l6-6l-6-6"},null,-1)])],4))],40,ir)):we("",!0),re.value?(Y(),ie("div",rr,[w("button",{ref_key:"moreBtnRef",ref:ke,type:"button",class:"code-action-btn inline-flex items-center justify-center p-[var(--ms-action-btn-padding)] rounded leading-none shrink-0 cursor-pointer text-[var(--code-action-fg)] hover:bg-[var(--code-action-hover-bg)] hover:text-[var(--code-action-hover-fg)] active:scale-[0.96] transition-colors","aria-expanded":ne.value,"aria-haspopup":"true",onClick:Do(r,["stop"]),onMouseenter:v[6]||(v[6]=z=>Ge(z,J(T)("common.more")||"More")),onFocus:v[7]||(v[7]=z=>Ge(z,J(T)("common.more")||"More")),onMouseleave:Ee,onBlur:Ee},[...v[16]||(v[16]=[qi('',1)])],40,ar),Ro(Wi,{name:"code-menu"},{default:Gt(()=>[ne.value?(Y(),ie("div",{key:0,ref_key:"moreMenuRef",ref:be,class:"code-more-menu min-w-[10rem] p-1 bg-[hsl(var(--ms-popover))] text-[hsl(var(--ms-popover-foreground))] border border-[var(--code-border)] shadow-[var(--ms-shadow-popover)]",role:"menu"},[y.showFontSizeButtons&&y.enableFontSizeControl?(Y(),ie(Po,{key:0},[w("button",{type:"button",role:"menuitem",class:"flex items-center gap-2 w-full py-1.5 px-2 rounded text-xs text-[var(--code-action-fg)] cursor-pointer whitespace-nowrap hover:bg-[var(--code-action-hover-bg)] hover:text-[var(--code-action-hover-fg)] disabled:opacity-40 disabled:cursor-not-allowed transition-colors",disabled:gl.value,onClick:v[8]||(v[8]=z=>{J(mt)(!0),te("decreaseFont")})},[v[17]||(v[17]=w("svg",{xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",width:"1em",height:"1em",viewBox:"0 0 24 24",class:"action-icon"},[w("path",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M5 12h14"})],-1)),w("span",null,Be(J(T)("common.fontSmaller")||"Font size −"),1)],8,ur),w("button",{type:"button",role:"menuitem",class:"flex items-center gap-2 w-full py-1.5 px-2 rounded text-xs text-[var(--code-action-fg)] cursor-pointer whitespace-nowrap hover:bg-[var(--code-action-hover-bg)] hover:text-[var(--code-action-hover-fg)] disabled:opacity-40 disabled:cursor-not-allowed transition-colors",disabled:Jt.value,onClick:v[9]||(v[9]=z=>{J(mt)(!0),te("resetFont")})},[v[18]||(v[18]=w("svg",{xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",width:"1em",height:"1em",viewBox:"0 0 24 24",class:"action-icon"},[w("g",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2"},[w("path",{d:"M3 12a9 9 0 1 0 9-9a9.75 9.75 0 0 0-6.74 2.74L3 8"}),w("path",{d:"M3 3v5h5"})])],-1)),w("span",null,Be(J(T)("common.fontReset")||"Font size reset"),1)],8,sr),w("button",{type:"button",role:"menuitem",class:"flex items-center gap-2 w-full py-1.5 px-2 rounded text-xs text-[var(--code-action-fg)] cursor-pointer whitespace-nowrap hover:bg-[var(--code-action-hover-bg)] hover:text-[var(--code-action-hover-fg)] disabled:opacity-40 disabled:cursor-not-allowed transition-colors",disabled:Yt.value,onClick:v[10]||(v[10]=z=>{J(mt)(!0),te("increaseFont")})},[v[19]||(v[19]=w("svg",{xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",width:"1em",height:"1em",viewBox:"0 0 24 24",class:"action-icon"},[w("path",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M5 12h14m-7-7v14"})],-1)),w("span",null,Be(J(T)("common.fontLarger")||"Font size +"),1)],8,dr)],64)):we("",!0),y.showExpandButton?(Y(),ie("button",{key:1,type:"button",role:"menuitem",class:"flex items-center gap-2 w-full py-1.5 px-2 rounded text-xs text-[var(--code-action-fg)] cursor-pointer whitespace-nowrap hover:bg-[var(--code-action-hover-bg)] hover:text-[var(--code-action-hover-fg)] transition-colors",onClick:v[11]||(v[11]=z=>{P(),te("toggleExpand")})},[k.isExpanded?(Y(),ie("svg",cr,[...v[20]||(v[20]=[w("path",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"m14 10l7-7m-1 7h-6V4M3 21l7-7m-6 0h6v6"},null,-1)])])):(Y(),ie("svg",fr,[...v[21]||(v[21]=[w("path",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M15 3h6v6m0-6l-7 7M3 21l7-7m-1 7H3v-6"},null,-1)])])),w("span",null,Be(k.isExpanded?J(T)("common.collapse")||"Collapse":J(T)("common.expand")||"Expand"),1)])):we("",!0),k.isPreviewable&&y.showPreviewButton?(Y(),ie("button",{key:2,type:"button",role:"menuitem",class:"flex items-center gap-2 w-full py-1.5 px-2 rounded text-xs text-[var(--code-action-fg)] cursor-pointer whitespace-nowrap hover:bg-[var(--code-action-hover-bg)] hover:text-[var(--code-action-hover-fg)] transition-colors",onClick:v[12]||(v[12]=z=>{P(),te("preview")})},[v[22]||(v[22]=w("svg",{xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",width:"1em",height:"1em",viewBox:"0 0 24 24",class:"action-icon"},[w("g",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2"},[w("path",{d:"M2.062 12.348a1 1 0 0 1 0-.696a10.75 10.75 0 0 1 19.876 0a1 1 0 0 1 0 .696a10.75 10.75 0 0 1-19.876 0"}),w("circle",{cx:"12",cy:"12",r:"3"})])],-1)),w("span",null,Be(J(T)("common.preview")||"Preview"),1)])):we("",!0)],512)):we("",!0)]),_:1})])):we("",!0)])])])):we("",!0),vl(w("div",{class:pt(["code-block-shell-content",{"code-block-shell-content--collapsed":k.isCollapsed}])},[ht(Q.$slots,"default")],2),[[ml,!!k.stream||!k.loading]]),vl(w("div",vr,[ht(Q.$slots,"loading",{},()=>[v[23]||(v[23]=w("div",{class:"loading-skeleton"},[w("div",{class:"skeleton-line"}),w("div",{class:"skeleton-line"}),w("div",{class:"skeleton-line short"})],-1))])],512),[[ml,!k.stream&&k.loading]]),w("span",mr,Be(k.copyText?J(T)("common.copied")||"Copied":""),1)],64))}}),hr={class:"html-preview-frame__header"},gr={class:"html-preview-frame__title"},yr={class:"html-preview-frame__label"},wr=["sandbox","srcdoc"],br=Ho(pl({__name:"HtmlPreviewFrame",props:{code:{},isDark:{type:Boolean},htmlPreviewAllowScripts:{type:Boolean},htmlPreviewSandbox:{},onClose:{type:Function},title:{}},setup(k){const M=k,y=import.meta!==void 0&&!1;let te=null;const{t:ne}=hl(),be=b(()=>{const P=M.code||"",H=P.trim().toLowerCase();return H.startsWith(" +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index-mCncasKK.js","assets/index-BkUUBejk.js","assets/index-SEzwqHoC.css"])))=>i.map(i=>d[i]); +import{bR as xi,cb as Si,bQ as Ho,M as pl,bl as Ci,af as fl,bY as Mi,cc as Bi,b$ as hl,aU as $,c0 as Ei,c1 as Fi,c2 as Pi,a0 as Mo,aD as No,bE as ue,az as Li,cd as wn,c8 as mt,aI as To,aL as Y,s as Sn,aw as Vt,au as pt,bk as J,ce as Bo,u as ie,I as Ro,A as Oi,bJ as Gt,aY as ht,bL as vl,v as w,t as we,bB as ml,bb as Be,q as b,b7 as $i,cf as zi,cg as bn,ch as Hi,ci as Ni,cj as Ti,ck as Ri,as as A,cl as Eo,cm as Di,cn as Ai,co as Wt,cp as Fo,bO as Do,T as ji,G as qi,F as Po,g as Wi,c7 as _i,b_ as Ii}from"./index-BkUUBejk.js";import{i as fe,t as kn}from"./safeRaf-DGuzXxDK.js";var xn=(k,M,y)=>new Promise((te,ne)=>{var be=P=>{try{r(y.next(P))}catch(H){ne(H)}},ke=P=>{try{r(y.throw(P))}catch(H){ne(H)}},r=P=>P.done?te(P.value):Promise.resolve(P.value).then(be,ke);r((y=y.apply(k,M)).next())});let Lo=!1,_t=null,It=null,Ut=null;function Ui(){return xn(this,null,function*(){if(Ut)return Ut;Ut=xn(null,null,function*(){if(!It)try{if(It=(function(k){const M=k;if(typeof M?.useMonaco=="function")return M;const y=k?.default;return typeof y?.useMonaco=="function"?y:null})(yield xi(()=>import("./index-mCncasKK.js"),__vite__mapDeps([0,1,2]))),!It)return null}catch{return null}try{return yield(function(k){return xn(this,null,function*(){return Lo?void 0:_t||(_t=xn(null,null,function*(){const y=globalThis?.MonacoEnvironment;y&&(typeof y.getWorker=="function"||typeof y.getWorkerUrl=="function")||typeof k?.preloadMonacoWorkers!="function"||(yield k.preloadMonacoWorkers()),Lo=!0}).finally(()=>{_t=null}),_t)})})(It),Si(),It}catch{return null}});try{return yield Ut}finally{Ut=null}})}var Vi=Object.defineProperty,Gi=Object.defineProperties,Ji=Object.getOwnPropertyDescriptors,Oo=Object.getOwnPropertySymbols,Yi=Object.prototype.hasOwnProperty,Qi=Object.prototype.propertyIsEnumerable,$o=(k,M,y)=>M in k?Vi(k,M,{enumerable:!0,configurable:!0,writable:!0,value:y}):k[M]=y,U=(k,M)=>{for(var y in M||(M={}))Yi.call(M,y)&&$o(k,y,M[y]);if(Oo)for(var y of Oo(M))Qi.call(M,y)&&$o(k,y,M[y]);return k},Me=(k,M)=>Gi(k,Ji(M)),j=(k,M,y)=>new Promise((te,ne)=>{var be=P=>{try{r(y.next(P))}catch(H){ne(H)}},ke=P=>{try{r(y.throw(P))}catch(H){ne(H)}},r=P=>P.done?te(P.value):Promise.resolve(P.value).then(be,ke);r((y=y.apply(k,M)).next())});const Xi={key:0,class:"code-block-header flex justify-between items-center border-b px-[var(--ms-inset-panel-x)] py-[var(--ms-inset-panel-y)] border-[var(--code-border)] bg-[var(--code-header-bg)] text-[var(--code-fg)]"},Ki={class:"flex items-center gap-0.5"},Zi=["aria-label"],er={class:"code-diff-stat removed"},tr={class:"code-diff-stat added"},nr=["aria-label"],lr={key:0,xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",width:"1em",height:"1em",viewBox:"0 0 24 24",class:"action-icon"},or={key:1,xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",width:"1em",height:"1em",viewBox:"0 0 24 24",class:"action-icon"},ir=["aria-pressed"],rr={key:3,class:"relative"},ar=["aria-expanded"],ur=["disabled"],sr=["disabled"],dr=["disabled"],cr={key:0,xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",width:"1em",height:"1em",viewBox:"0 0 24 24",class:"action-icon"},fr={key:1,xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",width:"1em",height:"1em",viewBox:"0 0 24 24",class:"action-icon"},vr={class:"code-loading-placeholder"},mr={class:"sr-only","aria-live":"polite",role:"status"},pr=pl({__name:"CodeBlockShell",props:{showHeader:{type:Boolean,default:!0},showCollapseButton:{type:Boolean,default:!0},showFontSizeButtons:{type:Boolean,default:!0},enableFontSizeControl:{type:Boolean,default:!0},showCopyButton:{type:Boolean,default:!0},showExpandButton:{type:Boolean,default:!0},showPreviewButton:{type:Boolean,default:!0},showTooltips:{type:Boolean,default:!0},isDark:{type:Boolean,default:!1},loading:{type:Boolean,default:!1},stream:{type:Boolean,default:!1},isCollapsed:{type:Boolean,default:!1},isExpanded:{type:Boolean,default:!1},copyText:{type:Boolean,default:!1},isPreviewable:{type:Boolean,default:!1},codeFontSize:{},codeFontMin:{},codeFontMax:{},defaultCodeFontSize:{},fontBaselineReady:{type:Boolean,default:!1},diffStats:{},diffStatsAriaLabel:{}},emits:["toggleCollapse","decreaseFont","resetFont","increaseFont","copy","toggleExpand","preview"],setup(k,{emit:M}){const y=k,te=M,ne=$(!1),be=$(null),ke=$(null);function r(){mt(!0),ne.value=!ne.value,ne.value&&document.addEventListener("click",H,{once:!0,capture:!0})}function P(){mt(!0),ne.value=!1}function H(Q){var v,z;const wt=Q.target;(v=be.value)!=null&&v.contains(wt)||(z=ke.value)!=null&&z.contains(wt)?document.addEventListener("click",H,{once:!0,capture:!0}):P()}const re=b(()=>y.showFontSizeButtons&&y.enableFontSizeControl||y.showExpandButton||y.isPreviewable&&y.showPreviewButton),{t:T}=hl(),gt=b(()=>y.showTooltips!==!1);function Ge(Q,v){gt.value&&_i(Q.currentTarget,v,"top",!1,void 0,y.isDark)}function Ee(){gt.value&&mt()}function yt(Q){Ge(Q,y.copyText?T("common.copied")||"Copied":T("common.copy")||"Copy")}const gl=b(()=>{var Q,v;return!!Number.isFinite(y.codeFontSize)&&((Q=y.codeFontSize)!=null?Q:0)<=((v=y.codeFontMin)!=null?v:0)}),Jt=b(()=>!y.fontBaselineReady||y.codeFontSize===y.defaultCodeFontSize),Yt=b(()=>{var Q,v;return!!Number.isFinite(y.codeFontSize)&&((Q=y.codeFontSize)!=null?Q:0)>=((v=y.codeFontMax)!=null?v:100)});return(Q,v)=>(Y(),ie(Po,null,[y.showHeader?(Y(),ie("div",Xi,[ht(Q.$slots,"header-left"),ht(Q.$slots,"header-right",{},()=>[w("div",Ki,[k.diffStats?(Y(),ie("div",{key:0,class:"code-diff-stats","aria-label":k.diffStatsAriaLabel},[w("span",er,"-"+Be(k.diffStats.removed),1),w("span",tr,"+"+Be(k.diffStats.added),1)],8,Zi)):we("",!0),y.showCopyButton?(Y(),ie("button",{key:1,type:"button",class:"code-action-btn inline-flex items-center justify-center p-[var(--ms-action-btn-padding)] rounded leading-none shrink-0 cursor-pointer text-[var(--code-action-fg)] hover:bg-[var(--code-action-hover-bg)] hover:text-[var(--code-action-hover-fg)] active:scale-[0.96] disabled:opacity-40 disabled:cursor-not-allowed transition-colors","aria-label":k.copyText?J(T)("common.copied")||"Copied":J(T)("common.copy")||"Copy",onClick:v[0]||(v[0]=z=>te("copy")),onMouseenter:v[1]||(v[1]=z=>yt(z)),onFocus:v[2]||(v[2]=z=>yt(z)),onMouseleave:Ee,onBlur:Ee},[k.copyText?(Y(),ie("svg",or,[...v[14]||(v[14]=[w("path",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M20 6L9 17l-5-5"},null,-1)])])):(Y(),ie("svg",lr,[...v[13]||(v[13]=[w("g",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2"},[w("rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}),w("path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"})],-1)])]))],40,nr)):we("",!0),y.showCollapseButton?(Y(),ie("button",{key:2,type:"button",class:"code-action-btn inline-flex items-center justify-center p-[var(--ms-action-btn-padding)] rounded leading-none shrink-0 cursor-pointer text-[var(--code-action-fg)] hover:bg-[var(--code-action-hover-bg)] hover:text-[var(--code-action-hover-fg)] active:scale-[0.96] disabled:opacity-40 disabled:cursor-not-allowed transition-colors","aria-pressed":k.isCollapsed,onClick:v[3]||(v[3]=z=>te("toggleCollapse")),onMouseenter:v[4]||(v[4]=z=>Ge(z,k.isCollapsed?J(T)("common.expand")||"Expand":J(T)("common.collapse")||"Collapse")),onFocus:v[5]||(v[5]=z=>Ge(z,k.isCollapsed?J(T)("common.expand")||"Expand":J(T)("common.collapse")||"Collapse")),onMouseleave:Ee,onBlur:Ee},[(Y(),ie("svg",{style:Vt({rotate:k.isCollapsed?"0deg":"90deg"}),xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",width:"1em",height:"1em",viewBox:"0 0 24 24",class:"action-icon"},[...v[15]||(v[15]=[w("path",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"m9 18l6-6l-6-6"},null,-1)])],4))],40,ir)):we("",!0),re.value?(Y(),ie("div",rr,[w("button",{ref_key:"moreBtnRef",ref:ke,type:"button",class:"code-action-btn inline-flex items-center justify-center p-[var(--ms-action-btn-padding)] rounded leading-none shrink-0 cursor-pointer text-[var(--code-action-fg)] hover:bg-[var(--code-action-hover-bg)] hover:text-[var(--code-action-hover-fg)] active:scale-[0.96] transition-colors","aria-expanded":ne.value,"aria-haspopup":"true",onClick:Do(r,["stop"]),onMouseenter:v[6]||(v[6]=z=>Ge(z,J(T)("common.more")||"More")),onFocus:v[7]||(v[7]=z=>Ge(z,J(T)("common.more")||"More")),onMouseleave:Ee,onBlur:Ee},[...v[16]||(v[16]=[qi('',1)])],40,ar),Ro(Wi,{name:"code-menu"},{default:Gt(()=>[ne.value?(Y(),ie("div",{key:0,ref_key:"moreMenuRef",ref:be,class:"code-more-menu min-w-[10rem] p-1 bg-[hsl(var(--ms-popover))] text-[hsl(var(--ms-popover-foreground))] border border-[var(--code-border)] shadow-[var(--ms-shadow-popover)]",role:"menu"},[y.showFontSizeButtons&&y.enableFontSizeControl?(Y(),ie(Po,{key:0},[w("button",{type:"button",role:"menuitem",class:"flex items-center gap-2 w-full py-1.5 px-2 rounded text-xs text-[var(--code-action-fg)] cursor-pointer whitespace-nowrap hover:bg-[var(--code-action-hover-bg)] hover:text-[var(--code-action-hover-fg)] disabled:opacity-40 disabled:cursor-not-allowed transition-colors",disabled:gl.value,onClick:v[8]||(v[8]=z=>{J(mt)(!0),te("decreaseFont")})},[v[17]||(v[17]=w("svg",{xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",width:"1em",height:"1em",viewBox:"0 0 24 24",class:"action-icon"},[w("path",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M5 12h14"})],-1)),w("span",null,Be(J(T)("common.fontSmaller")||"Font size −"),1)],8,ur),w("button",{type:"button",role:"menuitem",class:"flex items-center gap-2 w-full py-1.5 px-2 rounded text-xs text-[var(--code-action-fg)] cursor-pointer whitespace-nowrap hover:bg-[var(--code-action-hover-bg)] hover:text-[var(--code-action-hover-fg)] disabled:opacity-40 disabled:cursor-not-allowed transition-colors",disabled:Jt.value,onClick:v[9]||(v[9]=z=>{J(mt)(!0),te("resetFont")})},[v[18]||(v[18]=w("svg",{xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",width:"1em",height:"1em",viewBox:"0 0 24 24",class:"action-icon"},[w("g",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2"},[w("path",{d:"M3 12a9 9 0 1 0 9-9a9.75 9.75 0 0 0-6.74 2.74L3 8"}),w("path",{d:"M3 3v5h5"})])],-1)),w("span",null,Be(J(T)("common.fontReset")||"Font size reset"),1)],8,sr),w("button",{type:"button",role:"menuitem",class:"flex items-center gap-2 w-full py-1.5 px-2 rounded text-xs text-[var(--code-action-fg)] cursor-pointer whitespace-nowrap hover:bg-[var(--code-action-hover-bg)] hover:text-[var(--code-action-hover-fg)] disabled:opacity-40 disabled:cursor-not-allowed transition-colors",disabled:Yt.value,onClick:v[10]||(v[10]=z=>{J(mt)(!0),te("increaseFont")})},[v[19]||(v[19]=w("svg",{xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",width:"1em",height:"1em",viewBox:"0 0 24 24",class:"action-icon"},[w("path",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M5 12h14m-7-7v14"})],-1)),w("span",null,Be(J(T)("common.fontLarger")||"Font size +"),1)],8,dr)],64)):we("",!0),y.showExpandButton?(Y(),ie("button",{key:1,type:"button",role:"menuitem",class:"flex items-center gap-2 w-full py-1.5 px-2 rounded text-xs text-[var(--code-action-fg)] cursor-pointer whitespace-nowrap hover:bg-[var(--code-action-hover-bg)] hover:text-[var(--code-action-hover-fg)] transition-colors",onClick:v[11]||(v[11]=z=>{P(),te("toggleExpand")})},[k.isExpanded?(Y(),ie("svg",cr,[...v[20]||(v[20]=[w("path",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"m14 10l7-7m-1 7h-6V4M3 21l7-7m-6 0h6v6"},null,-1)])])):(Y(),ie("svg",fr,[...v[21]||(v[21]=[w("path",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M15 3h6v6m0-6l-7 7M3 21l7-7m-1 7H3v-6"},null,-1)])])),w("span",null,Be(k.isExpanded?J(T)("common.collapse")||"Collapse":J(T)("common.expand")||"Expand"),1)])):we("",!0),k.isPreviewable&&y.showPreviewButton?(Y(),ie("button",{key:2,type:"button",role:"menuitem",class:"flex items-center gap-2 w-full py-1.5 px-2 rounded text-xs text-[var(--code-action-fg)] cursor-pointer whitespace-nowrap hover:bg-[var(--code-action-hover-bg)] hover:text-[var(--code-action-hover-fg)] transition-colors",onClick:v[12]||(v[12]=z=>{P(),te("preview")})},[v[22]||(v[22]=w("svg",{xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",width:"1em",height:"1em",viewBox:"0 0 24 24",class:"action-icon"},[w("g",{fill:"none",stroke:"currentColor","stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2"},[w("path",{d:"M2.062 12.348a1 1 0 0 1 0-.696a10.75 10.75 0 0 1 19.876 0a1 1 0 0 1 0 .696a10.75 10.75 0 0 1-19.876 0"}),w("circle",{cx:"12",cy:"12",r:"3"})])],-1)),w("span",null,Be(J(T)("common.preview")||"Preview"),1)])):we("",!0)],512)):we("",!0)]),_:1})])):we("",!0)])])])):we("",!0),vl(w("div",{class:pt(["code-block-shell-content",{"code-block-shell-content--collapsed":k.isCollapsed}])},[ht(Q.$slots,"default")],2),[[ml,!!k.stream||!k.loading]]),vl(w("div",vr,[ht(Q.$slots,"loading",{},()=>[v[23]||(v[23]=w("div",{class:"loading-skeleton"},[w("div",{class:"skeleton-line"}),w("div",{class:"skeleton-line"}),w("div",{class:"skeleton-line short"})],-1))])],512),[[ml,!k.stream&&k.loading]]),w("span",mr,Be(k.copyText?J(T)("common.copied")||"Copied":""),1)],64))}}),hr={class:"html-preview-frame__header"},gr={class:"html-preview-frame__title"},yr={class:"html-preview-frame__label"},wr=["sandbox","srcdoc"],br=Ho(pl({__name:"HtmlPreviewFrame",props:{code:{},isDark:{type:Boolean},htmlPreviewAllowScripts:{type:Boolean},htmlPreviewSandbox:{},onClose:{type:Function},title:{}},setup(k){const M=k,y=import.meta!==void 0&&!1;let te=null;const{t:ne}=hl(),be=b(()=>{const P=M.code||"",H=P.trim().toLowerCase();return H.startsWith("
diff --git a/apps/kimi-code/dist-web/assets/DesignSystemView-B4B2cx0C.js b/apps/kimi-code/dist-web/assets/DesignSystemView-B4B2cx0C.js deleted file mode 100644 index e2afff6d49..0000000000 --- a/apps/kimi-code/dist-web/assets/DesignSystemView-B4B2cx0C.js +++ /dev/null @@ -1,13 +0,0 @@ -import{M as z,aD as q,aI as A,aL as n,u as l,v as t,G as c,H as e,F as u,aX as y,bb as k,I as s,bk as d,cx as o,cy as B,cz as x,cA as M,cB as h,cC as I,cD as E}from"./index-yKYHPeXU.js";const D={class:"ds-page"},H={class:"layout"},V={class:"content"},L={class:"content-inner"},R={id:"tokens"},O={class:"icon-sizes"},W={class:"sz"},N={class:"p-ic",style:{width:"14px",height:"14px"},viewBox:"0 0 24 24",fill:"currentColor"},P={class:"sz"},U={class:"p-ic",style:{width:"16px",height:"16px"},viewBox:"0 0 24 24",fill:"currentColor"},F={class:"sz"},j={class:"p-ic",style:{width:"20px",height:"20px"},viewBox:"0 0 24 24",fill:"currentColor"},G={class:"icon-grid"},K={class:"icon-group-label"},J={class:"ic-name"},Y={id:"primitives"},Q={class:"stage-wrap"},Z={class:"stage p"},X={class:"p-pill",style:{color:"var(--p-warning)"}},$={class:"stage-wrap"},_={class:"stage p col"},aa={class:"demo-row"},ta={class:"p-btn primary disabled"},ea={class:"p-spinner sm",viewBox:"0 0 24 24",style:{"--p-accent":"#fff","--p-line":"rgba(255,255,255,.35)"}},sa={class:"stage-wrap"},da={class:"stage p col"},oa={class:"demo-row"},ca={class:"stage-wrap"},ia={class:"stage p col",style:{gap:"0",background:"var(--p-surface)",padding:"0","max-width":"300px","align-items":"stretch"}},na={style:{display:"flex","align-items":"center",gap:"8px",padding:"7px 10px",margin:"1px 6px","border-radius":"8px",color:"var(--p-text)","font-size":"13px"}},la={style:{color:"var(--d-fg-faint)",flex:"none"},width:"14",height:"14",viewBox:"0 0 24 24",fill:"currentColor"},ra={style:{display:"flex","align-items":"center",gap:"8px",padding:"7px 10px",margin:"1px 6px","border-radius":"8px",color:"var(--p-text)","font-size":"13px"}},ba={style:{color:"var(--d-fg-faint)",flex:"none"},width:"14",height:"14",viewBox:"0 0 24 24",fill:"currentColor"},va={id:"chat"},pa={class:"stage-wrap"},ha={class:"stage p col"},ua={style:{"max-width":"560px",width:"100%"}},ga={class:"stage-wrap"},ma={class:"stage p col",style:{"align-items":"center",background:"#fff"}},fa={class:"p-composer",style:{width:"100%","max-width":"620px"}},wa={class:"p-composer-bar"},ya={class:"p-composer-left"},ka={class:"p-pill",style:{color:"var(--p-warning)"}},xa={class:"stage-wrap"},Ta={class:"stage p",style:{background:"var(--p-bg)"}},Sa={class:"am-mock"},Ca={class:"am-mock-row focus"},za={class:"am-mock-row"},qa={class:"am-mock-row"},Aa={class:"am-mock-row"},Ba={class:"dt"},Ma={class:"dw-spin"},Ia={class:"stage-wrap"},Ea={class:"stage p",style:{background:"var(--p-bg)"}},Da={class:"dw-bar"},Ha={class:"dw-pill"},Va={class:"dw-pill"},La={class:"dw-pill on"},Ra={class:"dw-running"},Oa={class:"dw-pill"},Wa={class:"stage-wrap"},Na={class:"stage p col",style:{background:"var(--p-bg)",gap:"var(--space-4)"}},Pa={class:"dw-panel"},Ua={class:"dw-head"},Fa={class:"dw-tab"},ja={class:"dw-chips"},Ga={class:"dw-chip on"},Ka={class:"dw-chip"},Ja={class:"dw-chip"},Ya={class:"dw-chip"},Qa={class:"dw-body col"},Za={class:"dw-row"},Xa={class:"dw-row"},$a={class:"dw-row fail"},_a={class:"dw-row cancelled"},at={class:"dw-panel"},tt={class:"dw-head"},et={class:"dw-tab"},st={class:"dw-chips"},dt={class:"dw-chip on"},ot={class:"dw-chip"},ct={class:"dw-chip"},it={class:"dw-chip"},nt={class:"dw-body grid"},lt={class:"dw-card"},rt={class:"cf"},bt={class:"cm"},vt={class:"cs"},pt={class:"sl"},ht={class:"tm"},ut={class:"dw-card"},gt={class:"cf"},mt={class:"cm"},ft={class:"cs"},wt={class:"sl"},yt={class:"tm"},kt={class:"dw-card fail"},xt={class:"cf"},Tt={class:"cm"},St={class:"cs"},Ct={class:"sl"},zt={class:"tm"},qt={class:"dw-card"},At={class:"cf"},Bt={class:"cm"},Mt={class:"cs"},It={class:"sl"},Et={class:"tm"},Dt="/repo",Ht=z({__name:"DesignSystemView",emits:["close"],setup(Vt,{emit:T}){const S=[{path:"/repo/apps/web/src/components/chat/TurnFilesSummary.vue",added:19,removed:4,hasWrite:!1,statsIncomplete:!1,diff:null},{path:"/repo/apps/web/src/composables/useFilePreview.ts",added:8,removed:1,hasWrite:!1,statsIncomplete:!1,diff:null},{path:"/repo/apps/web/src/components/chatTurnRendering.ts",added:0,removed:0,hasWrite:!0,statsIncomplete:!0,diff:null},{path:"/repo/apps/web/src/lib/toolDiff.ts",added:3,removed:2,hasWrite:!1,statsIncomplete:!1,diff:null}];function g(){}const C=T;function m(){C("close")}let p=null;function f(v){v.key==="Escape"&&m()}return q(()=>{document.addEventListener("keydown",f);const v=Array.prototype.slice.call(document.querySelectorAll('#nav a[href^="#"]')),a=new Map;v.forEach(b=>{const i=b.getAttribute("href");if(!i)return;const w=document.getElementById(i.slice(1));w&&a.set(w,b)});let r=null;p=new IntersectionObserver(b=>{b.forEach(i=>{i.isIntersecting&&(r&&r.classList.remove("active"),r=a.get(i.target)??null,r&&r.classList.add("active"))})},{rootMargin:"-20% 0px -70% 0px",threshold:0}),a.forEach((b,i)=>p.observe(i)),v.length&&v[0].classList.add("active")}),A(()=>{document.removeEventListener("keydown",f),p&&(p.disconnect(),p=null)}),(v,a)=>(n(),l("div",D,[t("div",{class:"ds-topbar"},[t("button",{class:"ds-back",type:"button",onClick:m},"← Back"),a[0]||(a[0]=t("span",{class:"ds-topbar-title"},"Design system",-1))]),t("div",H,[a[146]||(a[146]=c('',1)),t("main",V,[t("div",L,[a[144]||(a[144]=c('This document defines the visual language and component specification for Kimi Web — design tokens, component primitives, the chat interface, theming, and style rules. All UI work is grounded in it: unified, restrained, token-driven, and themeable.
Every UI decision traces back to the following principles. Kimi Web is a local Agent tool for developers: quick scanning, long stretches of staring, often in the dark — the design serves the task, and is restrained, clinical, and density-first.
Collapse every visual decision into tokens. Color tokens keep the existing short names and fill out the semantics (lowering migration cost), while spacing, z-index, motion, and font-weight fill in the scales that are currently missing. Every token has: name, light value, dark value, and usage.
--<category>-<role>-<state>. For example --color-text-muted, --radius-md, --space-4. To reduce churn, the existing short names (--bg / --ink / --line / --blue …) are kept as compatibility aliases for one release cycle. Semantic-first, in three layers: background / text / border + accent + status colors. All colors are defined in light / dark pairs, with contrast ≥ 4.5:1.
:root and a dark override in the data-color-scheme blocks — for example --color-bg is #ffffff in light and #121212 in dark; --color-accent is the brand blue (#1783ff light / #1a88ff dark). The semantic status colors (success / warning / danger / info) are independent palettes, one set each for light / dark.| Token | Light | Dark | Usage |
|---|---|---|---|
| --color-bg | #ffffff | #121212 | Page background |
| --color-surface | #f5f5f5 | #1f1f1f | Panel / sidebar / card head |
| --color-surface-raised | #ffffff | #292929 | Raised card / dialog / input |
| --color-menu-bg | rgba(255,255,255,.95) | rgba(41,41,41,.95) | Floating menu panel — frosted glass over --p-menu-backdrop blur |
| --color-surface-overlay | #ffffff | rgba(255,255,255,.1) | Field-control fill on raised cards (selects, steppers) — top rung; light tops out at white (the level is carried by the border), dark steps one rung above raised. Floating layers stay at raised |
| --color-well | #f5f5f5 | #1f1f1f | Content well on the page (code blocks, tool-output panels, match/file lists, media thumbnails) — light reuses the sunken recess; dark lifts one rung ABOVE the page, because a true recess (#121212) vanishes into the page there |
| --color-surface-deep | #f5f5f5 | #0d0d0d | Deep chrome plane one step BELOW the page (panel headers, diff gutters) — dark drops under --color-bg so chrome framing stays darker than the content it frames |
| --color-text | rgba(0,0,0,.9) | rgba(255,255,255,.84) | Body text / headings |
| --color-text-strong | #000000 | #ffffff | Max foreground emphasis — menu-row label & icon on hover |
| --color-text-muted | rgba(0,0,0,.6) | rgba(255,255,255,.56) | Secondary text / placeholder |
| --color-line | rgba(0,0,0,.13) | rgba(255,255,255,.12) | Divider / card border |
| --color-subtle | rgba(0,0,0,.05) | rgba(255,255,255,.05) | Subtle hairline — tertiary separators below --color-line (diff-gutter column rules, quiet dividers inside wells) |
| --color-selected | rgba(0,0,0,.05) | rgba(255,255,255,.1) | Neutral selected fill (sidebar rows, list pickers) — translucent, never accent-tinted |
| --color-hover | rgba(0,0,0,.03) | rgba(255,255,255,.05) | Row hover wash — lighter than the selected fill (hover < selected); translucent, sits on any surface. The global hover rule: transparent-base controls overlay this f1 wash (hover never darkens — never sunken); filled controls use their own hover token (accent-hover, send-bg-hover) |
| --color-selected-hover | rgba(0,0,0,.08) | rgba(255,255,255,.14) | Hover of a control RESTING at the selected fill (work cards) — one rung above f2, below f3: hover deepens/brightens a step, never drops below the rest state |
| --color-inline-code-bg | rgba(0,0,0,.03) | rgba(255,255,255,.1) | Inline-code chip fill — fills.f1 / fills.f2; dark lifts off any dark surface (sunken == bg there) |
| --color-media-alpha-bg-1 | ≈#858585 | ≈#76797e | Checkerboard square A of the <img> alpha canvas — color-mix of --color-bg/--color-text (52/48); applied via --media-alpha-canvas (16px period) |
| --color-media-alpha-bg-2 | ≈#6b6b6b | ≈#8c8f93 | Checkerboard square B (42/58) — both squares stay ≥3:1 against white and black; opaque images cover the canvas |
| --color-sidebar-bg | #f9fbfc | #0d0d0d | Sidebar surface — one step off --color-bg (just under white in light, one step BELOW the page in dark) so the session column reads as its own plane and never brighter than the reading surface |
| --color-scrim | rgba(0,0,0,.4) | rgba(0,0,0,.6) | Modal scrim — the dark veil behind dialogs/lightboxes (mask.base; legacy hardcoded overlays can migrate here) |
| --color-scrim-strong | rgba(0,0,0,.6) | rgba(0,0,0,.75) | Stronger scrim for full-screen media previews (mask.strong — the PhotoSwipe image preview backdrop) |
| --color-text-on-scrim | #ffffff | same | Text drawn on the scrim (captions over the media lightbox) |
| --color-accent | #1783ff | #1a88ff | Primary action / link / focus |
| --color-success | #0e7a38 | #3fb950 | Success / pass |
| --color-warning | #a9610a | #d29922 | Warning / pending |
| --color-danger | #c0392b | #f85149 | Danger / error / abort |
The palette is the production kimi.com palette (design tokens tokens.json): neutral-gray surfaces, an alpha-based label / fill / separator ramp (labels.* / fills.* / separator.s1), the KMBlue accent, and a true neutral dark ladder (#121212 → #1f1f1f → #292929; the deep chrome plane and sidebar derive one step below at #0d0d0d — the palette has nothing darker than primary).
The ONE deliberate exception is the status hues: success / warning / danger / done keep the app's own WCAG-tuned ramp (≥4.5:1 on the neutral surfaces) — the production status colours (positiveGreen #16c456, orange #ff9500, danger red #ff3849) are too bright against it. Diff add/del bands happen to coincide (both use the production 25% fills in light, 14% in dark).
The surface layers each have a role — choose by "field overlay / raised layer / content well / default flat layer / sunken layer / page background / deep chrome", and avoid treating --p-surface-raised as a universal background. In dark, elevation = lighter: floating layers sit above the content, content wells sit above the page, and chrome planes (sidebar, panel headers) sit below it — never the reverse. One consequence: on the page itself, never use --color-surface-sunken for a content carrier — it equals --color-bg in dark and the fill vanishes; use --color-well. Sunken stays correct INSIDE surface / raised cards, where it is a genuine recess. Field controls (selects, steppers) on a raised card use --color-surface-overlay, the top fill rung; floating layers keep --color-surface-raised — their elevation is shadow + hairline, not a lighter fill.
| Token | Light | Dark | Usage |
|---|---|---|---|
| --p-surface-overlay | #ffffff | #22272e | Field controls on raised cards — select, stepper (top fill rung; light = white) |
| --p-surface-raised | #ffffff | #1c2128 | Raised card / dialog / input (raised layer) |
| --p-well | #f3f5f8 | #13181e | Code block / tool output / list carrier directly on the page (content well — light: recessed, dark: one rung above the page) |
| --p-surface | #fafbfc | #13181e | Panel / sidebar / card head (default flat layer) |
| --p-surface-sunken | #f3f5f8 | #0d1117 | Recessed area INSIDE a surface / raised card — never a content carrier on the page (sunken layer) |
| --p-bg | #ffffff | #0d1117 | Page background |
| --p-surface-deep | #fafbfc | #0a0d12 | Panel header / diff gutter (deep chrome layer — below the page in dark) |
Three line tokens, three jobs: --color-line is the default structural separator, --color-subtle the tertiary separator that must stay quieter (diff-gutter column rules, quiet dividers inside wells), and --color-line-strong the edge of interactive controls (inputs, selects, secondary buttons). Width is one: 0.5px — every stroke is the same hairline, on static structural edges (card rims, plane seams, header dividers), interactive control rims and floating layers alike. Separation comes from luminance first — planes one rung apart already read as distinct in dark, so their shared edge stays a 0.5px hairline rather than a heavier border; same-rung neighbours (list rows, card head / body) are exactly where a hairline is required. In dark, drop shadows fade on near-black surfaces, so a floating layer's edge IS its hairline — never ship a shadow-only floating surface. (Legacy --line / --line2 alias --color-line / --color-subtle for one cycle; new work references the v2 names.)
All focusable controls (button, input, link, menu item, switch, checkbox) use the focus-ring token uniformly; do not hand-write a box-shadow focus ring.
| Token | Value | Usage |
|---|---|---|
| --p-focus-ring | 0 0 0 3px var(--p-accent-soft) | Default focus ring (link, menu item, switch, checkbox) |
| --p-focus-ring-strong | 0 0 0 3px var(--p-accent-soft), 0 0 0 1px var(--p-accent) | Strong focus ring (button, primary action) |
The text-selection color uses --p-selection uniformly (light rgba(23,131,255,.18) / dark rgba(88,166,255,.32)), applied by the global ::selection rule; do not set a separate highlight background.
All disabled controls use opacity:.5 + cursor:not-allowed uniformly; do not separately grey out or recolor.
Kimi Web uses two font tokens: --font-ui (UI and body, with Schibsted Grotesk for Latin and Noto Sans SC for Simplified Chinese) and --font-mono (code and monospace). Components always reference the variables; do not hard-code font names.
Body and UI use self-hosted Schibsted Grotesk for Latin text and self-hosted Noto Sans SC Variable for Simplified Chinese. Platform fonts remain as fallbacks:
--font-ui: "Schibsted Grotesk Variable", "Helvetica Neue", Arial, - "Noto Sans SC Variable", "Noto Sans SC", "PingFang SC", - "Microsoft YaHei", - -apple-system, BlinkMacSystemFont, "Segoe UI", - Roboto, Ubuntu, sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji";
Code, line numbers, diffs, and Bash commands use JetBrains Mono (a self-hosted variable font), falling back to the system monospace. Other tool labels and summaries use the UI font:
--font-mono: "JetBrains Mono Variable", "JetBrains Mono", - ui-monospace, "SF Mono", Menlo, Consolas, monospace;
| Font | Source | Bundled | Usage |
|---|---|---|---|
| JetBrains Mono | @fontsource-variable/jetbrains-mono | ✓ self-hosted | monospace / code (--font-mono) |
| Schibsted Grotesk | prepare-fonts → app-ui/assets/fonts | ✓ generated + bundled | UI / body / display (--font-ui, --font-display), wght 400-900, normal + italic |
| Noto Sans SC | prepare-fonts → app-ui/assets/fonts | ✓ generated + bundled | Simplified Chinese UI / body, wght 100–900 |
| System UI / CJK fonts | operating system | — | late fallback for UI / body |
var(--font-ui) / var(--font-mono); do not hard-code font names like 'Schibsted Grotesk' / 'JetBrains Mono'.--font-ui (Schibsted Grotesk for Latin, Noto Sans SC for Simplified Chinese); code / monospace use --font-mono (JetBrains Mono).font-optical-sizing: auto is enabled globally.The user font-size preference is one of four named steps (small / medium / large / xlarge, Medium default) written to data-font-scale on <html>; the step name is persisted, never a px value. The step only moves --base-font; every size token derives additively (default + shift), and line heights are locked to integer px via round(size × ratio, 1px) — never a unitless ratio.
Two token groups share the shift but keep their own ratios: --ui-* for chrome (tight, 1.40–1.50) and --md-* for Markdown content + the composer (loose, 1.56–1.63; body is anchored to the UI body size — the spec's +2px offset was dropped as a product decision — while keeping its own looser line-height ratios). T0/T1 cap at 24/22px on the top steps (built into the tokens via min() — do not remove). Use the .text-ui-* / .text-md-* utility classes; legacy aliases --ui-font-size (→ --ui-b2), --content-font-size (→ --md-b1) and the whole 6-level --text-* ramp (xs→c1, sm→b2−1px, base→b2, lg→t2, xl→t1, 2xl→t0) keep older components on the ramp. Panel titles sit at the base step (--ui-b2); dropdown menu items sit one rung below (--text-sm = b2 − 1px) — both still follow the user's font scale.
The fixed product type tokens still define scale-independent defaults: transcript prose enables text-autospace: normal for mixed CJK and Latin text. Drop stray font-weight: 650 / 750; converge on 400 / 500 (regular / emphasis), with a dedicated 600 weight for sidebar section labels.
| Token | Value | Usage |
|---|---|---|
| --font-ui | "Schibsted Grotesk Variable", …, "Noto Sans SC Variable", … | UI & body (Schibsted Grotesk + Noto Sans SC) |
| --font-kbd | "Schibsted Grotesk Variable", system-ui, sans-serif | keyboard shortcut keycaps |
| --font-mono | JetBrains Mono… | code, Bash commands, line numbers, diffs |
| data-font-scale | small / medium / large / xlarge | user preference on <html>; sets --base-font (12–18px), Medium = 14px default |
| --ui-t0…--ui-c2 | default + --ui-shift, t0/t1 capped via min() | chrome type ramp (title / subtitle / body / caption); .text-ui-* classes |
| --md-h1…--md-b3 | default + --md-shift | Markdown ramp (headings / body / secondary / code); .text-md-* classes |
| --ui-font-size / --content-font-size | var(--ui-b2) / var(--md-b1) | legacy aliases kept on the ramp |
| --code-font-size | calc(var(--content-font-size) - 2px) | standalone code surfaces (diff view, file preview, tool cards) — one step below body, 12px @ Medium; prose-embedded code stays on the --md-* ramp |
| --text-xs / sm / base / lg / xl / 2xl | c1 / b2−1 / b2 / t2 / t1 / t0 | legacy ramp, aliased into the scale |
| --leading-tight/normal/prose/relaxed | 1.25 / 1.5 / 1.6 / 1.7 | headings / UI / chat prose / long text |
| --weight-regular/option-label/medium/ui-strong | 400 / 475 / 500 / 525 | body / settings labels / emphasis / compact UI emphasis |
| --weight-section-label | 600 | sidebar section labels |
Icons use three size tokens uniformly. The global .p-ic default is 16px (--p-ic-md); components pick as needed, and random pixel sizes are forbidden.
| Token | Value | Usage |
|---|---|---|
| --p-ic-sm | 14px | small button, badge, menu item, inline link icon |
| --p-ic-md | 16px | default (button, icon button, toolbar) |
| --p-ic-lg | 20px | Toast status icon, empty-state illustration |
Icons always come from the centralized registry lib/icons.ts: in templates use the <Icon name size /> component (components/ui/Icon.vue); for v-html contexts (such as a tool glyph) use iconSvg(name, size). Do not hand-write <svg> — the scripts/check-style.mjs icon-from-registry rule flags stray SVGs. Every glyph shares the 24×24 source grid and currentColor (colour follows text); size uses the three tokens below, and only icons imported in lib/icons.ts are bundled by unplugin-icons at build time. Three collections feed the registry, in this order of preference: ~icons/kimi/* — Kimi Design System icons (24×24 outlined, 1.8px stroke), local SVGs under src/icons/kimi/ registered as a custom collection in the Vite config, used whenever a Kimi glyph exists for the intent; ~icons/tabler/* — Tabler Icons (MIT), for the few gaps it uniquely covers (today: the right-panel toggle); and ~icons/ri/* — Remix Icon (Apache-2.0), for the remaining intents the Kimi set does not cover yet. A few glyphs are filed under their intent rather than the upstream asset name (see the lib/icons.ts header). When an icon is missing, prefer a glyph from the Kimi icon set: copy the SVG into src/icons/kimi/ (kebab-case name, monochrome currentColor) and register it — two static imports (component + ?raw string) plus one entry in ICONS; reach for Remix only when no Kimi glyph fits, and never draw paths in a component.
Do not use emoji as functional icons. The Kimi brand mark (the robot mascot logo) is a brand asset and is not part of this icon system.
A few special graphics are not in the registry; each has a dedicated component maintained in one place, and must not be copied by hand: <ContextRing :pct /> (the Composer context progress ring, data-driven), <AuthStateIcon kind /> (the success / expired / error colored illustrations in the login flow), <Spinner /> (loading state). Status dots (such as in the Provider list) always use CSS dots (border-radius:50%), not SVG. The scripts/check-style.mjs icon-from-registry rule exempts the above and the brand mark; all other hand-written <svg> is flagged.
A 4px base grid. All spacing, gaps, and padding inside and outside components come from this scale — no arbitrary pixels.
High-density navigation lists like the sidebar share one rhythm, all on the 4px grid: in-row vertical padding --space-1 (4px), no margin between rows (the hover pill provides the separation); section gap (between logo / search / action buttons / group title / list) uniformly --space-2 (8px); between groups --space-2; the brand header is slightly looser at the top (--space-3). When building similar lists, reuse this scale — do not hand-write 1/6/7/10px.
Merge the existing 14 values into the nearest of 7 scale steps. Rule: the component type determines the radius, not the author's feel. The Composer shell is the sole product-specific exception: its 32px radius pairs with superellipse(1.5) so the flatter curve stays visually concentric with its controls.
| Token | Value | Usage | Merged from |
|---|---|---|---|
| --radius-xs | 4px | small badge, inline tag | 2/3/4px → |
| --radius-sm | 6px | small button, icon button, menu item | 5/6px → |
| --radius-md | 8px | button, input, badge, card | 7/8/9px → |
| --radius-lg | 12px | menu, toast, bubble, floating card | 10/12px → |
| --radius-xl | 16px | container baseline: dialogs, settings cards, sheets, work panel | 13/16px → |
| --radius-2xl | 20px | workspace attachment card bottom (0 0 2xl 2xl) tucked under the composer | 18/20px → |
| --radius-composer | 32px | Composer shell, with --corner-shape-composer | product-specific |
| --radius-menu-row | var(--radius-sm) | Menu rows inset 6px inside the plain --radius-lg menu frame (concentric: 12px − 6px hug) | product-specific |
| --radius-full | 999px | pill badge, avatar, send button | 999px / 50% |
Shadows express only "elevation", never decoration (no colored glow). z-index is unified into a scale, eradicating 9999-style one-upping.
| Z-index Token | Value | Usage |
|---|---|---|
| --z-base | 0 | normal flow |
| --z-raised | 1 | in-component local stacking (menu scroll thumbs, strip badges, floating pills) — never a global layer |
| --z-sticky | 100 | sticky header / sidebar |
| --z-dropdown | 200 | dropdown menu |
| --z-overlay | 300 | overlay / bottom Sheet |
| --z-modal | 400 | dialog — sibling overlays tie-break by DOM order, so the global confirm (ConfirmDialogHost) mounts on demand to always land last / on top |
| --z-modal-dropdown | 500 | menus / popovers that open above a modal dialog (teleported to <body>, e.g. the settings SecondaryModelPicker cascade) |
| --z-toast | 600 | toast |
| --z-tooltip | 650 | tooltip bubble — transient and pointer-events none, so it sits above everything (dialogs, toasts) to stay visible anywhere |
| --z-max | 9999 | reserved: only this tier for extreme fallback |
| Token | Value | Usage |
|---|---|---|
| --ease-out | cubic-bezier(0.16, 1, 0.3, 1) | enter, hover, expand |
| --ease-in-out | cubic-bezier(0.4, 0, 0.2, 1) | panel width, layout changes |
| --duration-fast | 120ms | press, focus |
| --duration-base | 160ms | hover, show/hide |
| --duration-slow | 260ms | dialog, Sheet, layout |
| --duration-hover-intent | 250ms | hover-intent reveal gate (TOC rail) |
| --duration-spin | 700ms | spinner rotation period (mention-tip probe spinner) |
| --duration-flash | 1200ms | one-shot attention flashes (search locate, provider-row added) — a highlight timeout, past the show/hide ramp on purpose |
| --anim-rive-spin | 416.7ms | new-chat / folder-plus icon: plus spin on hover |
| --anim-leftbar | 533.3ms | sidebar toggle icon: arrow fly-in on hover |
| --anim-leftbar-shrink | 200ms | sidebar toggle icon: divider shrink on hover |
The --anim-* lengths are track timings ported verbatim from the designer's Rive exports, so they sit outside the --duration-* ramp on purpose — retiming the ramp must not distort them. Their interpolation stays linear because the easing is already baked into the dense keyframe stops; a token easing would double-apply. Three hover tracks use them today: the sidebar toggle shrinks its divider to half height while an arrow flies in and settles (the expand variant mirrors the track from the left), and the new-chat / folder-plus pluses do one bouncy spin. Each track is keyed to an id inside its own glyph (#bar-divider, #bar-arrow / #bar-arrow-expand, #p1, #af-p1) so every instance of the icon animates, and all revert on mouse-out. They still fall under the global reduced-motion switch below.
@media (prefers-reduced-motion: reduce), all animation and transition durations drop to about 0.001ms (effectively off), and the chat working indicator's mascot renders its static fallback instead of the Rive loop. Components should not check this individually; it is handled uniformly in the global styles. The switch clears durations, not transition-delay: a hover-intent gate (the conversation TOC's 250ms reveal) decides whether hidden content appears, and clearing it would make pointer fly-bys strobe content for reduced-motion users. Layout sizes and responsive breakpoints are tokenized too: sidebar width, content reading-column width, and two global breakpoints. Components should not hard-code pixels.
| Token | Value | Usage |
|---|---|---|
| --p-sidebar-w | 264px | left session sidebar width |
| --p-content-max | 760px | chat reading-column max width (regular chat prose) |
| --p-content-wide | 920px | wide content (settings / panel) |
| --p-table-max | 1040px | desktop wide-table max width (see §04) |
| --p-table-cell-max | 700px | max width of a single table column; longer cell content wraps (see §04) |
| --p-bp-sm | 640px | mobile / desktop boundary |
| --p-bp-md | 980px | narrow / wide screen boundary |
Component primitives are the "smallest correct units" of the site UI. Each primitive exposes variants along only two dimensions — variant / size — with appearance driven by tokens, so it naturally supports light / dark mode and customizable theme colors.
| Scenario | Use |
|---|---|
| Primary action (submit / confirm) | Button variant=primary |
| Secondary action / cancel | Button secondary / ghost |
| Destructive action (delete / abort) | Button danger / danger-soft |
| Status marker | Badge |
| Toolbar filter / model switch | Pill |
| 2–5 mutually exclusive options | SegmentedControl |
| Top tabs | Tabs |
| Switch / multi-select | Switch / Checkbox |
| Scrollable regions with overlay controls | ScrollArea |
| Floating content card / list action menu | Card / Menu |
| Inline notice / global toast | Banner / Toast |
| Dialog / confirmation · bottom panel (mobile) | Dialog / Sheet |
4 semantic variants × 3 sizes. The primary action primary takes its color from the current theme color (§05 can switch between the blue and black families). Radius uses --radius-md uniformly (small size --radius-sm), weight 600, with a visible focus ring.
<Button variant="primary" size="md" :loading="submitting">Save</Button> - // variant: primary | secondary | ghost | danger | danger-soft - // size: sm | md | lg
Unified into three sizes — 26 / 32 / 44px — with the neutral --color-hover wash on hover and a visible focus ring. Replaces the ad-hoc icon + click areas scattered across components today.
sm 26 / md 32; on touch devices the tap target should be ≥ 44px, so use lg 44px, satisfying the §01 accessibility principle (the mobile three-piece set uses lg). Icon-only buttons must also name themselves on hover: pass tooltip (usually the same text as label — label alone only sets the aria-label); bare icon <button>/<a> triggers wrap the Tooltip component directly. Collapsed into two kinds: Badge (status badge, with an optional status dot) and Pill (the clickable pill in the composer toolbar). Radius, font size, and padding are all unified.
Kbd renders a shortcut as keycaps — one block per key, never inline text like (⌘K). Caps are 18px tall (Badge sm rhythm): transparent ground with a 0.5px hairline edge, 11px --font-kbd (Inter + system-ui), text colour inherited from the row that carries it — the cap has no fill or colour of its own, so it follows its context (bright inside the accent-ringed recording box, quiet in a hint row). Typical placement: pushed to the row's trailing edge, opposite the label (e.g. the sidebar search row), and inside dialog navigation hints.
All cards across the site share one structure — head / body / foot — and come in two tiers by visual weight:
0.5px hairline, --radius-md, no shadow. The head is compact mono with no fill, low weight by default, not competing with the conversation.--radius-lg, a faint popover shadow (--shadow-menu), a plain dark title head, and a hairline footer whose actions read in number-key order (chips on the buttons) leading to one solid primary action. No semantic color band.head / body / foot; operation cards are flat + 0.5px hairline + radius-md with no shadow, while the attention card is the single exception — raised surface, radius-lg and a soft shadow, because it floats above the transcript in place of the composer.Unified 38px height (32px small), --radius-md radius, --color-surface-overlay background, and a unified blue focus ring (0 0 0 3px accent-soft). Select is a custom combobox and listbox, not a native <select>; opening it centres the selected option in the scrollable menu. Open Select roots enter the dropdown layer; containing settings groups temporarily release clipping and join that layer so later sections cannot cover the menu.
Diff controls: The non-selectable branch summary starts with a 14px branch icon, aligns to the panel header's 12px inset, uses 12px labels, and ends with a 0.5px hairline. List and tree choices use the 14px list and tree-view registry icons. Flat-list and tree-view paths use the UI font at 12px. Tree roots share the flat list's 14px content inset, then each depth advances by 12px and adds a grey indentation rule.
Diff empty state: Centre the clean-workspace message in the available panel height and lead with a quiet 32px status icon.
Diff detail body: the right-side diff detail reuses HighlightedCode unframed (the panel owns the edge and scroll) — shiki highlighting with the language inferred from the file path, an old/new line-number gutter, hunk headers as a muted band, at the shared code size --code-font-size (12px at Medium, one step below body text). The file preview's code body (text / JSON / HTML and Markdown source) renders through the same component with a per-row number gutter plus search-hit / jump-target row states.
Inline code, code blocks, and diff contents use the monospace font (--p-font-mono); diff change counts and branch summaries use the UI font. Code blocks have a filename title bar and a copy button; the action edge uses a compact 6px inset. Diffs use + / - row colors to express additions and deletions — additions use a success light background, deletions use a danger light background, with no gradients.
jwt.verify(token) to verify the signature, returning 401 on failure.import { verify } from './jwt';
-
- export function auth(token: string) {
- return verify(token, process.env.JWT_SECRET!);
- }One dialog primitive replaces 6 hand-written implementations: unified --radius-xl radius, --shadow-xl shadow, 20px head padding, right-aligned footer actions, and an IconButton close button.
md 440 / lg 640 / xl 760 (--p-content-max) — chosen by content weight. Height comes in two kinds: auto (default, grows with content up to max-height) and fixed (constant height min(680px, 100vh - 64px), with overflow scrolled inside the body). Content / multi-tab dialogs (settings, model picker, provider manager, folder browser) always use fixed so the frame size stays constant and doesn't jump when switching tabs or content length; short confirmation dialogs keep auto. Selectable controls inside Settings use 0.5px hairlines. Its navigation stays transparent on the grouped canvas — separated from the content region by the 0.5px hairline (horizontal in the stacked mobile layout) — and uses 12px labels at weight 525 with 16px registry icons; the selected tab paints the same neutral --color-hover wash as hover, with the label simply brightening to --color-text — the Kimi app settings nav's recipe (.ss-nav-item--active → Fills-F1, no accent tint, no weight change); section captions use 16px UI text in --color-text. Every setting row has a plain-language description; option labels use --color-text at weight 475 with a 1px gap before that description. Chinese descriptions use “思考” and “计划模式” rather than the English terms; “skills” stays lowercase when it appears within a sentence. Every settings section puts its rows inside one rounded group with 0.5px dividers; the content region paints the flat --color-surface so each group (--color-surface-raised) reads one rung above it — never a sunken pit, which would sink the dialog's content below its chrome in dark. The font-size stepper is a compact 32px UI-font control with 12px values and custom minus and plus buttons. Its 52px desktop row centres the control with equal space above and below. Archived workspace headings reuse the sidebar’s folder-closed registry icon, and Restore actions lead with the undo icon. Archive counts use weight 500; timestamps and workspace paths use the UI font. Dialog backdrop: Use a restrained 28% neutral overlay so the workspace remains legible without competing with the modal.
Settings regions: The settings title and close action belong to the right content region. The navigation is a separate full-height region that starts at the dialog's top edge, not content beneath a dialog-wide header.
Archived sessions: Start with the localized page title. Do not add a repeated English kicker above it.
Settings interaction: Notification labels and descriptions are not selectable; their switches remain fully interactive.
Conversation chrome: Header labels are not selectable; the rename input remains selectable and editable. Branch names start with a 14px branch icon. The overflow trigger is a compact 24px control with a 14px icon. Below a 720px header container, hide the workspace prefix and give the conversation title the available width. On macOS desktop the header doubles as the window-drag region and interactive controls opt out with no-drag; while one of its menus or a dock work panel is open every window-drag strip (chat header, sidebar header, panel header) drops the drag region so an outside press anywhere reaches the page and dismisses the overlay (window dragging is simply paused).
Session search: follows the §09 flush picker anatomy — a boxed Input under the head, and a result list that fills the body's available height and owns vertical scrolling.
Model picker: follows the §09 flush picker anatomy; the provider filter remains horizontally scrollable without showing a persistent scrollbar. Only the model list scrolls; the shortcut bar remains pinned at the bottom.
Unified information architecture: status icon + title + description. The status color appears only on the icon, avoiding large colored areas that create visual noise. For an undoable action there is a second, lighter form — the Action toast (ActionToast.vue): a pill floating top-center just below the 48px header, carrying a one-line sentence whose actions are plain inline <button>s (styled accent by the component), plus close. Self-timed (default 8s, hover pauses); the parent re-keys to reset and wraps it in a <Transition>. First used by session archive (Undo / Settings); warnings keep the bottom-right Toast stack.
Loaders fall into two categories by scenario — do not mix them:
WorkingIndicator — the 小蓝 mascot (KimiMascot, the kimi.com avatar Rive asset, with a static SVG fallback under reduced motion or when the runtime fails) plus a phase label. All other loading states use the plain Spinner.Inline text link: the default is the accent color with no underline; on hover it shows an underline and darkens. File links inside inline code use a 1.5px underline offset so the line stays clear of the chip background. The .muted variant uses the secondary text color. Used for in-text jumps, external links, "view all", and other lightweight actions.
Desktop menus use a 3.5px panel inset. Standard items use 5px × 9px padding and a 7px icon gap. Their three-layer neutral shadow stays below 4% opacity.
Dropdown menu panel: frosted glass — the translucent --color-menu-bg fill over a blurred, saturated page backdrop (--p-menu-backdrop) — plus hairline + light shadow (--shadow-menu, a three-layer neutral ramp). This is the one place glassmorphism is the design language rather than an exception (§06); every floating menu surface (Menu.vue, the Select listbox, composer dropdowns, slash/mention popups) uses the token pair, never ad-hoc blur values. Menu items support icons, the current (active) state, the danger state, and the disabled state, with separators grouping items. All menu actions use 13px labels at weight 475 with 16px leading icons; both share a 16px line box for vertical alignment. Menu timestamps use the UI font. On touch / mobile, use lg (≥44px row height) while keeping the same type size. A dropdown menu pops in from its trigger corner — fade plus a slight 0.97 scale over --duration-base (exit --duration-fast), the composer model dropdown's motion language; the transform origin and the nudge direction follow the anchoring, including the upward flip near the viewport edge.
Row states: hover uses the mode-aware --color-hover wash (it lightens under dark, never darkens); a leading icon sits one rung below the label (--muted), and on hover both label and icon step up to --color-text-strong, the max foreground tier. Selection keeps the accent pair (--color-accent-soft / --color-accent-hover); danger keeps its own colour.
Mutually exclusive short option groups, commonly used for 2–5 option switches such as "light / dark / follow system" or the four font-scale steps. Options may include a 14px registry icon or a colour swatch. A single raised indicator with a soft shadow (no border — the edge stays clean) slides and resizes between options using the standard motion tokens. Three sizes: md (default, settings pages), sm (compact rows), and xs (dense menus such as the composer model dropdown — 20px items, 12px labels).
Linked model + thinking-effort picker (settings → Agent → Subagents, experimental; components/settings/SecondaryModelPicker.vue, shared by both ends). Use it whenever two choices are only valid as a pair — here an effort is meaningless without its model, and every model declares a different supported set. It is a cascading variant of the §03 Select: the trigger is the Select trigger verbatim (value renders model · effort, the unset state uses the placeholder tint), and the dropdown opens as a SINGLE-LEVEL model list (grouped by provider) on the floating menu surface (--color-menu-bg / --p-menu-backdrop / --shadow-lg). The menu teleports to <body> with position: fixed — it opens on top of the settings modal (on the --z-modal-dropdown rung), and only a body-level surface escapes the dialog's scrolling-body clip; it re-anchors to the trigger on any outside scroll and closes on window resize (the UserMenu teleport's full recipe). Hovering or clicking a model row flies its effort submenu out to the RIGHT of the row — same menu surface, anchored to the row's live position, flipping to the left only near the viewport edge per the §03 anchoring rules — with a 250ms hover-intent grace (the UserMenu flyout's recipe) so the diagonal path into the submenu doesn't collapse it. Every model row carries a trailing chevron-right affordance; clicking an effort confirms the pair and closes — one atomic write, never two staggered patches. Flyout options follow the composer's thinking-level model (segmentsFor): effort models get off + their declared levels (always-thinking ones get no off), boolean-thinking models get on/off, unsupported models get off alone; while no effort is set at all, a "Model default" entry leads (it writes the model alone — POST /config merges and cannot clear a stored effort, so the entry disappears once one is set). A configured effort the model no longer declares is appended as an extra flyout option so the current pair stays visible and re-selectable. Keyboard mirrors the Select contract (focus stays on the trigger, Esc preventDefaults so the hosting dialog does not close): ↑/↓ move within the active level (the flyout follows model moves), → opens the flyout, ← collapses it, Enter confirms, Home/End jump. ARIA: combobox trigger → dialog menu holding a model listbox plus the effort listbox flyout with option rows. The menu itself flips upward when the trigger sits near the viewport bottom.
Tabs with a bottom hairline, used for grouping and switching sibling content. The current tab is marked with accent text + an accent underline.
A two-state switch for settings that take effect immediately. The 36×20 track has a 0.5px hairline and full radius; its 16px knob uses 1.5px internal offsets so the visible inset remains 2px and symmetric after accounting for the border. On hover, the knob eases to an 18px rounded rectangle towards the track centre. When on, the track turns accent and the knob slides right.
A 17×17 checkbox. When checked it fills with the accent color and shows a white tick (inline SVG). Often paired with a text label.
A 32px default avatar with md radius; .sm is 24px. Can hold an initial or an icon; falls back to this placeholder when there is no image.
A centered placeholder for empty lists / panels: a 48px faint icon + title + hint, avoiding blank pages.
A 0.5px hairline divider (--p-line); .p-divider-v is the vertical divider, used between inline elements.
A CSS-only hover hint, wrapped in .p-tip. Inverted background (--p-text / --p-bg), single line, no wrapping — carries only short notes.
An inline notice bar placed at the top of a content area. Three states — .info / .warning / .danger — each with a matching 18px icon.
A mobile bottom slide-up panel: xl top radius + drag handle, xl shadow. At ≤640px, dialogs become bottom-anchored Sheets.
A placeholder for loading content, using a breathing opacity animation (no gradients), following the no-gradient-text rule. Composed into titles / text lines / avatars.
An inline combination of "primary action + command text + copy", sitting between a button and a code block — used for install / onboarding / one-click execution. The primary action reuses Button primary; the command area uses a mono light-grey background.
The application top bar. Solid by default; the .frost variant is translucent + background blur, used only for sticky navigation bars. Together with the floating menu surfaces (Menu / Dropdown), it is one of the two exceptions to the no-glassmorphism rule (see §06).
The in-transcript find bar (Cmd/Ctrl+F), implemented by components/chat/TranscriptSearch.vue. A floating card pinned to the transcript's top-right (top: --panel-head-h + --space-3, right: --space-3 — equal inset on both axes), --z-sticky, raised surface + 0.5px hairline + --shadow-menu. One radius for both states: --radius-2xl is a full capsule at the collapsed height and a card once the footer expands — never animate between two radii.
| Part | Rule |
|---|---|
| Input row | Search icon (muted) + bare input — the list-style bare-input exception family (sidebar search row, inline rename), NOT the boxed Input primitive; the 38px bordered control would break the pill. Circular close IconButton sm (concentric with the capsule end); a 0.5px hairline separator before it. Height comes from the grid: 32px control (--space-8) + 2× --space-1 padding = 40px — at which --radius-2xl is exactly the half-height capsule. |
| Footer (results) | Expands via the 0fr→1fr grid fold (--duration-slow), hairline top separator, prev/next IconButton sm left, right-aligned muted count (N/M results · --ui-font-size-sm). Only exists once a query has settled — while typing or empty, the bar stays a bare pill. |
| States | collapsed (empty query) / searching (Spinner sm in the input row during the ~800ms debounce) / results / no-results (count reads "No results", nav disabled). Disabled is uniformly opacity:.5. |
| Focus | Composer-style: a neutral hairline overlay (::after + --color-composer-focus-line) fading in on :focus-within. No accent ring. |
| Match ink | CSS Custom Highlight API — the bar mutates no transcript DOM. All matches: --color-search-match (yellow); current: --color-search-match-current + a 2px --color-warning outline ring (a positioned overlay — highlight pseudos can't paint box outlines). Tokens live in app-ui/style.css with light/dark pairs. |
| Keyboard | Cmd/Ctrl+F opens + focuses (repeat = re-focus + select-all; hardcoded, reserved in the desktop keymap), Enter / Shift+Enter steps matches (wrapping), Esc closes from ANY control inside (container-level, so it never reaches the conversation's Esc-abort). |
| Matching semantics | Rendered transcript DOM only (unloaded older pages are out of scope), capped at 1000 matches (count reads N/1000+). Matches span inline nodes within one block, never cross block breaks; inert and display:none content is excluded. Stepping scrolls the match's own rect into view, not its parent element. |
A small group title for sidebar lists, used to section the content below (such as Workspaces in the sidebar). Spec: 13px / 700 / uppercase / letter-spacing .08em, color --color-fg-faint; left-aligned to the row's starting padding (--sb-pad-x), keeping the same indent as the group rows below. For scripts without case (such as Chinese), text-transform:uppercase simply has no effect — no special handling needed.
The message stream is the core of Kimi Web. Tool calls render as quiet activity lines — one borderless line per call, bespoke per tool kind, auto-grouped, expanding on demand — while Question / Approval elevate to a floating neutral surface because they need a decision, and the Swarm composite keeps a card; the Composer collapses into a single rounded container.
User-message bubbles follow the kimiwork production recipe (MessageItem .user-bubble): a neutral --color-user-bubble-bg fill (BubbleGray — #f5f5f5 light / #292929 dark), uniform --radius-lg corners, no border, no shadow.
Message timestamps use 12px UI text at weight 500, matching the compact metadata scale without switching to a monospace face.
The user-message metadata row sits one 8px spacing step below the bubble, so its actions and timestamp read as supporting information rather than part of the bubble edge.
Overlong user messages clamp at 10 measured lines, the tail dissolving through an alpha mask rather than a tint overlay (the translucent accent fill would double-composite); a floating pill toggle centred on the fade expands in place and collapses back, and the collapse pins the toggle itself so the reading position survives. Skill / plugin command args clamp through the same wrapper, beside the card head. Like the transcript's other disclosure controls (thinking row, turn fold, tool lines), the toggle is a bare native button carrying aria-expanded — chat-surface disclosure controls do not use the §03 Button primitive.
The floating jump-to-latest control uses 12px UI text at weight 525, led by the full down-arrow icon rather than a disclosure caret.
Thinking is an inline, borderless disclosure row in the message stream — never a side panel. The k15 bulb (the thinking registry icon) leads the row in every state; while streaming the "Thinking…" label breathes (opacity only, never a gradient shimmer) and whole elapsed seconds tick beside it, afterwards the label settles to "Thinking process" with the final span as · Ns (renderer-measured, live sessions only — history shows no seconds). Collapsed by default, it expands in place with the standard grid-rows animation and a 90° chevron rotation, and it folds itself back once the stream moves past it, even if the user expanded mid-stream. The header only animates its text colour on hover (standard duration and easing tokens), no card shell.
I looked at the structure of src/auth; it is currently based on a session cookie. The scope of the change is below — once you confirm, I'll start.
src/auth/middleware.ts, 42 lines changed. Allow?Wide markdown tables (desktop): regular chat prose stays within the 760px reading column (--p-content-max), and tables stay there too by default — an overflowing table scrolls horizontally inside its own wrapper, so the page and the chat area never scroll sideways. A clipped table shows a gradient fade at its truncated right edge, and hovering the table reveals a small widen button at its top-right corner; clicking it lets the table grow naturally with its content up to 1040px (--p-table-max), centred within the conversation pane, and clicking again restores the default width. At the default width a single column is capped at 36% of the pane; once widened the cap relaxes to 700px (--p-table-cell-max), so long cell content wraps inside the cell instead of stretching the table. The conversation outline (TOC) keeps its usual position just outside the reading column; when a widened table grows past it and scrolls under the rail, the TOC is hidden temporarily and returns as soon as the table leaves, without touching the user's TOC setting. On mobile a table never breaks out of the reading column.
High-frequency calls like read / bash / grep are "operational noise" — boxed, collapsible cards quickly drown out the conversation. Tool calls therefore render as one quiet borderless line in the message stream — never a card — and each tool kind composes that line for its own content, so the stream reads like an activity log rather than a pile of widgets. The three visual-weight tiers:
src/auth/middleware.ts, 42 lines changed.<button> carrying aria-expanded (keyboard path); the head itself is a plain click target (mouse path), so trailing slots may hold genuine buttons of their own (e.g. Agent's "open detail").--color-text — the one interactive place to go); the action label (Run / Read / Edit…), the mono command / pattern and secondary context all sit at --color-text-muted; auxiliary elements (glyphs, chevrons, trailing meta) stay --color-text-faint. The stream thus reads in three quiet tiers: prose in text, tool lines in muted, thinking / captions in faint. Line content is centre-aligned so mono-only rows (Bash) sit level with the icon and chevron. Truncating line content (the CSS-ellipsis spans) sets --leading-tight rather than the row's line-height: 1 — a 1em line box is shorter than the font's ascent + descent, so overflow: hidden would clip descenders (j / p / g / y); mono runs take the font's own normal leading instead, since JetBrains Mono's ≈1.32em metrics exceed --leading-tight. The 16px chevron still drives the ~24px row height.:line-range or a +N −M stat with a mini segmented bar; Grep shows the pattern in mono plus a match count; Glob / Ls list paths; Todo carries the active task with a done/total progress bar; goal tools show a coloured status pill; ExitPlanMode expands into a read-only plan receipt with its persisted review outcome. Unrecognized tools fall back to glyph + localized label + argument summary.--shadow-xs, flush with the stream's left edge, ≤560px). The card echoes only the picks, checked with the live QuestionCard's CSS glyph language one step down (14px); passed-over options are not echoed. Dismissed (or zero-answer) collapses to a slim italic one-line card; while running, and for unrecognized output (background launch / error), it stays the plain quiet disclosure line with the raw output.path:line opens the preview at that line). Code-bearing details — the Read content, the Edit diff, the Write content — are syntax-highlighted by file type (github-light / github-dark, following the colour scheme), with the Read output's real line numbers as the gutter; highlighting mounts lazily on first expand and degrades to plain text for unknown languages or oversized content.Read 2 files · Ran 5 commands (1 failed) · 26s), the failure clause hanging on its kind in danger red, the total span faint at the tail — one line, ellipsis-truncated, the full sentence in the title tooltip. Thinking items fold into the run but are not narrated in the sentence. The row shares the thinking row's language (borderless faint text row, text-colour hover only, one whole-row button with a rotating chevron) but rides a roomier 8px vertical padding — 30px against the quiet lines' 22px, so the turn-level summary keeps its presence between prose paragraphs; while the turn streams through the run the row stays expanded and the summary turns live (current action + cumulative per-kind stats + ticking whole seconds), and once every item settles it folds itself back — even if the user expanded it mid-run (the thinking block's vocabulary); a settled → running transition (the stream appending to the same run) reopens it. The glyph carries the state: the current step's own icon breathing while running, green ✓ / red ✕ once settled. A run needs ≥ 2 steps — a lone step renders standalone as the block it always was. Text never folds (it breaks the run), and neither do successful media tools (no card — inline media is the turn's output); everything else folds, cards included: Todo / Goal progress narration, the sub-agent identity card, Question / Swarm cards and unrecognized kinds (skills, MCP tools) all join the run — the stay-expanded-while-live rule keeps a card visible exactly while it is active. The expanded run is the items flat in order (thinking rows + tool rows), each with its own in-row details intact — the lines keep their own 4px row rhythm but breathe 8px apart, with a small inset below the head.TurnFold.vue): when an assistant turn settles, every block before the LAST text block — thinking segments, activity runs, interim text paragraphs, Todo / Goal / sub-agent cards — folds into a single bare row reading Worked 4m57s (whole seconds, no glyph, no summary sentence), expanding into the folded blocks in order, each with its own rendering intact. The span is the turn's ELAPSED time (turnWorkMs): it ticks from the stamped start while the turn is open — approval/question waits included by design, so no park bookkeeping exists — then reads the daemon's own durationMs once settled (the server message stamps for history turns); the wall clock only feeds the live tick, so throttled tabs, session switches and remounts cannot corrupt the settled value. Without any stamp the row falls back to the generic Work details. Streaming turns show no row and a forced-open body — the live transcript is untouched, the fold lands only when the stream moves past the turn (or the turn parks). The split never hides the turn's output: the final text block and any trailing blocks (inline media, standalone cards) stay visible, and a text-only turn folds nothing. Fold state is a plain component ref — nothing persists, switching sessions resets to folded. Inside the right-side sub-agent transcript (ChatPane's inspector mode), the turn fold and the run-end footer are suppressed entirely and activity runs and thinking blocks stay pinned open (their heads demoted to plain captions) — an inspection view exists to show the whole trajectory, so nothing folds away — and disclosure bodies open instantly while their chevrons retain the standard rotation: animating the height of a full historical stream would relayout the entire panel on every animation frame.Question and Approval — they genuinely need the user's attention. The Swarm composite keeps one quiet card (raised surface, 0.5px hairline, large radius) for its phase overview + member accordion.NotificationCard.vue): the hidden <notification> injections (background-task / sub-agent settlement) render where they landed in the turn — a 28px status chip + title/sub head tinted with the toast status token pairs (completed → success, failed / timed_out / lost → danger, killed → warning, else neutral surface), expanding in place to the fields, the body, an output-file row (copy path) and the raw payload. ≥2 CONSECUTIVE notifications merge into one neutral group card (count + per-item status dots + compact rows, each expanding on its own). Notifications break the activity run but are never turn boundaries, and they never fold — a notification is an event worth noticing, not process noise, so it punches out of the turn fold and renders right after the fold row, in order..turn-failed): the notification card's danger shell (danger-soft surface, danger hairline, 24px status chip with the warning glyph) carrying a title keyed by the wire error kind (model failure vs step-limit stop), the provider message as a muted sub, a mono diagnostics meta (code · HTTP status · request id), and exactly ONE secondary sm action — Continue, which submits a short continue prompt through the normal path. It renders only while the session sits idle on lastTurnReason === 'failed' (a turn with zero assistant output included, so it pins to the tail rather than any assistant row), it is not dismissible, and it vanishes the moment a new turn starts. While the turn is still fighting, the working indicator instead narrates the retry backoff ("retrying n/max" from the live agent.status.updated phase) — a retrying turn never shows the card. The transient error toast now fires only for background sessions; the viewed session's failure is fully covered by the card.goal_continuation trigger (goal mode's self-driven next turn — a turn boundary, unlike task notifications) never renders its machine prompt; instead the assistant turn it opens shows one faint 12px line flush with the stream's left edge — the target glyph shared with the Goal tool (this turn belongs to the goal) + a localized label — ABOVE the turn's content and OUTSIDE the turn fold, so the row survives as the turn's provenance after settling. The marker lands with the trigger (before the first assistant block), and while the newest exchange is a goal-continuation turn the undo affordances (edit-and-resend, Esc undo) are suppressed — rewinding would drop the hidden trigger while refilling the older user text.TurnFilesSummary.vue): between the turn's final text and its footer, a §03 Card (hairline border, no shadow — NOT the quiet tool line, the artifacts are worth a discrete object) lists every file the turn's Edit / Write calls touched. The head reads "N files changed" with the aggregate +A −D and the mini diffbar; the aggregate hides whenever any row's stats are incomplete (a Write or an underivable edit makes the total a lower bound, never presented as exact). Each row is one clickable workspace-relative path (short and self-locating; a file outside the cwd stays absolute) with its per-file +A −D at the right edge. The row's action keys on the tool kind, and the stats tell it apart: a Write has no per-file count (its diff is underivable) and opens the whole file in the preview; an Edit / MultiEdit carries its +A −D and opens that file's turn diff in the right-side detail layer (TurnDiffPanel.vue — the turn's own X→Y change, not the git diff), whose header keeps an open-file action. The first three files show inline; the rest collapse behind a "N more files" ghost-button row in the card's foot. Where nothing handles the row action (the BTW side chat), the card renders its file rows as plain text instead of links.The two attention cards replace the composer in the dock and share one contract: a floating neutral shell (--color-surface-raised + hairline + --radius-lg + --shadow-menu), a plain dark 16px title head, and a hairline footer whose actions read in number-key order with exactly one accent primary. There is no semantic colour band — the floating card itself is the "needs a decision" signal.
--p-chip-num over --color-inline-code-bg, the same chip vocabulary as option rows and the multi-step chip; exactly one primary action, the rest are ghost. Feedback mode swaps the whole footer for submit / cancel.Textarea primitive (no bespoke input), three rows at rest, auto-growing with its content UPWARD — the card is bottom-anchored in the dock, so the bottom edge and the submit / cancel footer never move — capped at 40% of the VISUAL viewport, then scrolling internally. Height budgets follow --app-height (the visual viewport, which shrinks with the iOS keyboard where dvh does not): the card caps at calc(var(--app-height, 100dvh) - 72px) and the dock takes over the same budget. While the feedback box grows, every body kind yields and scrolls internally by default instead of pushing the card past its cap — the plan scroll area, code previews, shell output, todo lists, invocation chips and generic text — with only the one-line plan path pinned at full height. A plan review's option rows belong to the plan region and never shrink, so once fixed chrome (options, grown feedback box) exceeds what the capped card leaves — e.g. the iOS keyboard shrinking the visual viewport — the WHOLE plan region (body and options together) becomes one scroller, keeping an approve option reachable by scrolling instead of clipped out of reach. Below even that budget — the cap under the fixed chrome alone (header plus mobile's stacked ≥46px submit / cancel buttons), as when an iOS landscape keyboard leaves a ~200px visual viewport — the body has already shrunk to zero and the CARD itself becomes the scroller of last resort (overflow-y: auto, with its own scroll seam), so the footer buttons are never clipped away.HighlightedCode (syntax-highlighted, 24-row cap with scroll); Edit approvals render the before/after hunk as a highlighted line diff. Plan / diff / file kinds get a head expand toggle that lifts the cap so the block fills the card; the card itself never exceeds the pane (only the scroll area shrinks) — with the dock work pills visible, the dock takes over the same height budget as a flex column, so an expanded card yields the pills' height instead of pushing them past the pane's top edge. Once the plan scrolls, a soft shadow fades in at the scroll area's top edge — the sidebar's scroll-linked seam language, so clipped content reads as passing under the card chrome.danger-soft filled hint row under the command — detection is a display-layer heuristic on the client.Unified into a single raised container: --radius-composer (32px) with --corner-shape-composer: superellipse(1.5) and a stable 0.5px edge. Focus crossfades a low-chroma line-and-accent edge over --duration-slow with --ease-in-out, while the neutral shadow stays unchanged — there is no added halo and no layout shift. The composer input (a ProseMirror contenteditable on desktop, a textarea on the web during the migration) uses text-autospace: normal for mixed CJK and Latin input. Toolbar controls use a quiet 32px full-round geometry with 8px edge inset; the send button remains a standard 32px circle, with its glyph at 28px (--composer-send-icon-size, the production kimi.com size; it sits outside the --p-ic-* scale on purpose).
Fill and edge tokens: the card's fill and rest border are their own tokens — --color-composer-bg and --color-composer-line — running the kimiwork / kimi.com production input recipe (.chat-input__shell): fill = groupedBackground.secondary (#ffffff light / #1f1f1f dark), rest border = separator.s1 (13% black / 12% white), focus line = fills.f4 (25% in both schemes), and --shadow-input = effect.shadow.inputDefault (0 5px 16px -4px rgba(0,0,0,0.07), kept identical in dark — the hairline carries the edge there). Only colours sit in the tokens; the 32px superellipse shape and the focus-only edge overlay are unchanged.
Send button tokens: the send circle runs on --color-send-bg / --color-send-bg-hover / --color-send-icon (+ *-disabled, --opacity-send-disabled, --shadow-send[-hover]), following the production recipe (.chat-input__send): a neutral labels.primary fill (90% black light / 84% white dark, hover #252525 / 84.8%) with the production lift shadow (0 7px 16px -13px 38% + 0 1px 2px 7%, one step larger on hover), a groupedBackground.secondary glyph, and a disabled state of the same vocabulary — fills.f2 fill with a labels.quaternary glyph at full opacity. The button is disabled exactly when submit would no-op — an empty draft with no ready attachment (image-only sends stay enabled), an upload in flight, or the starting spinner — so disabled is a first-class persistent state, never a fade.
Layering, anchors, and motion: the dock normally stays at --z-sticky so the Latest Messages pill can remain visible above its veil. While any Composer popup or work panel is open, the dock temporarily joins --z-dropdown, ensuring permission, work-mode, and model menus — and the work panel — always paint above that pill. The permission menu's left edge and the model menu's right edge each follow their own trigger pill. All three menus use --shadow-menu and the same trigger-corner pop motion as Session Row menus: 0.97 scale with a 2px shift toward the trigger, --duration-base on entry, and --duration-fast on exit.
Attachment strip: attachments hang inside the composer card above the input as two grouped rows — images/videos as shared MediaThumb rounded thumbnails, files as the shared AttachmentChip pill — the same pair the sent bubble renders, so a draft looks exactly like the sent message. File-store videos render a static play tile instead of fetching a first frame. The strip caps at two thumbnail rows and scrolls beyond that instead of pushing the input down; while overflowing, a quiet count badge pins to the bottom-left and new attachments auto-scroll into view (to the end of whichever group grew). With two or more attachments, a one-click clear-all pins to the strip's top-right corner as a quiet 22px badge (trash glyph, danger on hover). The composer's pending preview and the bubble's media clicks open the same MediaLightbox preview, which owns Escape via the shared dialog stack: images go through PhotoSwipe (@moonshot-ai/app-client's lib/mediaPreview) and zoom out of the clicked thumbnail (scrim = --color-scrim-strong, caption = --color-text-on-scrim; the slide area is inset — 24px sides matching the video modal, 56px top/bottom clearing the close button and caption — so a viewport-filling image never kisses the edges), videos keep the custom modal. Both share the --color-scrim-strong backdrop and the same close button — the raised 36px circle (.media-lightbox-close) fixed at the viewport's top-right, rendered by MediaLightbox for both (PhotoSwipe's own top bar is disabled; zoom stays on wheel / pinch / image click). ReadMedia tool cards open it too (an App-level instance fed by the openMedia chain): the image zooms out of the card's thumbnail, and videos show as a static play tile that opens the modal player — no more right-side-panel detour or inline <video>.
Mention pills: @-mentions render as inline pills in the editor — one element of the cross-surface rich-text vocabulary specified in §05 Rich Text Messages (visual recipe, wire forms, and behavior contract live there).
',12)),t("div",ga,[a[38]||(a[38]=t("div",{class:"stage-bar"},[t("span",{class:"st"},"Composer")],-1)),t("div",ma,[t("div",fa,[a[36]||(a[36]=t("div",{class:"p-composer-ta ph"},"Message Kimi, / to run a command, @ to reference a file…",-1)),t("div",wa,[t("div",ya,[a[33]||(a[33]=t("button",{class:"p-icon-btn"},[t("svg",{class:"p-ic",viewBox:"0 0 24 24",fill:"currentColor"},[t("path",{fill:"currentColor",d:"M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"})])],-1)),t("span",ka,[s(d(o),{name:"shield-question",size:"sm"}),a[32]||(a[32]=e("yolo",-1))]),a[34]||(a[34]=t("span",{class:"p-pill"},[t("svg",{class:"p-ic",viewBox:"0 0 24 24",fill:"currentColor"},[t("path",{fill:"currentColor",d:"M8 4h13v2H8zM4.5 6.5a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3m0 7a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3m0 6.9a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3M8 11h13v2H8zm0 7h13v2H8z"})]),e("plan")],-1))]),a[35]||(a[35]=c('--color-text fill with a --color-bg glyph (never the accent), disabled while the input is empty or an upload is in flight. The transparent dock floats over the transcript, while the scrolling content receives bottom padding equal to the live dock height so its final item can still clear the composer. Composer chrome is not selectable; only the message input permits text selection. Each permission mode has its own registry icon — manual hand, yolo shield-question, auto full-access — paired with the label in the pill (collapsing to the accessible icon below a 620px composer container) and leading its dropdown row in the mode's colour, with the current row's check trailing the row's end. The right toolbar is the flexible region: the model pill shrink-wraps its content, then shrinks and truncates internally only when the toolbar runs out of room. The dock's work pills and panels — pill vocabulary, panel shell and motion, the two-tone head, per-kind bodies, filtering, and the open/cancel model — are specified in Dock · work pills & panels below. Workspace attachment card: on the empty session, the workspace picker is a separate attachment card tucked under the composer — and the composer card itself stays complete (its own 0.5px border, --radius-composer corners with --corner-shape-composer, and shadow are never altered). The attachment lives inside the composer's padding box as the card's sibling, so its width always matches; its top --space-4 slides behind the card (the card is raised to --z-sticky), its square top edge stays hidden, and only the rounded bottom (0 0 --radius-xl --radius-xl) shows. Background --color-hover at 60% via color-mix (≈0.03 black in light, self-adapting in dark), no border, no shadow. Inside sits one quiet capsule trigger: transparent, --radius-full, 16px leading icon and 12px label at weight 475 in --color-text-muted; hover deepens to --color-selected and the label turns --color-text. The dropdown follows the §03 menu spec and is viewport-aware (flips above when more room, clamps max-height to the scrollport); at --z-dropdown it outranks both the card and the fixed click-outside backdrop (--z-sticky), which renders outside the composer because the card's container-type captures position: fixed descendants.
The slash, mention, and add popups share one geometry, all on dedicated tokens: the --color-menu-bg-frost surface (the frostier recipe), frame padding --menu-row-hug, rows at --menu-row-padding-block × --menu-row-padding-inline with --radius-menu-row caps (plain corners — the frame is --radius-lg with NO corner-shape, and the row radius stays concentric: 12px frame − 6px hug = 6px), an icon-to-label gap of --menu-row-gap-icon, and a --menu-rows-seam between stacked rows. Touch rows pad to --menu-row-touch-padding-block with a hard floor of --touch-target-min. Scroll height caps at --p-slash-menu-h / --p-mention-menu-h / --p-add-menu-h; the scroll-edge fade is --menu-scroll-fade, and the overlay thumb rides --menu-scrollbar-width / --menu-scrollbar-edge / --menu-scrollbar-track-inset / --menu-scrollbar-thumb-min in --color-menu-scrollbar (hover --color-menu-scrollbar-hover) — 3px visually, with a wider invisible drag strip.
The composer's + button opens the add menu — the autocomplete family's action-list member: one column of icon + label (+ muted description) rows (Files, Goal, Plan, Swarm) on the same frost surface and row geometry as the slash/mention popups, capped at --p-add-menu-h. Semantically it is an action menu, not an autocomplete listbox: rows are menuitem commands, DOM focus moves into the menu (arrows navigate, Enter activates, Escape closes), and the + button carries aria-haspopup="menu" — the textarea's combobox ARIA never points at it.
Design decision: deliberately NOT the §03 Menu/MenuItem primitives. Those are the trigger-dropdown family (sidebar, user menu, session rows) — --color-menu-bg, --radius-lg, a min-width box. The add menu instead shares the composer menus' material (the frostier surface, the composer corner curve, rows hugging the frame at the composer's text column, muted description sub-lines). Bending MenuItem into that material would require exactly the per-screen appearance overrides the primitive contract forbids, so the add menu keeps bespoke rows built directly on the shared --menu-row-* tokens. This paragraph is the canonical record of that choice — reviews should not re-litigate it.
Plan and Goal are the primary work modes — mutually exclusive, at most one armed at a time. Arming happens only through the slash commands (/plan, /goal) or the add menu; an armed mode renders as the input row's leading pill — a neutral --color-surface chip with the mode's 14px icon, its label, and a × that disarms. The × is an IconButton sized --wm-x-size (below the sm default, so its hover wash never outgrows the pill's rounded end) with a --wm-x-ring hit reserve that stays inside the textarea's indent; on touch it meets --touch-target-min. The input's first-line text indent reserves exactly the pill's width (the desktop ProseMirror editor applies it to the first paragraph), so the caret and placeholder slide right rather than colliding; arming Goal also swaps the placeholder to its objective prompt. Sending with Goal armed creates the goal and the pill hands off to the dock's goal pill; a live goal does NOT lock the primary modes — /goal then just focuses the goal's panel, and /plan still arms (the only hard rule: one message cannot both create a goal and enter plan — the goal write carries the plan disarm atomically — and a failed mode write is treated as a send failure: the optimistic message rolls back, the error toasts). Swarm is deliberately NOT a mode: an orthogonal toolbar chip with its own enable confirmation, disarmed from the chip's ×.
Work pills (WorkPill.vue): the dock's workbar above the composer carries one pill vocabulary — font-driven by flex computation: the 1.5em leading icon and the label's own line box (--text-base at --leading-normal) both come to 21px at the 14px UI text, and 8px block padding wraps them to 37px, nothing pinning pixels, so the pills rescale with the font; borderless, with --radius-lg rounded-square corners, filled at the fills ladder's --color-selected rung over the shared --p-menu-backdrop blur — the panel's frosted recipe scaled down to a chip, so transcript text never reads through the light fill (the neutral hover wash layers on top — one layer, never two), the icon at full text colour, --space-2 / --space-3 block / inline padding — the trailing side adds a --space-05 optical compensation against the leading glyph — and a --space-1-5 content gap. Every pill stays expanded — the Code client does not collapse status chips — carrying icon + label and a trailing meta that answers one question — what is live on this surface right now — for background bash tasks, background sub-agents, todos, and the goal alike; the row insets to the composer's text column — dock inline inset + the card's 0.5px border + the input wrap's 16px inline padding — not the card edge. The active pill keeps the wash on permanently — one fills-ladder step deeper, neutral. The meta is always muted ink and surface-specific: the goal carries its status word, colour-coded by state (active --color-success, paused --color-warning, blocked --color-danger); the bash and sub-agent pills carry a pulsing dot and the running count, only while something runs (a quiet absence otherwise); the todos carry the done/total fraction. The plan pill joins the row only once plan mode is live server-side or a persisted plan exists — a merely armed directive stays in the composer's inline work-mode pill (its × cancels) and never reaches this bar, so every pill here reads as live server truth rather than local intent. Narrow panes wrap the row instead of clipping (the dock height observes); below the --p-bp-sm breakpoint the pills collapse to their icons — label and meta hide (the threshold is read from the token, never a hardcoded width).
Panel shell & motion: a pill toggles the shared work panel — the menu family material: 70% page background via color-mix over the shared --p-menu-backdrop blur (the frostier recipe, see §06 Glassmorphism exemption), a 0.5px --color-line edge, --radius-2xl, and the menu panel's --shadow-menu. The panel pops from the clicked pill along the trigger-corner motion tokens (--motion-panel-scale / --motion-panel-shift), and switching pills replays the pop from the newly clicked pill (the shell is keyed by panel kind); while it is open the dock raises to --z-dropdown so it clears the transcript's new-message pill, and every window-drag strip pauses so an outside press reaches the page and dismisses. The panel owns Escape while open (a document-capture handler guarded by the shared IME latch, so a candidate-cancelling Escape is never swallowed), and a scrolled body dissolves toward the head through the --menu-scroll-fade alpha mask instead of hard-clipping. Height is content-sized up to min(360px, 50vh); the filtered panels pin instead — see Filtering & sizing. The panel is chrome: nothing inside it is text-selectable, the head's filter chips included.
Panel head (WorkPanelHead.vue): every work panel head is one tab row — a leading icon at the pill's 1.5em glyph size, the panel title at full text colour, and a muted trailing meta (--color-text-muted) carrying that panel's one live number, joined by the row's --space-2 gap with no separator glyph: the goal's wall-clock time, the bash / sub-agent running count, the todos' done/total. Actions right-align in the head: the goal's pause / resume / cancel / close as quiet neutral IconButtons (cancel deliberately shares that neutral vocabulary), the plan's open-in-side-panel, dismiss-directive (shown only while the directive is live), and close, and the bash / sub-agent filter chips as a SegmentedControl. The plan head's meta carries the latest plan's review outcome. On touch these meet the 44px minimum (--touch-target-min) via the hover: none capability query — a width-only gate would miss tablets — and below 480px the head wraps so the actions take a full row.
Panel bodies: todos read as a quiet list — a green circle-check for done, a hollow ring for pending, an xs Spinner for in-progress. The goal's detail (full objective, completion criterion, rendered with the chat Markdown renderer) fills the body — no footer strip. Bash tasks are long rows (TasksPane.vue): a StatusDot while running, circle-check when done, a close glyph for failed and for cancelled (a user stop is neutral, never reported as a failure), the command as the meta line, and a bare duration that renders only when computable — never the raw protocol status word. Sub-agents form a card grid (SubagentGrid.vue) — an auto-fill grid (minmax --p-subagent-card-min) of cards at the --color-selected rung with --radius-lg corners; each card carries the task name, a stable session-wide number (creation order across the session's background sub-agents — unique even across swarms; once shown it sticks, so late-arriving history takes the next tail number), an optional prompt meta line one size down (--text-sm), an icon-led model · effort line and the status row a further size down (--text-xs) — the status row pairs the task-row state glyph with the localized label, plus a clock-led bare duration that renders only when computable; the full-bleed circle-check (drawn for the todo rows' ring family) is scaled onto the shared icon grid wherever it sits beside other glyphs (task rows, cards, filter chips). A failed row tints its name --color-danger; a cancelled one stays neutral. Rows hover with a rounded strip that grows by padding and a matching negative margin — the row metrics never shift. The row stop is danger-coloured; the card cancel reveals on hover and is always visible on touch as a --touch-target-min corner target. Every pane's empty state is centered and muted. State and time labels — the head meta included — set text-autospace: normal, the transcript's mixed CJK/numeric spacing rule, so durations like 9小时2分 breathe.
State vocabulary: two glyph families meet here by design — the activity dot (shared with the transcript's tool rows and swarm members) backs the task rows, the todo rows speak the default loader ring, and the sub-agent cards skip glyphs in favour of a localized text label:
',8)),t("table",Ba,[a[83]||(a[83]=t("thead",null,[t("tr",null,[t("th",null,"Surface"),t("th",null,"State"),t("th",null,"Meaning"),t("th",null,"Glyph"),t("th",null,"Ink")])],-1)),t("tbody",null,[t("tr",null,[a[48]||(a[48]=t("td",null,"Bash rows",-1)),a[49]||(a[49]=t("td",{class:"tk"},"running",-1)),a[50]||(a[50]=t("td",null,"Task in flight",-1)),t("td",null,[s(d(h),{status:"running"}),a[47]||(a[47]=e(" pulsing dot",-1))]),a[51]||(a[51]=t("td",null,[t("code",null,"--color-accent")],-1))]),t("tr",null,[a[54]||(a[54]=t("td",null,null,-1)),a[55]||(a[55]=t("td",{class:"tk"},"done",-1)),a[56]||(a[56]=t("td",null,"Finished cleanly",-1)),t("td",null,[s(d(o),{name:"circle-check",size:"sm"}),a[52]||(a[52]=e()),a[53]||(a[53]=t("code",null,"circle-check",-1))]),a[57]||(a[57]=t("td",null,[t("code",null,"--color-success")],-1))]),t("tr",null,[a[60]||(a[60]=t("td",null,null,-1)),a[61]||(a[61]=t("td",{class:"tk"},"failed",-1)),a[62]||(a[62]=t("td",null,"Errored — the row's name tints to match",-1)),t("td",null,[s(d(o),{name:"close",size:"sm"}),a[58]||(a[58]=e()),a[59]||(a[59]=t("code",null,"close",-1))]),a[63]||(a[63]=t("td",null,[t("code",null,"--color-danger")],-1))]),t("tr",null,[a[66]||(a[66]=t("td",null,null,-1)),a[67]||(a[67]=t("td",{class:"tk"},"cancelled",-1)),a[68]||(a[68]=t("td",null,"User stop — neutral, never a failure",-1)),t("td",null,[s(d(o),{name:"close",size:"sm"}),a[64]||(a[64]=e()),a[65]||(a[65]=t("code",null,"close",-1))]),a[69]||(a[69]=t("td",null,[t("code",null,"--color-text-muted")],-1))]),t("tr",null,[a[71]||(a[71]=t("td",null,"Todo rows",-1)),a[72]||(a[72]=t("td",{class:"tk"},"in_progress",-1)),a[73]||(a[73]=t("td",null,"Being worked on",-1)),t("td",null,[t("span",Ma,[s(d(I),{size:"xs"})]),a[70]||(a[70]=e(" xs Spinner ring",-1))]),a[74]||(a[74]=t("td",null,[e("the row's own ink ("),t("code",null,"--color-text"),e(")")],-1))]),a[81]||(a[81]=t("tr",null,[t("td"),t("td",{class:"tk"},"pending"),t("td",null,"Not started"),t("td",null,[t("span",{class:"dw-ring"}),e(" hollow ring")]),t("td",null,[t("code",null,"--color-line-strong"),e(" stroke")])],-1)),t("tr",null,[a[77]||(a[77]=t("td",null,null,-1)),a[78]||(a[78]=t("td",{class:"tk"},"done",-1)),a[79]||(a[79]=t("td",null,"Completed",-1)),t("td",null,[s(d(o),{name:"circle-check",size:"md"}),a[75]||(a[75]=e()),a[76]||(a[76]=t("code",null,"circle-check",-1))]),a[80]||(a[80]=t("td",null,[t("code",null,"--color-success")],-1))]),a[82]||(a[82]=t("tr",null,[t("td",null,"Sub-agent cards"),t("td",{class:"tk"},"all"),t("td",null,"The task-row glyph plus a localized label — failed alone tints danger — with a clock-led bare duration trailing"),t("td",null,"glyph + label"),t("td",null,"muted / danger")],-1))])]),a[140]||(a[140]=c("Filtering & sizing: the bash and sub-agent heads carry the same four icon-led filter chips — recent (clock; running + the five most recently finished), running (play), done (circle-check; every terminal state), all (list) — and both filtered panels pin to --p-dock-panel-h so filtering never resizes the panel; the body scrolls inside. When the head runs out of room the chips collapse into a dropdown menu (FilterControl.vue measures the head itself, so the switch follows the panel's own width, not the viewport; the menu teleports to <body> and anchors to the trigger, so the panel's clip and backdrop-filter never squeeze it); the head title never wraps and is never clipped — once the filter is a dropdown the head always fits. An empty pane names its filter (no completed tasks is not no tasks).
Opening & cancelling: clicking a row or card opens the task's detail in the right-side panel. Opening is gated: the overlay navigation button renders only when a stable agentId (on-demand transcript) or locally held output exists, so a REST-only cold-loaded row stays inert instead of opening an empty panel — and says so with the not-allowed cursor. Open and cancel are sibling controls (a full-cover overlay button, the stop IconButton floating above it): no nested interactives, and the stop action exists only while the task runs.
Structured references travel as plain text on the wire and render as pills at both ends: the composer's editing surface and the rendered message stream share one pill vocabulary, so what you type is what the message shows. This section is the category spec — every future rich-text element (marks, embeds, interactive chips) joins it here.
The mention is the first rich-text element. It exists in two synchronized forms: an atom inside the desktop composer's ProseMirror document, and a pill in the message stream — assistant Markdown decorates local links with the same classes, and user/queue bubbles (verbatim wire text, never Markdown) render through the declarative ComposerText component. Both are the same mark: not a filled chip — no background, no vertical padding — just body text in a heavier weight (--weight-ui-strong) on a lighter ink (--color-text-muted), so the baseline stays flush with the surrounding text; a 2px horizontal inset (padding-inline: var(--space-05)) supplies the CJK/Latin autospacing that can't cross element boundaries and sets the mention apart from plain text; a 13px muted kind glyph leads, and the label is the basename — never the full path, which lives on the tooltip. Hover deepens the ink (glyph included) toward --color-text on every surface; in the message stream the clickable kinds (file, skill) also take the pointer cursor and the link underline, while the composer keeps pure editing semantics — I-beam, no underline, a click just places the caret. The shared classes live in app-ui's global sheet; the kind → glyph mapping is single-sourced in app-composer (mentionIcons) and also drives the mention menu rows, so a file looks identical in the menu, the editor, and the sent bubble. Copying from a bubble re-serializes the selection back to the wire text — pills carry their full attrs in data-mention-*, so a copy/paste round trip restores the link instead of a truncated basename.
Kinds and wire forms (serialization is a Markdown link, so the daemon payload stays plain text and the TUI needs nothing new):
| Kind | Glyph | Wire form | Click (in messages) |
|---|---|---|---|
| File | single folded-corner file glyph for every file (no per-extension variants) | [name](path) — the dest is canonically encoded per path segment (every non-unreserved ASCII character → %XX; non-ASCII stays literal, so a CJK path reads as itself — one decodeURIComponent restores it on every surface) | opens the file preview |
| Folder | folder glyph | [name](path/) — trailing slash marks the kind (dest % → %25 as above) | inert (no target yet) |
| Skill | sparkling glyph | [name](kimi-code://skill/<name>) — the app's deep-link protocol | opens the skill's SKILL.md in the preview panel |
Hover tooltip: one document-level singleton (mentionTooltip) serves every pill — composer NodeViews, ComposerText-rendered bubbles, and Markdown anchors are all raw DOM a Vue wrapper can't reach, so it delegates on document mouseover into a single shared bubble. The bubble keeps the design-system tooltip's dark skin but is interactive. File and folder pills show the full path, wrapping anywhere within a fixed max width: every / separator muted, the basename bold (--weight-semibold). Skill pills show a card — the name with an open button at the right (opens the skill's SKILL.md in the preview panel; the path rides the wire skill descriptor through AppSkill.path), the description below, clamped to four lines; an unresolvable skill degrades to the name alone. Timing mirrors TooltipBubble (150ms show delay, top placement with flip, viewport clamping) and the bubble stays open while hovered so the button is reachable; native title tooltips are removed wherever a pill appears. The bubble is a documented structural exception to the component-primitive rule (like the dock overlay): its anchors are ProseMirror NodeViews and pillified spans a Vue wrapper can't reach, so the open/copy buttons re-implement the Button primitive's contract (size, hover, :focus-visible ring) by hand instead of importing it.
Edge cases: labels cap at 32 chars — a longer name takes a middle ellipsis that keeps the head of the base name and the whole extension (the full name stays in the data attributes, the full path on the tooltip). A pill whose target was deleted after the fact: hovering fires a one-byte existence probe (an inline spinner sits at the tail of the tooltip's path text while in flight), and a definitive not-found fades the pill and strikes it through — in messages and in the composer alike. Clicks are never gated on the verdict (the preview's own not-found state is the final answer); only confirmed-existing verdicts are cached, scoped to the session, so a recreated file recovers on the next hover and a flaky daemon can never strike a pill by mistake.
Behavior contract: a bare @ opens the menu instantly with the workspace root listing (Esc dismisses); with a query, the menu groups Files (debounced daemon search) above Skills (instant local filter), captions shown only when both exist, and an in-flight search never hides the current rows (a corner spinner marks it). Full-width @ from IMEs triggers identically. Insertion replaces the @token and adds a separating trailing space; the pill is one atom — Backspace removes it whole, and a zero-width caret anchor keeps the caret on the pill's line when it ends a paragraph. Drafts, history recall, and queue reloads revive pills from their link form on load, and pasting mention-link text (e.g. a copied pill) revives them too — the serializer round-trips both ways. Skill activation: sending a message with exactly one skill pill activates that skill via the existing channel (the pill form of /skill:<name>, the full text — the pill traveling as its mention link — becomes the args, attachments ride along), and the sent bubble shows the original message verbatim with the pill revived in place (a slash-typed activation, whose bare args carry no pill, keeps the identity card instead); two or more skill pills degrade to plain references, because each activation is its own turn.
Implementation map: schema/serialization/offset mapping in app-composer's composerTextDoc (pure, node-tested); the editor surface in composerEditor; user/queue bubbles render via app-composer's ComposerText (one segment pass, declarative tree — no post-processing); assistant-side classification (classifyMentionHref) and decoration in app-markdown's Markdown.vue link pass; hover + skill-click routing in app-composer's mentionTooltip singleton, wired per app shell. When editing these, keep the two surfaces in lockstep — a pill that serializes one way in the composer must read the same way in a message.
Kimi Web uses one unified theme: the same components, fonts, radii, shadows, and surfaces — theming only swaps color values. Every semantic color token ships a light value in :root and a dark override in the data-color-scheme blocks; the semantic status colors (success / warning / danger) are independent palettes, one set each for light / dark.
The app has one accent: the brand blue (--color-accent, #1783ff light / #58a6ff dark). Use it sparingly — the accent is reserved for the primary action, focus rings, links, and active marks (current tab, toggles); large fills always come from the neutral surface tokens. Selection that means "where I am" (sidebar rows, list pickers) is deliberately NOT accent-tinted — it uses --color-selected so it reads as location, not as an action.
Each semantic token ships a light value in :root and a dark override in the two data-color-scheme blocks (explicit choice, or following the OS preference via prefers-color-scheme). Switching light / dark simply swaps between these two sets of derived tokens, with zero structural change.
Anti-pattern rules that all UI code must follow. These rules are also the basis of the check-style detection script, one-to-one with a warning.
| Rule ID | What it detects | Action |
|---|---|---|
| no-gradient-text | gradient text / gradient background | Forbidden |
| no-glassmorphism | backdrop-filter: blur (TopBar sticky nav bar and menu surfaces via --p-menu-backdrop are the exceptions) | TopBar + menus exempt |
| no-color-glow | colored / large-radius box-shadow glow | Forbidden |
| no-emoji-icon | using emoji as a functional icon (no exceptions). Emoji inside user content — session titles, messages — is not chrome and is out of scope (see §07 Session row's emoji icon) | Forbidden |
| no-hardcoded-hex | unregistered hex color inside a component <style> | Warning |
| no-hardcoded-font | hard-coded font-family in a component (e.g. 'Inter') instead of var(--font-ui) | Warning |
| radius-from-scale | radius value not in {4,6,8,12,16,20,999} | Warning |
| z-from-scale | z-index using an unregistered large number | Warning |
| weight-from-scale | font-weight not in {400,500} | Warning |
Every interactive primitive should define the following states where applicable; missing ones are flagged by the style rules. focus-visible always uses --p-focus-ring (appears only on keyboard focus, see §08); disabled is uniformly opacity:.5.
| State | Button | Input | Card | Menu item | Switch |
|---|---|---|---|---|---|
| default | ✓ | ✓ | ✓ | ✓ | ✓ |
| hover | ✓ | ✓ | ✓ | ✓ | — |
| active / pressed | ✓ | — | — | — | — |
| focus-visible | ✓ | ✓ | — | — | ✓ |
| disabled | ✓ | ✓ | — | ✓ | — |
| loading | ✓ | — | — | — | — |
| selected / active | — | — | — | ✓ | ✓ |
| error | — | ✓ | — | — | — |
| readonly | — | ✓ | — | — | — |
WorkingIndicator component: the 小蓝 mascot plus a phase label — "Requesting…" until the assistant's reply starts, "Working…" once it is streaming. All other loading states (including ActivityNotice) use the plain Spinner. backdrop-filter: blur is banned site-wide, with two exceptions: the .frost variant of TopBar — only in the one place of the "sticky navigation bar", used to stay readable over scrolling content — and the floating menu surfaces: Menu.vue, the Select listbox and the composer dropdowns use the --color-menu-bg token (a 95% surface), while the autocomplete family — slash/mention popups and the dock work panel — deliberately runs a frostier recipe of its own: 70% page background (single-sourced as --color-menu-bg-frost) over the shared --p-menu-backdrop blur, which stays the single-sourced blur token across all of them. The dock's work surfaces — the work panel and its pills, persistent over the scrolling transcript — ride that same frostier recipe (specified per-surface in §04 (Dock · work pills & panels)) rather than a bespoke one. No other component (card, dialog, Toast, panel) may use glassmorphism; violations are flagged under no-glassmorphism, and menu blur with ad-hoc values (anything but the token) is flagged too. The structural spec for the app shell (three-column grid + right preview panel) and the left session sidebar. These are business-agnostic "skeletons" — components, fonts, radii, and surfaces are reused from §02 / §03, but layout and alignment have their own conventions.
On web it is a single-row 5-track grid: the sidebar and the right panel each occupy a permanent auto track, with the conversation column in the middle; two 0-width tracks are for the ResizeHandles. (The desktop app adds a second row for its terminal panel — desktop-only, see below.)
grid-template-columns: auto 0 minmax(0, 1fr) 0 auto;\n /* sidebar ↑ ↑handle ↑conversation ↑handle ↑right panel (auto) */
| Token | Value | Usage |
|---|---|---|
| sidebar width | 270px default (adjustable) | expanded sidebar width, changed by dragging the ResizeHandle; should approach §02's --p-sidebar-w (264px) |
| --preview-w | 460px | width of the right preview panel when open |
| --panel-head-h | 48px | unified height for all right panel heads + the conversation column head; both use a 0.5px bottom hairline |
| --p-bp-sm | 640px | ≤640 switches to a mobile single column (top bar + conversation), no sidebar / handle / right panel |
0 ↔ var(--preview-w) and no transition — animating a grid track would relayout the whole app grid every frame (when open it squeezes the conversation column, rather than switching templates).min-height:0; min-width:0, so only the inner scroll containers scroll and the page itself does not scroll.--sb-*)All sidebar rows (group head, session row, New chat, search, and Settings buttons) share 4 custom properties. Their 16px icon slots and --sb-gap place every label on the same x-axis as the workspace name.
| Token | Value | Usage |
|---|---|---|
| --sb-inset | 12px | row box (hover/selected pill) inset from the sidebar edges — matches the brand header's 12px padding |
| --sb-pad-x | 20px | content start x (= --sb-inset + 8px row padding) |
| --sb-gutter | 16px | leading icon slot width — matches the workspace folder icon so the session title aligns under the workspace name |
| --sb-gap | 8px | gap between the icon slot and the text |
--sb-pad-x + --sb-gutter + --sb-gap. The group head has a folder icon and the session row has a status slot; both icons are the same width and position, so the titles align naturally. The sidebar from top to bottom: brand header → action group → pinned head (pinned section + "Workspaces" label) → scrolling grouped list (workspace head + session rows) → user-menu footer. New chat and Search are direct sibling controls in the same grid container; the optional new-workspace action shares the first row, while Search spans the next row. A 4px gap keeps Search clear of the scroll boundary. The pinned head sits OUTSIDE the scroll container (the action-group / footer pattern — never position: sticky, which would need an opaque plate over the frosted tint), so the pinned sessions and the "Workspaces" label stay put while the workspace groups scroll beneath; the pinned section is collapsible (a chevron on its label, revealed on hover/focus and kept visible while folded; state persisted) so a long pinned set can't eat the sidebar, and it re-expands when a new session is pinned. Both pinned edges use three light near, middle and far fades across 18px, entering over 260ms only while more session content exists beyond that edge — the top seam lives at the pinned head's bottom border. The footer seam is a 0.5px hairline. Controls reuse the §03 primitives as much as possible. The sidebar sits on --color-sidebar-bg (one step off --color-bg: warm off-white just under white in light, one step BELOW the page in dark — the session column reads as its own plane, and with dark elevation = lighter the chrome never sits brighter than the conversation pane; the hairline still separates it from the pane). Vertical rhythm: the brand header keeps 12px padding (on macOS desktop the left padding grows to 80px to clear the traffic lights); rows inside the action group stack flush (0 gap, same rhythm as the list rows); adjacent groups are separated by 12px. The search glyph has a -0.5px optical correction to align its visual centre with the label. Row hover uses --sb-hover (= the global --color-hover wash); the selected row uses the lighter --sb-selected wash derived from --color-selected — On macOS desktop the sidebar is instead frosted: the window carries a native NSVisualEffectView ('menu' vibrancy, following the in-app scheme via the nativeTheme mirror, its state pinned to inactive so the material keeps its flat pressed-down colour — ≈ #282829 dark / #E7E7E7 light — with no active/inactive drift) and the sidebar column drops --color-sidebar-bg for a single translucent --color-sidebar-tint wash that presses the pinned material one step — ≈ #282829 → ≈ #1e1e1f in dark (rgba(0,0,0,0.25)), ≈ #E7E7E7 → ≈ #f1f1f1 in light (rgba(255,255,255,0.4)) — with header and footer staying transparent so the tint reads as one uniform pane; the root chain (html/body/#app/.app) stays unpainted only under the macos-desktop + vibrancy flags — the latter is the Settings → Appearance accessibility switch (default on; persisted main-side so the window is created with the right material, and live-applied on toggle): off repaints the root chain and the sidebar falls back to opaque --color-sidebar-bg, while the traffic-light layout keeps keying off macos-desktop alone — while the conversation pane, chat header and right preview keep their own opaque surfaces. The list's hover-icon clusters (session-row kebab, group-head actions) paint NOTHING there — no plate, no wash, no blur (real backdrop blur does not even render over this window: Chromium's backdrop sampler returns a flat wash above the transparent BrowserWindow + vibrancy view). Instead the row's title/name dissolves before it ever reaches the buttons: a two-stage mask-image fade — a subtle 16px dissolve at rest, extending over the cluster zone only while the actions are revealed (row hover / keyboard focus / menu open): 34px on session rows (the pin+kebab cluster overhangs the title by ≈25px), 68px on group heads (the floating cluster is ≈60px wide). The fade is zone-based, so short rows render untouched, and text-overflow becomes clip so a long tail dissolves instead of dotting.
| Block | Use | Note |
|---|---|---|
| Brand header | logo + name + collapse IconButton (right-aligned) | on Windows / web the brand is left and the collapse IconButton sm is right-aligned inside the header; the dev-only backend version/address pill uses the UI font, not monospace; the logo is animated (a blinking eye). On macOS desktop the header is a bare drag strip (brand hidden, traffic lights + resident floating toggle over it) |
| New chat | full-width left-aligned button (custom) | 500-weight label; same rhythm as the session rows in the list (left-aligned, hover = --sb-hover). Do not use Button (centered, breaks the rhythm) |
| Search | bare search row (custom) | 500-weight label; no border, hover/focus shows the faint --color-hover wash; icon + label, with the Kbd keycaps (⌘K / Ctrl K) pushed to the trailing edge — label and shortcut are justified apart. Do not use Input (the 38px bordered version is too heavy). It is a direct sibling of New chat in the action group |
| Section label | .p-section-label | uppercase muted small titles like "Workspaces", using --weight-section-label (600) |
| Pinned head | fixed block above the scroll container (.sessions-head): the pinned section (PinnedSessionList.vue) + the "Workspaces" section label | stays put while the workspace groups scroll; owns the top scroll-linked seam (hairline + fade, only while scrolled). Pinned rows render in pure recency order (updatedAt desc — no manual ordering, no attention tiering): drag a session row in to pin it (the drop spot carries no position meaning), drag a pinned row out to unpin. The pinned section folds via its label chevron (persisted, kimi-web.pinned-collapsed) and re-expands only on an explicit pin (never on load backfill); the expanded rows are capped at 40vh with their own scroll so a long pinned set can't push the list or footer out of view |
| Workspace head / session row | see next two sections | share --sb-* alignment |
| User-menu footer | account area (components/UserMenu.vue) opening an upward §03 menu | pinned row under the session list, separated by a 0.5px --line hairline; trigger keeps the same list-style family as New chat (24px round avatar + nickname when signed in, user icon + sign-in hint otherwise). The menu box follows the trigger's left edge and width (ResizeObserver-tracked, so it survives a sidebar resize) and is teleported to body because the column's container-type would capture position:fixed. Rows: plan usage / theme / language are macOS-style hover flyout submenus — the parent row carries the module icon, a faint current value and a fixed chevron-right, and hovering (or moving focus to the parent row, or pressing Enter / Space / → on it) opens a teleported panel anchored to the parent menu's right edge (content-adaptive width floored by the menu's own min-width and capped at the parent menu's width; flips left near the viewport edge) with a 250ms hover-intent close grace; the usage panel shows weekly + 5h rows (used-percent values via settings.planUsage.usedPct, with severity colours), while the theme (three schemes) and language (two locales) panels move the check to the picked option without closing the menu — then the upgrade entry below the top plan level, settings (with an always-visible Kbd keycap shortcut hint on desktop) and a confirming sign-out; all menu icons come from the Kimi set, and the whole menu (flyouts included) runs one step above the §03 default density — base-size labels, taller rows |
kimi-web.sidebar-multi-tab, default OFF) forks the sidebar into two forms. OFF = the legacy single session list: no 进行中/已完成/工作空间 tabs (statusTab pinned to open, tab shortcuts inert), no session-admin entries (the 列表管理 menu item hides and the workspace home itself falls back to the classic 新建会话 doodle hero — no workspace head, no recent-sessions list), the row hover action + context menu and the chat-header ⋯ menu read 归档 with the archive glyph (no success-hover variant, the header's Done pill + reopen button hide), and the archive ActionToast is the legacy «撤销 · 或到 · 设置 · 查看已归档的会话» linking to Settings → Archived. ON = the status-tabs form documented in this section (tabs, session admin page, the complete/reopen relabel, «已完成 · 撤销» toast). The preference is an app-core singleton (useSidebarTabs) consumed directly by Sidebar / SessionRow / ConversationPane / App.vue — no prop threading. A session row is an inset rounded pill, structured as: status slot → title → time → attention Badge → hover actions (pin / archive).
| Part | Rule |
|---|---|
| Container | padding: 8px 8px inside the list's --sb-inset gutter, radius-sm; no fixed/min height — row height is font-driven (title line-height: --leading-tight, ≈16px) → ≈32px total, the sidebar-wide row rhythm. The hover actions are absolutely positioned so they never force the row taller (no hover jitter). hover = --sb-hover (the global --color-hover wash); active = --sb-selected (75% of the global selected wash) — neutral, no accent tint, no border, no weight change |
| Status slot (lead) | fixed --sb-gutter width; running = Spinner sm, otherwise unread = 7px accent dot; empty while an attention Badge owns the row — one status at a time, decided by the shared SessionDisplayStatus enum (app-core sessionDisplayStatus.ts: approval › question › running › aborted › unread) |
| Title | flex:1 with truncation and user-select:none; double-click enters inline rename (compact input, not Input), whose text remains selectable |
| Emoji icon | the session icon is the title's LEADING emoji cluster (app-core splitSessionEmoji — no icon field; every client renders the title as-is). The emoji is an ordinary title character — no decoration at rest or on hover (it stays a <button> for a11y), and clicking it opens SessionEmojiPicker — a Menu-shelled panel (bare list-style search row → scrollable sections: Recently used persisted in localStorage (cap 8) + the grouped emoji dataset, with remove/random as MenuItems in the footer; a query swaps the sections for keyword-search results), teleported + fixed + --z-dropdown, popping from the trigger corner like the right-click menu. The menu's "Set Emoji…" opens the same picker and is the discoverable path. Inline rename edits the whole title — the emoji is an ordinary character in the input |
| Time | mono xs, fg-faint; yields to the hover actions on hover |
| Attention Badge | Badge sm: info (needs answer) / warning (needs approval) / danger (aborted) |
| Hover actions | IconButton sm × 2 — pin + archive — cross-faded over the time on row hover (no kebab button). Right-clicking the row opens the full menu (copy ID / rename / emoji / fork / export / pin / archive + timestamp) anchored to the cursor, except over the inline rename input, where the native text-editing menu stays |
| Flat-style variant (flat list + pinned section) | the sidebar's flat list rows AND — always, regardless of view mode — the pinned section's rows differ from the grouped row in three ways (all keyed off the facade projecting cwdLabel): ① no leading status slot — the title is left-aligned at the row's content edge; ② a second line under the title: folder-closed icon sm + the cwd's final directory name (- when the session has no cwd), xs faint like the time — except the icon, which takes --color-text-muted (one rung stronger, the same optical compensation as the group head's folder; the open-folder glyph's thin back-flap washed out at 14px) — rest-width tail mask fade; when the session has an associated PR (v2 git domain), a small tag (git-pull-request icon + #number, 2xs medium on a soft ground with a hairline edge and radius-sm corners — a mini §03 Badge) sits at the line's right edge, state-colored the GitHub way (open = --color-success-soft ground + --color-success text, merged = --color-done-soft + --color-done purple, closed = neutral sunken) and opens the PR on click; ③ the first line's right side shows status — attention Badges anchored to the row's right edge, running Spinner, unread dot — INSTEAD of the time, which only renders when there is nothing to report (the Spinner yields to the attention pills: a session waiting for approval/answer never shows both); on hover the actions cross-fade IN as the whole status cluster fades OUT — pills and pin/archive never co-exist (grouped rows keep pills visible on hover). Height stays font-driven — the pill just grows the line. Grouped rows never set cwdLabel and keep the classic structure. The flat ↔ grouped switch lives in a dropdown on the SESSIONS section label (fixed list-settings icon + hover tooltip; the menu opens with a muted group label, per-view icons, and the current view checked at the row's right edge; mode persisted per device) |
| Archive | no confirm — the hover archive button / menu item archives immediately, then App.vue shows the §03 ActionToast (top-center) with Undo (restores the session) and Settings (opens the archived list) |
The group head and session rows share --sb-*: folder icon (open/closed) → name, with the kebab and "+" revealed on hover.
--sb-gap before the name — it does not pad out the --sb-gutter slot.--color-text-muted, one step lighter than session titles), so group heads remain clear without competing with list content. No path subtitle; hovering the name shows the full root path in a Tooltip.IconButton sm inside a floating actions layer anchored to the row's right edge — no reserved layout space, so the name uses the full row width when idle. Shown on hover, keyboard focus, or while the menu is open; the layer backs itself with the sidebar surface (container background) plus the row hover wash (an ::after shown only while the row is hovered), so its color exactly equals the row's current background and the overlapped name tail doesn't bleed through (hidden via opacity:0, staying in the tab order). On macOS desktop the layer paints nothing at all — the name's mask-image fade (see the sidebar section above) dissolves the tail before it reaches the buttons--sb-selected fill as a selected session row (selection reads as "where I am"; the fill wins over hover). Once a session is selected or created, the fill moves to that session row.The "expand / collapse" controls at the bottom of each workspace group are compact list controls (same family as search, New chat, inline rename — not Buttons) sharing one row: expand (chevron-down) first, collapse (chevron-up) after a faint middot when both are present. Expanding reveals the next batch of sessions, fetching the next page from the server only when the locally loaded rows can't cover it — the control never exposes whether a reveal came from memory or the network.
| Part | Rule |
|---|---|
| Row | a single flex row holding the controls, all content-width — hover washes just the button as a snug pill, never the full row. Font-driven height (≈32px like a session row), radius-sm; hover = --sb-hover (no text recolor); :focus-visible uses --p-focus-ring |
| Chevron | sm (down = expand, up = collapse); the row indents by --sb-gutter + --sb-gap so the first button's chevron starts exactly at the session-title x, lining the control's leading edge up with the titles above |
| Label | font-ui, text-xs, --color-text-muted; truncated |
| Separator | faint middot (--color-text-faint) with --space-1 side margins, rendered only when both controls are present |
| Behavior | each group keeps a display cap starting at the first page; "Show more" steps it up by one batch (5) and fetches the next page only when the loaded rows fall short (busy = "Loading…", disabled); "Show less" resets the cap to the first page (view-layer trim — data is kept, no refetch). "Show more" exists while undisplayed loaded rows remain or the server has more; "Show less" appears once past the first page |
A 4px grab strip layered over the 1px column border (margin: 0 -2px makes the whole 4px grabbable) with a centred 2px indicator bar. The bar stays transparent at rest and shows the neutral fills one step up the ramp — f2 on hover, f3 while the drag is live (the sidebar column is translucent on macOS, so f1 read too faint) — never the accent.
| Rule | Value |
|---|---|
| Width / cursor | 4px strip, 2px bar / col-resize mid-range; w-resize / e-resize at the drag limits (hints the direction that still resizes) |
| Normal / hover / drag | transparent / --color-selected (f2) / --color-line-strong (f3) — the neutral ramp one step up, never accent |
| Layer | --z-dropdown, above pane-level sticky chrome (chat dock at --z-sticky) so the overhang stays visible and grabbable |
| Behavior | panel width follows the pointer 1:1 while dragging (the parent disables transitions to avoid lag); on release it is persisted to localStorage |
The right panels (file preview / Diff / compaction summary / sub-agent / side chat) share one track and one head primitive.
PanelHeader primitive (48px = --panel-head-h), the same height as the conversation column head, so the hairline runs as one line.0 → var(--preview-w) with no animation, squeezing the conversation column in a single layout.position:fixed; inset:0).The native terminal (components/terminal/) sits in the conversation column's own bottom grid slot — the sidebar and the right panel span BOTH rows and keep full height (the VS Code layout: the panel belongs to the editor area, not to the whole window). Its height transitions 0 ↔ var(--terminal-h) (260px default, 120 min, 60% viewport max; persisted), squeezing the conversation column above instead of overlaying it. The panel mounts lazily on first open and then stays mounted so xterm scrollback survives a collapse.
row-resize mid-range, n/s-resize at the limits, same neutral f2/f3 ramp, never accent). The shared useResizable hook owns it via axis: 'y'; the height var is written imperatively during a drag (same no-Vue-rerender rule as --preview-w).radius-sm pill (leading terminal glyph, muted while exited + shell label + hover close affordance), the active tab uses --color-selected, hover --color-hover; a "+" action appends a tab. Tabs follow the §08 tablist keyboard model (roving tabindex, ←/→/Home/End), the close affordance is its own button (no nested interactives), and the height separator is keyboard-operable (↑/↓ in steps, value exposed). Trailing actions: restart (only while the active tab exited) and a collapse chevron. Collapsing sets inert on the region — the xterm instances and their scrollback stay mounted but leave the tab order.--color-* tokens at runtime (re-read on scheme flips; the ANSI hues the status ramp doesn't cover use dedicated --color-term-magenta/cyan tokens); the font is the app JetBrains Mono stack sized off the content token scale. While focused, the panel owns every key except the registered app shortcuts (chat-level Esc / find / select-all chords stay inert inside it).ctrl+` (⌃` on macOS — VS Code's binding; ⌘` stays free for the OS window switcher — customizable in the shortcut registry), and the View menu's Toggle Terminal item. New tabs spawn in the visible workspace root. Terminal state is per session: switching sessions swaps the visible bucket while the others keep their PTYs and xterm views alive (scrollback survives a round trip; the ten most recent sessions are kept, LRU). The panel never renders on mobile / web.Kimi Web is a local developer tool; it does not target a specific WCAG conformance level, nor maintain a full screen-reader QA matrix. This section collects only the rules that are "low-cost, don't hurt the look, and directly benefit keyboard-heavy users", as the baseline contract for each primitive; the more expensive, lower-ROI parts (such as real-time announcement orchestration for streaming output) are not mandatory for now.
:focus-visible (not :focus). It appears only on keyboard focus; mouse clicks don't trigger it, so it doesn't pollute the mouse-driven visual; the ring's strength is tuned uniformly with --p-focus-ring, not overridden per place. Anything doable with a mouse must also be doable with a keyboard; Tab order follows the DOM, with no invented skipping. Composite controls define their keyboard model per the table below; a missing model is treated as incomplete:
| Control | Keyboard behavior |
|---|---|
| Dialog | Tab cycles within the dialog (focus trap); Esc closes; focus returns to the trigger element after closing. |
| Menu | ↑ / ↓ move the highlight, Enter selects, Esc closes. |
| Tabs | ← / → switch tabs (roving tabindex); only the current tab is in the Tab sequence. |
| Switch / Segmented | ← / → or Space / Enter to toggle. |
:focus-visible + --p-focus-ring (primary actions may use --p-focus-ring-strong).outline: none is forbidden. To remove the default outline, you must provide an equivalent replacement style.aria-label — IconButton already enforces this with a required label prop.role="dialog" + aria-modal="true", with the title as the dialog's accessible name.aria-hidden="true" to avoid being read out by screen readers.Desktop click targets ≥ 32px; touch devices ≥ 44px (consistent with the §01 principle and the IconButton lg tier).
Handled uniformly in the global styles per §02's @media (prefers-reduced-motion: reduce); components do not check this individually. The chat working indicator's mascot renders its static fallback.
Screen-reader announcements are not a mandatory contract in this product. Short hints like Toast can use role="status" / aria-live; chat streaming output is currently not announced word-by-word, which is an acceptable trade-off, to be added later if a real need arises.
Every overlay in the app — pickers, browsers, managers, confirmations — is built on the single §03 Dialog primitive. This chapter fixes the two layout anatomies allowed inside that frame, plus the row and footer contracts that make all dialogs read as one family. Do not hand-roll a third anatomy.
All dialogs share the §03 primitive: --radius-xl radius, --shadow-xl shadow, a restrained 28% neutral backdrop, a head (title + IconButton close), a body, and a right-aligned foot. Widths md 440 / lg 640 / xl 760 and auto / fixed height are chosen per §03. One interruptive overlay at a time; Esc closes; focus is trapped and restored. A blocking flow that must be resolved rather than dismissed (server token) uses hideClose with closeOnOverlay/closeOnEsc off — never a hand-written overlay.
The default: the body carries its own padding and the caller drops content straight in. Confirmations put their Buttons in the #foot slot (right-aligned, cancel → confirm). Used by: confirm, login, status panel, server token.
:padded="false" with height="fixed"; the consumer owns the zone layout inside a full-height column. The zones below are the whole vocabulary — a picker dialog composes them and adds nothing else. Used by: model picker, session search, folder browser, provider manager.
| Zone | Contract |
|---|---|
| Search | The boxed §03 Input, inset 22px so its edge aligns with the head title. Autofocus on open. No leading icon, no borderless variant. |
| Filter chips | Optional. 28px pill: transparent + muted text by default, --color-hover on hover, --color-selected + medium --color-text when active. Horizontally scrollable with the scrollbar hidden. Never a row of Buttons. |
| List | flex:1, owns the vertical scrolling, padded 4px 8px so rows bleed near the dialog edge. role="listbox"; rows carry role="option" + aria-selected. |
| Row | 8px 12px padding, --radius-md. Two quiet lines: name 14/20 (medium when current) and a meta line 12/18 in --color-text-faint — provider · context · capability labels, dot-separated. No badge rows, no raw-id line (search still matches them). Trailing slot: check icon (current row only), then the star IconButton. |
| Row states | Hover / keyboard-selected → --color-hover; current → --color-selected — a neutral "where I am" fill, never an accent tint, never an inset stroke. The star stays hidden until row hover, keyboard selection, or starred; it is always visible on touch devices and colored --star when starred. |
| State rows | Loading / unavailable / empty: centered on both axes, muted 14px; warning color only for the unavailable case. |
| Shortcut bar | The footer: full-bleed, padding 8px 16px, border-top --color-line, left-aligned. Keyboard hints are Kbd keycaps + 12px --color-text-faint labels, groups separated by "·", the whole bar aria-hidden. An instructional sentence (folder browser) reuses the same bar without keycaps. |
↑/↓ move a keyboard selection (rendered identical to hover) and always scrollIntoView({ block: 'nearest' }); Enter selects and closes; Esc closes.background only (--duration-fast ease-out); the open/close animation lives in the primitive, not in the consumer.| Dialog | Anatomy | Composition |
|---|---|---|
| Model picker | flush · lg · fixed | search + provider chips + model rows + shortcut bar |
| Session search | flush · lg · fixed | search + result rows (sidebar-style alignment: one icon gutter, shared left text edge, shared right meta edge; workspace rows single-line name + right-aligned path, session rows title + time over a workspace · snippet meta line; quiet uppercase section heads with counts; empty query shows a few top workspaces + recent sessions) + shortcut bar |
| Folder browser | flush · lg · fixed | breadcrumb bar + filter bar + folder rows + actions + hint bar |
| Provider manager | flush · xl · fixed | management rows with inset dividers (rows are not selectable) + add section + shortcut bar |
| Confirm / Login / Status | padded · md · auto | title + message or form + right-aligned foot |
| App update (desktop) | padded · lg · auto | version title (stays "发现新版本 vX" even while downloading) + quiet meta line (release date · current version) + height-capped scrolling what's-new list + right-aligned action row (skip → download; downloading → background + disabled live-percent button; later → restart) with the auto-download checkbox right-aligned on its own foot row below (a pure preference for future checks) |
| Server token | padded · md · auto | hideClose, no Esc/overlay close — resolved only by a valid token |
| Settings | flush · xl · fixed | page-like exception: side-nav region, per §03 |
| Onboarding wizard | not a Dialog | full-page takeover (not built on §03): one centered column (brand lockup → step content → ghost actions + centered primary CTA); selectable options share the option-card pattern — 0.5px --color-line hairline, --color-accent border + --color-accent-soft fill when selected |
The session admin page (/admin/sessions, opened from the sidebar's list-management menu) is a full-pane management view for cross-workspace session triage: filters, a server-side paged table, and batch lifecycle actions. It is a main-view peer of the conversation pane (mainView in the facade, switched with v-show so the chat stays alive) and page-private by decision — everything lives under components/admin/ (SessionAdminView/Table/Pagination, FilterSelect, MultiSelectMenu, SessionAdminMenu, useAnchoredMenu); nothing here promotes to §03 until a second consumer exists. Data is one GET /api/v2/sessions page-mode call per filter/page change (all conditions pushed down, no client-side aggregation); batch archive/restore go through the v2 batch endpoints with per-item outcomes.
A 48px title bar (a back IconButton — chevron-left, tooltip 返回, closing the page back to the chat underneath via closeSessionAdmin — then the page title, semibold base; hairline bottom edge; on macOS desktop it doubles as the window-drag region and takes the chat header's collapsed-sidebar clearance — 146px / 78px / Windows fallback — so ONLY the bar insets, the body never does) → muted subtitle → query-form filter bar → table card → pager. The title bar and the scroll container are siblings (bar fixed, body scrolls). The page wrapper spans the whole conversation column — do not apply the chat content measure (--p-content-max) here: under table-layout: fixed a narrow wrapper crushes the table's flexible columns to zero width (the title/prompt columns collapsed in practice). An admin table surface owns the pane width. The status filter defaults to 全部 (all) — the page is the whole inventory, 重置 restores that same default.
--p-content-max is a READING measure for prose-like content (chat, dialogs). Full-bleed work surfaces — tables, grids, dashboards — span the pane instead. Pick one deliberately; inheriting the chat measure by default is the bug. The card is the flat shell: 0.5px --color-line hairline, --radius-lg, no shadow. Inside, table-layout: fixed with a colgroup: fixed-width utility columns (checkbox, workspace, status, the two time columns, actions) and two flexible content columns (title at max(200px, 20%) — a floor, not a bare percentage — last prompt taking the rest). The head row is a pinned-height 32px box (it hosts the batch transform below); body rows are 40px. Rows are separated by 0.5px --color-subtle hairlines (none after the last row), with a --color-hover wash on hover. Every cell truncates single-line with ellipsis and carries a title tooltip. Time columns are absolute (YYYY-MM-DD HH:mm) in --font-mono xs with tabular-nums — the admin page is an audit view, so no relative times; empty values render a faint —. The status column sits right after the workspace column and reuses the sidebar row's lifecycle glyphs: state-open (dashed ring, --color-success) for 进行中 and state-done (checked ring, --color-done) for 已完成, icon + label. First load swaps the body for a centered §03 Spinner; refetches keep the stale rows and dim the card (opacity + pointer-events: none) rather than flashing it away; filters with no matches render the centered faint empty line.
Responsive steps (the card is the query container; web's narrow panes matter too): the time columns give ground FIRST — at ≤1020px their values swap to the compact MM-DD HH:mm at 108px (each time cell carries both spans, CSS toggles them — no JS measuring); at ≤760px the time columns hide outright (the col and the cells share sa-c-time/sa-col-time classes) and the workspace column drops its folder icon. The table itself floors at 640px and the card takes overflow-x: auto — past the floor the card scrolls horizontally instead of crushing title/prompt to zero, and auto still clips the rounded corners like hidden did.
| Column | Width | Content |
|---|---|---|
| checkbox | 36px | header select-this-page (indeterminate) + row checkboxes |
| 会话名 | 20% | title (emoji verbatim), weight 475, ellipsis; hosts inline rename |
| 工作空间 | 116px | folder-closed icon + workspace name (cwd basename fallback) |
| 状态 | 88px | state-open/state-done glyph + label |
| 最后一条 prompt | flex | muted, ellipsis, faint — when null |
| 最后更新 / 完成时间 | 140px ×2 | mono tabular-nums absolute time; completed shows — while open |
| 操作 | 84px | lifecycle IconButton (state-done completes an open row, undo reopens a done one; tooltips carry 标记完成/恢复进行中) + ⋯ IconButton dropdown (Rename… / Fork / Export) |
GitHub issues/PR semantics: while a selection exists, the head row transforms IN PLACE — the checkbox column stays put, and the remaining column headers swap for a single <th colspan> batch bar ("已选 n 项" + Mark-as-done / Reopen, each disabled when the selection holds no row of that lifecycle) inside the SAME pinned 32px box, so the table body does not move a pixel. Mark-as-done is the bar's one accent-primary button (fill + on-accent icon/label); Reopen stays a quiet hairline button. The selection itself is facade-owned (a reactive id set plus a per-id lifecycle map, reconciled against fresh rows on every landing) and survives page and filter changes — the batch count deliberately includes rows no longer visible. Successful batch items leave the selection and the current page is silently re-pulled; failures stay selected. There is deliberately no "clear selection" button for ordinary selections: unchecking rows or the header checkbox is the way out. Toasts ride App.vue's shared ActionToast channel (succeeded count, failed count when partial, Undo through the inverse batch endpoint); an all-failed batch has nothing to undo and surfaces as a WarningToast instead.
Page-size caps (≤100) make "mark everything done" hopeless against thousands of rows, so the batch bar borrows Gmail's escalation: once the header checkbox has the whole page selected and total says more rows exist, a link-style button appears IN the bar (zero-offset — no banner pushing the table down): "选中当前条件下的全部 N 项" (busy-label 正在选中… while fetching). Activating it materializes every matching id into the selection via the ids projection (GET /api/v2/sessions?fields=id,archived — the one cheap shape whose page_size ceiling relaxes to 10000; cursor-walked for larger sets), merging atomically: a filter change mid-flight discards the fetch entirely. While active the count reads "已选中全部 n 项" and the link becomes 清除选择. Exclusions are free — unchecking a row just drops it from the set; emptying the selection drops the mode, as does a right-click single-row collapse. The mode ties itself to the filter fingerprint it was built from: a real condition change (query-form apply with different values, or any granular setter) clears the whole selection — re-applying the SAME conditions keeps it. Batch executions chunk at the wire's 5000-unique-ids ceiling (sequential, merged per-item outcomes; a thrown chunk aborts the rest and counts everything unexecuted as failed — the succeeded ids still reconcile).
All quiet icon/text buttons on the page (the batch bar's Reopen, the pager buttons) set border: none; background: transparent explicitly — or, for the bordered shapes (the filter triggers), an explicit 0.5px hairline. Row actions are pure §03 IconButtons (the sidebar row's language): the lifecycle glyph (state-done completes an open row, undo reopens a done one — tooltips carry the labels) plus the ⋯ dropdown; a text button per row read as visual noise once every row carried one.
<button> still inherits the UA stylesheet's outset border, which reads as a stray box around every quiet control. Any quiet button outside the §03 primitives must reset both properties. The filter bar is a query form (antd Pro semantics): the controls edit a local DRAFT only — nothing is requested until an explicit apply. 查询 (a §03 Button variant=primary size=sm) applies the whole draft through the facade in one shot (applySessionAdminFilters — atomic write + page reset + exactly one request, never the per-setter debounce dribble), 重置 (a ghost sibling) restores defaults the same way, and Enter inside the bar queries too — except while any overlay (a select dropdown) is open, where Enter belongs to the overlay. Pagination is exempt: page/page-size changes still fetch immediately. Entry paths can pre-seed the conditions: the workspace home's 查看更多 opens the page with its workspace already selected in the filter (openSessionAdmin(workspaceId) applies the filter atomically with the navigation), and because the page is v-show-kept, the draft re-seeds from the applied filters on EVERY entry so the controls always show the true conditions. The controls themselves are muted sm labels plus a family of quiet 30px hairline controls (0.5px --color-line, --radius-md, transparent ground, --color-hover on hover/open) — deliberately NOT the §03 Select (a 32px+ form control with an accent focus ring, too heavy for a filter strip):
--color-success open / --color-done done).updatedTo bound as the local calendar day N days back (computed at apply time, so a saved draft never goes stale), and the facade's day-end mapping turns it into updatedBefore. Deliberately not a calendar range picker — triage asks "older than X", not "between two dates".useAnchoredMenu is the page's one menu mechanic: fixed-position §03 Menu surface, pop-from-anchor motion (fade + 0.97 scale on the menu tokens, origin and nudge following the upward flip at the viewport edge), closed by outside mousedown / Esc / scroll / resize. Three anchor modes — left-aligned under a trigger (filter selects), right-edge under a trigger (the row ⋯), and openAt(x, y) at a raw viewport point for the row contextmenu. The contextmenu has two shapes: on a row outside the multi-selection the selection first collapses to just that row and the menu is the single shape (Open session / Rename… / Fork / Export / — / the lifecycle action); on a row inside it, the multi shape (a muted count head + Mark-as-done (n) / Reopen (n), disabled per availability). Open session is the facade's selectSession — a user navigation that also leaves the admin page back to chat.
The title cell swaps for an accent-ringed input (--color-accent border + --color-accent-bd ring, --radius-sm) — Enter commits, Esc cancels, blur commits, settled once. The table is server-fed, so a commit is followed by a silent re-pull of the current page (the facade's pool update never reaches it).
This document defines the visual language and component specification for Kimi Web — design tokens, component primitives, the chat interface, theming, and style rules. All UI work is grounded in it: unified, restrained, token-driven, and themeable.
Every UI decision traces back to the following principles. Kimi Web is a local Agent tool for developers: quick scanning, long stretches of staring, often in the dark — the design serves the task, and is restrained, clinical, and density-first.
Collapse every visual decision into tokens. Color tokens keep the existing short names and fill out the semantics (lowering migration cost), while spacing, z-index, motion, and font-weight fill in the scales that are currently missing. Every token has: name, light value, dark value, and usage.
--<category>-<role>-<state>. For example --color-text-muted, --radius-md, --space-4. To reduce churn, the existing short names (--bg / --ink / --line / --blue …) are kept as compatibility aliases for one release cycle. Semantic-first, in three layers: background / text / border + accent + status colors. All colors are defined in light / dark pairs, with contrast ≥ 4.5:1.
:root and a dark override in the data-color-scheme blocks — for example --color-bg is #ffffff in light and #121212 in dark; --color-accent is the brand blue (#1783ff light / #1a88ff dark). The semantic status colors (success / warning / danger / info) are independent palettes, one set each for light / dark.| Token | Light | Dark | Usage |
|---|---|---|---|
| --color-bg | #ffffff | #121212 | Page background |
| --color-surface | #f5f5f5 | #1f1f1f | Panel / sidebar / card head |
| --color-surface-raised | #ffffff | #292929 | Raised card / dialog / input |
| --color-menu-bg | rgba(255,255,255,.95) | rgba(41,41,41,.95) | Floating menu panel — frosted glass over --p-menu-backdrop blur |
| --color-surface-overlay | #ffffff | rgba(255,255,255,.1) | Field-control fill on raised cards (selects, steppers) — top rung; light tops out at white (the level is carried by the border), dark steps one rung above raised. Floating layers stay at raised |
| --color-well | #f5f5f5 | #1f1f1f | Content well on the page (code blocks, tool-output panels, match/file lists, media thumbnails) — light reuses the sunken recess; dark lifts one rung ABOVE the page, because a true recess (#121212) vanishes into the page there |
| --color-surface-deep | #f5f5f5 | #0d0d0d | Deep chrome plane one step BELOW the page (panel headers, diff gutters) — dark drops under --color-bg so chrome framing stays darker than the content it frames |
| --color-text | rgba(0,0,0,.9) | rgba(255,255,255,.84) | Body text / headings |
| --color-text-strong | #000000 | #ffffff | Max foreground emphasis — menu-row label & icon on hover |
| --color-text-muted | rgba(0,0,0,.6) | rgba(255,255,255,.56) | Secondary text / placeholder |
| --color-line | rgba(0,0,0,.13) | rgba(255,255,255,.12) | Divider / card border |
| --color-subtle | rgba(0,0,0,.05) | rgba(255,255,255,.05) | Subtle hairline — tertiary separators below --color-line (diff-gutter column rules, quiet dividers inside wells) |
| --color-selected | rgba(0,0,0,.05) | rgba(255,255,255,.1) | Neutral selected fill (sidebar rows, list pickers) — translucent, never accent-tinted |
| --color-hover | rgba(0,0,0,.03) | rgba(255,255,255,.05) | Row hover wash — lighter than the selected fill (hover < selected); translucent, sits on any surface. The global hover rule: transparent-base controls overlay this f1 wash (hover never darkens — never sunken); filled controls use their own hover token (accent-hover, send-bg-hover) |
| --color-selected-hover | rgba(0,0,0,.08) | rgba(255,255,255,.14) | Hover of a control RESTING at the selected fill (work cards) — one rung above f2, below f3: hover deepens/brightens a step, never drops below the rest state |
| --color-inline-code-bg | rgba(0,0,0,.03) | rgba(255,255,255,.1) | Inline-code chip fill — fills.f1 / fills.f2; dark lifts off any dark surface (sunken == bg there) |
| --color-media-alpha-bg-1 | ≈#858585 | ≈#76797e | Checkerboard square A of the <img> alpha canvas — color-mix of --color-bg/--color-text (52/48); applied via --media-alpha-canvas (16px period) |
| --color-media-alpha-bg-2 | ≈#6b6b6b | ≈#8c8f93 | Checkerboard square B (42/58) — both squares stay ≥3:1 against white and black; opaque images cover the canvas |
| --color-sidebar-bg | #f9fbfc | #0d0d0d | Sidebar surface — one step off --color-bg (just under white in light, one step BELOW the page in dark) so the session column reads as its own plane and never brighter than the reading surface |
| --color-scrim | rgba(0,0,0,.4) | rgba(0,0,0,.6) | Modal scrim — the dark veil behind dialogs/lightboxes (mask.base; legacy hardcoded overlays can migrate here) |
| --color-scrim-strong | rgba(0,0,0,.6) | rgba(0,0,0,.75) | Stronger scrim for full-screen media previews (mask.strong — the PhotoSwipe image preview backdrop) |
| --color-text-on-scrim | #ffffff | same | Text drawn on the scrim (captions over the media lightbox) |
| --color-accent | #1783ff | #1a88ff | Primary action / link / focus |
| --color-success | #0e7a38 | #3fb950 | Success / pass |
| --color-warning | #a9610a | #d29922 | Warning / pending |
| --color-danger | #c0392b | #f85149 | Danger / error / abort |
The palette is the production kimi.com palette (design tokens tokens.json): neutral-gray surfaces, an alpha-based label / fill / separator ramp (labels.* / fills.* / separator.s1), the KMBlue accent, and a true neutral dark ladder (#121212 → #1f1f1f → #292929; the deep chrome plane and sidebar derive one step below at #0d0d0d — the palette has nothing darker than primary).
The ONE deliberate exception is the status hues: success / warning / danger / done keep the app's own WCAG-tuned ramp (≥4.5:1 on the neutral surfaces) — the production status colours (positiveGreen #16c456, orange #ff9500, danger red #ff3849) are too bright against it. Diff add/del bands happen to coincide (both use the production 25% fills in light, 14% in dark).
The surface layers each have a role — choose by "field overlay / raised layer / content well / default flat layer / sunken layer / page background / deep chrome", and avoid treating --p-surface-raised as a universal background. In dark, elevation = lighter: floating layers sit above the content, content wells sit above the page, and chrome planes (sidebar, panel headers) sit below it — never the reverse. One consequence: on the page itself, never use --color-surface-sunken for a content carrier — it equals --color-bg in dark and the fill vanishes; use --color-well. Sunken stays correct INSIDE surface / raised cards, where it is a genuine recess. Field controls (selects, steppers) on a raised card use --color-surface-overlay, the top fill rung; floating layers keep --color-surface-raised — their elevation is shadow + hairline, not a lighter fill.
| Token | Light | Dark | Usage |
|---|---|---|---|
| --p-surface-overlay | #ffffff | #22272e | Field controls on raised cards — select, stepper (top fill rung; light = white) |
| --p-surface-raised | #ffffff | #1c2128 | Raised card / dialog / input (raised layer) |
| --p-well | #f3f5f8 | #13181e | Code block / tool output / list carrier directly on the page (content well — light: recessed, dark: one rung above the page) |
| --p-surface | #fafbfc | #13181e | Panel / sidebar / card head (default flat layer) |
| --p-surface-sunken | #f3f5f8 | #0d1117 | Recessed area INSIDE a surface / raised card — never a content carrier on the page (sunken layer) |
| --p-bg | #ffffff | #0d1117 | Page background |
| --p-surface-deep | #fafbfc | #0a0d12 | Panel header / diff gutter (deep chrome layer — below the page in dark) |
Three line tokens, three jobs: --color-line is the default structural separator, --color-subtle the tertiary separator that must stay quieter (diff-gutter column rules, quiet dividers inside wells), and --color-line-strong the edge of interactive controls (inputs, selects, secondary buttons). Width is one: 0.5px — every stroke is the same hairline, on static structural edges (card rims, plane seams, header dividers), interactive control rims and floating layers alike. Separation comes from luminance first — planes one rung apart already read as distinct in dark, so their shared edge stays a 0.5px hairline rather than a heavier border; same-rung neighbours (list rows, card head / body) are exactly where a hairline is required. In dark, drop shadows fade on near-black surfaces, so a floating layer's edge IS its hairline — never ship a shadow-only floating surface. (Legacy --line / --line2 alias --color-line / --color-subtle for one cycle; new work references the v2 names.)
All focusable controls (button, input, link, menu item, switch, checkbox) use the focus-ring token uniformly; do not hand-write a box-shadow focus ring.
| Token | Value | Usage |
|---|---|---|
| --p-focus-ring-w | 3px | The focus ring's spread width — the rings below derive from it, and so does anything that must reserve room for the ring (clip protection) |
| --p-focus-ring | 0 0 0 3px var(--p-accent-soft) | Default focus ring (link, menu item, switch, checkbox) |
| --p-focus-ring-strong | 0 0 0 3px var(--p-accent-soft), 0 0 0 1px var(--p-accent) | Strong focus ring (button, primary action) |
The text-selection color uses --p-selection uniformly (light rgba(23,131,255,.18) / dark rgba(88,166,255,.32)), applied by the global ::selection rule; do not set a separate highlight background.
All disabled controls use opacity:.5 + cursor:not-allowed uniformly; do not separately grey out or recolor.
Kimi Web uses two font tokens: --font-ui (UI and body, with Schibsted Grotesk for Latin and Noto Sans SC for Simplified Chinese) and --font-mono (code and monospace). Components always reference the variables; do not hard-code font names.
Body and UI use self-hosted Schibsted Grotesk for Latin text and self-hosted Noto Sans SC Variable for Simplified Chinese. Platform fonts remain as fallbacks:
--font-ui: "Schibsted Grotesk Variable", "Helvetica Neue", Arial, + "Noto Sans SC Variable", "Noto Sans SC", "PingFang SC", + "Microsoft YaHei", + -apple-system, BlinkMacSystemFont, "Segoe UI", + Roboto, Ubuntu, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji";
Code, line numbers, diffs, and Bash commands use JetBrains Mono (a self-hosted variable font), falling back to the system monospace. Other tool labels and summaries use the UI font:
--font-mono: "JetBrains Mono Variable", "JetBrains Mono", + ui-monospace, "SF Mono", Menlo, Consolas, monospace;
| Font | Source | Bundled | Usage |
|---|---|---|---|
| JetBrains Mono | @fontsource-variable/jetbrains-mono | ✓ self-hosted | monospace / code (--font-mono) |
| Schibsted Grotesk | prepare-fonts → app-ui/assets/fonts | ✓ generated + bundled | UI / body / display (--font-ui, --font-display), wght 400-900, normal + italic |
| Noto Sans SC | prepare-fonts → app-ui/assets/fonts | ✓ generated + bundled | Simplified Chinese UI / body, wght 100–900 |
| System UI / CJK fonts | operating system | — | late fallback for UI / body |
var(--font-ui) / var(--font-mono); do not hard-code font names like 'Schibsted Grotesk' / 'JetBrains Mono'.--font-ui (Schibsted Grotesk for Latin, Noto Sans SC for Simplified Chinese); code / monospace use --font-mono (JetBrains Mono).font-optical-sizing: auto is enabled globally.The user font-size preference is one of four named steps (small / medium / large / xlarge, Medium default) written to data-font-scale on <html>; the step name is persisted, never a px value. The step only moves --base-font; every size token derives additively (default + shift), and line heights are locked to integer px via round(size × ratio, 1px) — never a unitless ratio.
Two token groups share the shift but keep their own ratios: --ui-* for chrome (tight, 1.40–1.50) and --md-* for Markdown content + the composer (loose, 1.56–1.63; body is anchored to the UI body size — the spec's +2px offset was dropped as a product decision — while keeping its own looser line-height ratios). T0/T1 cap at 24/22px on the top steps (built into the tokens via min() — do not remove). Use the .text-ui-* / .text-md-* utility classes; legacy aliases --ui-font-size (→ --ui-b2), --content-font-size (→ --md-b1) and the whole 6-level --text-* ramp (xs→c1, sm→b2−1px, base→b2, lg→t2, xl→t1, 2xl→t0) keep older components on the ramp. Panel titles sit at the base step (--ui-b2); dropdown menu items sit one rung below (--text-sm = b2 − 1px) — both still follow the user's font scale.
The fixed product type tokens still define scale-independent defaults: transcript prose enables text-autospace: normal for mixed CJK and Latin text. Drop stray font-weight: 650 / 750; converge on 400 / 500 (regular / emphasis), with a dedicated 600 weight for sidebar section labels.
| Token | Value | Usage |
|---|---|---|
| --font-ui | "Schibsted Grotesk Variable", …, "Noto Sans SC Variable", … | UI & body (Schibsted Grotesk + Noto Sans SC) |
| --font-kbd | "Schibsted Grotesk Variable", system-ui, sans-serif | keyboard shortcut keycaps |
| --font-mono | JetBrains Mono… | code, Bash commands, line numbers, diffs |
| data-font-scale | small / medium / large / xlarge | user preference on <html>; sets --base-font (12–18px), Medium = 14px default |
| --ui-t0…--ui-c2 | default + --ui-shift, t0/t1 capped via min() | chrome type ramp (title / subtitle / body / caption); .text-ui-* classes |
| --md-h1…--md-b3 | default + --md-shift | Markdown ramp (headings / body / secondary / code); .text-md-* classes |
| --ui-font-size / --content-font-size | var(--ui-b2) / var(--md-b1) | legacy aliases kept on the ramp |
| --code-font-size | calc(var(--content-font-size) - 2px) | standalone code surfaces (diff view, file preview, tool cards) — one step below body, 12px @ Medium; prose-embedded code stays on the --md-* ramp |
| --text-xs / sm / base / lg / xl / 2xl | c1 / b2−1 / b2 / t2 / t1 / t0 | legacy ramp, aliased into the scale |
| --leading-tight/normal/prose/relaxed | 1.25 / 1.5 / 1.6 / 1.7 | headings / UI / chat prose / long text |
| --weight-regular/option-label/medium/ui-strong | 400 / 475 / 500 / 525 | body / settings labels / emphasis / compact UI emphasis |
| --weight-section-label | 600 | sidebar section labels |
Icons use three size tokens uniformly. The global .p-ic default is 16px (--p-ic-md); components pick as needed, and random pixel sizes are forbidden.
| Token | Value | Usage |
|---|---|---|
| --p-ic-sm | 14px | small button, badge, menu item, inline link icon |
| --p-ic-md | 16px | default (button, icon button, toolbar) |
| --p-ic-lg | 20px | Toast status icon, empty-state illustration |
Icons always come from the centralized registry lib/icons.ts: in templates use the <Icon name size /> component (components/ui/Icon.vue); for v-html contexts (such as a tool glyph) use iconSvg(name, size). Do not hand-write <svg> — the scripts/check-style.mjs icon-from-registry rule flags stray SVGs. Every glyph shares the 24×24 source grid and currentColor (colour follows text); size uses the three tokens below, and only icons imported in lib/icons.ts are bundled by unplugin-icons at build time. Three collections feed the registry, in this order of preference: ~icons/kimi/* — Kimi Design System icons (24×24 outlined, 1.8px stroke), local SVGs under src/icons/kimi/ registered as a custom collection in the Vite config, used whenever a Kimi glyph exists for the intent; ~icons/tabler/* — Tabler Icons (MIT), for the few gaps it uniquely covers (today: the right-panel toggle); and ~icons/ri/* — Remix Icon (Apache-2.0), for the remaining intents the Kimi set does not cover yet. A few glyphs are filed under their intent rather than the upstream asset name (see the lib/icons.ts header). When an icon is missing, prefer a glyph from the Kimi icon set: copy the SVG into src/icons/kimi/ (kebab-case name, monochrome currentColor) and register it — two static imports (component + ?raw string) plus one entry in ICONS; reach for Remix only when no Kimi glyph fits, and never draw paths in a component.
Do not use emoji as functional icons. The Kimi brand mark (the robot mascot logo) is a brand asset and is not part of this icon system.
A few special graphics are not in the registry; each has a dedicated component maintained in one place, and must not be copied by hand: <ContextRing :pct /> (the Composer context progress ring, data-driven), <AuthStateIcon kind /> (the success / expired / error colored illustrations in the login flow), <Spinner /> (loading state). Status dots (such as in the Provider list) always use CSS dots (border-radius:50%), not SVG. The scripts/check-style.mjs icon-from-registry rule exempts the above and the brand mark; all other hand-written <svg> is flagged.
A 4px base grid. All spacing, gaps, and padding inside and outside components come from this scale — no arbitrary pixels.
High-density navigation lists like the sidebar share one rhythm, all on the 4px grid: in-row vertical padding --space-1 (4px), no margin between rows (the hover pill provides the separation); section gap (between logo / search / action buttons / group title / list) uniformly --space-2 (8px); between groups --space-2; the brand header is slightly looser at the top (--space-3). When building similar lists, reuse this scale — do not hand-write 1/6/7/10px.
Merge the existing 14 values into the nearest of 7 scale steps. Rule: the component type determines the radius, not the author's feel. The Composer shell is the sole product-specific exception: its 32px radius pairs with superellipse(1.5) so the flatter curve stays visually concentric with its controls.
| Token | Value | Usage | Merged from |
|---|---|---|---|
| --radius-xs | 4px | small badge, inline tag | 2/3/4px → |
| --radius-sm | 6px | small button, icon button, menu item | 5/6px → |
| --radius-md | 8px | button, input, badge, card | 7/8/9px → |
| --radius-lg | 12px | menu, toast, bubble, floating card | 10/12px → |
| --radius-xl | 16px | container baseline: dialogs, settings cards, sheets, work panel | 13/16px → |
| --radius-2xl | 20px | workspace attachment card bottom (0 0 2xl 2xl) tucked under the composer | 18/20px → |
| --radius-composer | 32px | Composer shell, with --corner-shape-composer | product-specific |
| --radius-menu-row | var(--radius-sm) | Menu rows inset 6px inside the plain --radius-lg menu frame (concentric: 12px − 6px hug) | product-specific |
| --radius-menu-item | calc(--radius-lg − --menu-pad − --p-hairline) | Items of the §03 dropdown Menu — concentric with the frame: 12px − the 3.5px panel inset − the 0.5px hairline = 8px | product-specific |
| --radius-select-option | calc(--radius-md − --space-1 − --p-hairline) | Options of the §03 Select listbox family (Select, SecondaryModelPicker) — concentric: 8px frame − 4px pad − hairline = 3.5px | product-specific |
| --radius-dropdown-row | calc(--radius-lg − --space-1 − --p-hairline) | Rows of the composer dropdowns (model / permission) and the workspace picker — concentric: 12px frame − 4px pad − hairline = 7.5px | product-specific |
| --radius-window | 14px | macOS hidden-titlebar window corner (measured on macOS 26 Tahoe) — referenced only through calc(), never directly | platform constant |
| --radius-window-chip | calc(--radius-window − --space-2) | Sidebar footer chip's bottom-left corner on macOS desktop — concentric with the window corner (14px − the footer's 8px inset = 6px, exactly --radius-sm) | product-specific |
| --radius-full | 999px | pill badge, avatar, send button | 999px / 50% |
Shadows express only "elevation", never decoration (no colored glow). z-index is unified into a scale, eradicating 9999-style one-upping.
| Z-index Token | Value | Usage |
|---|---|---|
| --z-base | 0 | normal flow |
| --z-raised | 1 | in-component local stacking (menu scroll thumbs, strip badges, floating pills) — never a global layer |
| --z-sticky | 100 | sticky header / sidebar |
| --z-dropdown | 200 | dropdown menu |
| --z-overlay | 300 | overlay / bottom Sheet |
| --z-modal | 400 | dialog — sibling overlays tie-break by DOM order, so the global confirm (ConfirmDialogHost) mounts on demand to always land last / on top |
| --z-modal-dropdown | 500 | menus / popovers that open above a modal dialog (teleported to <body>, e.g. the settings SecondaryModelPicker cascade) |
| --z-toast | 600 | toast |
| --z-tooltip | 650 | tooltip bubble — transient and pointer-events none, so it sits above everything (dialogs, toasts) to stay visible anywhere |
| --z-max | 9999 | reserved: only this tier for extreme fallback |
| Token | Value | Usage |
|---|---|---|
| --ease-out | cubic-bezier(0.16, 1, 0.3, 1) | enter, hover, expand |
| --ease-in-out | cubic-bezier(0.4, 0, 0.2, 1) | panel width, layout changes |
| --duration-fast | 120ms | press, focus |
| --duration-base | 160ms | hover, show/hide |
| --duration-slow | 260ms | dialog, Sheet, layout |
| --duration-hover-intent | 250ms | hover-intent reveal gate (TOC rail) |
| --duration-spin | 700ms | spinner rotation period (mention-tip probe spinner) |
| --duration-flash | 1200ms | one-shot attention flashes (search locate, provider-row added) — a highlight timeout, past the show/hide ramp on purpose |
| --anim-rive-spin | 416.7ms | new-chat / folder-plus icon: plus spin on hover |
| --anim-leftbar | 533.3ms | sidebar toggle icon: arrow fly-in on hover |
| --anim-leftbar-shrink | 200ms | sidebar toggle icon: divider shrink on hover |
The --anim-* lengths are track timings ported verbatim from the designer's Rive exports, so they sit outside the --duration-* ramp on purpose — retiming the ramp must not distort them. Their interpolation stays linear because the easing is already baked into the dense keyframe stops; a token easing would double-apply. Three hover tracks use them today: the sidebar toggle shrinks its divider to half height while an arrow flies in and settles (the expand variant mirrors the track from the left), and the new-chat / folder-plus pluses do one bouncy spin. Each track is keyed to an id inside its own glyph (#bar-divider, #bar-arrow / #bar-arrow-expand, #p1, #af-p1) so every instance of the icon animates, and all revert on mouse-out. They still fall under the global reduced-motion switch below.
@media (prefers-reduced-motion: reduce), all animation and transition durations drop to about 0.001ms (effectively off), and the chat working indicator's mascot renders its static fallback instead of the Rive loop. Components should not check this individually; it is handled uniformly in the global styles. The switch clears durations, not transition-delay: a hover-intent gate (the conversation TOC's 250ms reveal) decides whether hidden content appears, and clearing it would make pointer fly-bys strobe content for reduced-motion users. Layout sizes and responsive breakpoints are tokenized too: sidebar width, content reading-column width, and two global breakpoints. Components should not hard-code pixels.
| Token | Value | Usage |
|---|---|---|
| --p-sidebar-w | 264px | left session sidebar width |
| --p-content-max | 760px | chat reading-column max width (regular chat prose) |
| --p-content-wide | 920px | wide content (settings / panel) |
| --p-table-max | 1040px | desktop wide-table max width (see §04) |
| --p-table-cell-max | 700px | max width of a single table column; longer cell content wraps (see §04) |
| --p-bp-sm | 640px | mobile / desktop boundary |
| --p-bp-md | 980px | narrow / wide screen boundary |
Component primitives are the "smallest correct units" of the site UI. Each primitive exposes variants along only two dimensions — variant / size — with appearance driven by tokens, so it naturally supports light / dark mode and customizable theme colors.
| Scenario | Use |
|---|---|
| Primary action (submit / confirm) | Button variant=primary |
| Secondary action / cancel | Button secondary / ghost |
| Destructive action (delete / abort) | Button danger / danger-soft |
| Status marker | Badge |
| Toolbar filter / model switch | Pill |
| 2–5 mutually exclusive options | SegmentedControl |
| Top tabs | Tabs |
| Switch / multi-select | Switch / Checkbox |
| Scrollable regions with overlay controls | ScrollArea |
| Floating content card / list action menu | Card / Menu |
| Inline notice / global toast | Banner / Toast |
| Dialog / confirmation · bottom panel (mobile) | Dialog / Sheet |
6 semantic variants × 3 sizes. The primary action primary takes its color from the current theme color (§05 can switch between the blue and black families). Radius uses --radius-md uniformly (small size --radius-sm), weight 600, with a visible focus ring. The text variant is the exception to the box: a chromeless inline action — underlined muted text, sized and weighted by its context — for quiet fallbacks such as a copy-link next to a muted label. Use it wherever a native link-styled button would tempt you; never hand-roll one.
<Button variant="primary" size="md" :loading="submitting">Save</Button> + // variant: primary | secondary | ghost | danger | danger-soft | text + // size: sm | md | lg
Unified into three sizes — 26 / 32 / 44px — with the neutral --color-hover wash on hover and a visible focus ring. Replaces the ad-hoc icon + click areas scattered across components today.
sm 26 / md 32; on touch devices the tap target should be ≥ 44px, so use lg 44px, satisfying the §01 accessibility principle (the mobile three-piece set uses lg). Icon-only buttons must also name themselves on hover: pass tooltip (usually the same text as label — label alone only sets the aria-label); bare icon <button>/<a> triggers wrap the Tooltip component directly. Collapsed into two kinds: Badge (status badge, with an optional status dot) and Pill (the clickable pill in the composer toolbar). Radius, font size, and padding are all unified.
Kbd renders a shortcut as keycaps — one block per key, never inline text like (⌘K). Caps are 18px tall (Badge sm rhythm): transparent ground with a 0.5px hairline edge, 11px --font-kbd (Inter + system-ui), text colour inherited from the row that carries it — the cap has no fill or colour of its own, so it follows its context (bright inside the accent-ringed recording box, quiet in a hint row). Typical placement: pushed to the row's trailing edge, opposite the label (e.g. the sidebar search row), and inside dialog navigation hints.
All cards across the site share one structure — head / body / foot — and come in two tiers by visual weight:
0.5px hairline, --radius-md, no shadow. The head is compact mono with no fill, low weight by default, not competing with the conversation.--radius-lg, a faint popover shadow (--shadow-menu), a plain dark title head, and a hairline footer whose actions read in number-key order (chips on the buttons) leading to one solid primary action. No semantic color band.ActionCard.vue in app-ui): one clickable row for pick-one choices (the OAuth login entries, the custom-provider entry). Leading visual slot, title with an optional trailing status Badge, second-line hint, fixed chevron; hover and the focus ring share the site language, and disabled dims + disarms it (the login entries use it while the daemon-support probe is in flight). Consumers never hand-roll a card-shaped <button>.head / body / foot; operation cards are flat + 0.5px hairline + radius-md with no shadow, while the attention card is the single exception — raised surface, radius-lg and a soft shadow, because it floats above the transcript in place of the composer.Unified 38px height (32px small), --radius-md radius, --color-surface-overlay background, and a unified blue focus ring (0 0 0 3px accent-soft). Select is a custom combobox and listbox, not a native <select>; opening it centres the selected option in the scrollable menu. The listbox teleports to <body> with position: fixed — anchored to the trigger (opening toward the roomier side: flipping upward near the viewport bottom and shrinking its max-height to fit when neither side has room; re-anchoring on scroll/resize; closing when focus tabs away) on the --z-modal-dropdown rung, so it floats above modal dialogs and no scrolling container can clip it; while it is open, scroll gestures outside the menu are swallowed so the surface behind it cannot scroll. Listbox options round at --radius-select-option, concentric with the menu frame (--radius-md − --space-1 pad − the 0.5px hairline = 3.5px).
Diff controls: The non-selectable branch summary starts with a 14px branch icon, aligns to the panel header's 12px inset, uses 12px labels, and ends with a 0.5px hairline. List and tree choices use the 14px list and tree-view registry icons. Flat-list and tree-view paths use the UI font at 12px. Tree roots share the flat list's 14px content inset, then each depth advances by 12px and adds a grey indentation rule.
Diff empty state: Centre the clean-workspace message in the available panel height and lead with a quiet 32px status icon.
Diff detail body: the right-side diff detail reuses HighlightedCode unframed (the panel owns the edge and scroll) — shiki highlighting with the language inferred from the file path, an old/new line-number gutter, hunk headers as a muted band, at the shared code size --code-font-size (12px at Medium, one step below body text). The file preview's code body (text / JSON / HTML and Markdown source) renders through the same component with a per-row number gutter plus search-hit / jump-target row states.
Inline code, code blocks, and diff contents use the monospace font (--p-font-mono); diff change counts and branch summaries use the UI font. Code blocks have a filename title bar and a copy button; the action edge uses a compact 6px inset. Diffs use + / - row colors to express additions and deletions — additions use a success light background, deletions use a danger light background, with no gradients.
jwt.verify(token) to verify the signature, returning 401 on failure.import { verify } from './jwt';
+
+ export function auth(token: string) {
+ return verify(token, process.env.JWT_SECRET!);
+ }One dialog primitive replaces 6 hand-written implementations: unified --radius-xl radius, --shadow-xl shadow, 20px head padding, right-aligned footer actions, and an IconButton close button.
sm 360 / md 440 / lg 640 / xl 760 (--p-content-max) — chosen by content weight; sm is for quiet single-purpose dialogs (login, confirm). Height comes in two kinds: auto (default, grows with content up to max-height) and fixed (constant height min(680px, 100vh - 64px), with overflow scrolled inside the body). Content / multi-tab dialogs (settings, model picker, provider manager, folder browser) always use fixed so the frame size stays constant and doesn't jump when switching tabs or content length; short confirmation dialogs keep auto. Selectable controls inside Settings use 0.5px hairlines. Its navigation stays transparent on the grouped canvas — separated from the content region by the 0.5px hairline (horizontal in the stacked mobile layout) — and uses 12px labels at weight 525 with 16px registry icons; the selected tab paints the same neutral --color-hover wash as hover, with the label simply brightening to --color-text — the Kimi app settings nav's recipe (.ss-nav-item--active → Fills-F1, no accent tint, no weight change); section captions use 16px UI text in --color-text. Every setting row has a plain-language description; option labels use --color-text at weight 475 with a 1px gap before that description. Chinese descriptions use “思考” and “计划模式” rather than the English terms; “skills” stays lowercase when it appears within a sentence. Every settings section puts its rows inside one rounded group with 0.5px dividers; the content region paints the flat --color-surface so each group (--color-surface-raised) reads one rung above it — never a sunken pit, which would sink the dialog's content below its chrome in dark. The font-size stepper is a compact 32px UI-font control with 12px values and custom minus and plus buttons. Its 52px desktop row centres the control with equal space above and below. Archived workspace headings reuse the sidebar’s folder-closed registry icon, and Restore actions lead with the undo icon. Archive counts use weight 500; timestamps and workspace paths use the UI font. Dialog backdrop: Use a restrained 28% neutral overlay so the workspace remains legible without competing with the modal.
Settings regions: The settings title and close action belong to the right content region. The navigation is a separate full-height region that starts at the dialog's top edge, not content beneath a dialog-wide header.
Archived sessions: Start with the localized page title. Do not add a repeated English kicker above it.
Settings interaction: Notification labels and descriptions are not selectable; their switches remain fully interactive.
Conversation chrome: Header labels are not selectable; the rename input remains selectable and editable. Branch names start with a 14px branch icon. The overflow trigger is a compact 24px control with a 14px icon. Below a 720px header container, hide the workspace prefix and give the conversation title the available width. On macOS desktop the header doubles as the window-drag region and interactive controls opt out with no-drag; while one of its menus or a dock work panel is open every window-drag strip (chat header, sidebar header, panel header) drops the drag region so an outside press anywhere reaches the page and dismisses the overlay (window dragging is simply paused).
Session search: follows the §09 flush picker anatomy — a boxed Input under the head, and a result list that fills the body's available height and owns vertical scrolling.
Model picker: follows the §09 flush picker anatomy; the provider filter remains horizontally scrollable without showing a persistent scrollbar. Only the model list scrolls; the shortcut bar remains pinned at the bottom.
Unified information architecture: status icon + title + description. The status color appears only on the icon, avoiding large colored areas that create visual noise. For an undoable action there is a second, lighter form — the Action toast (ActionToast.vue): a pill floating top-center just below the 48px header, carrying a one-line sentence whose actions are plain inline <button>s (styled accent by the component), plus close. Self-timed (default 8s, hover pauses); the parent re-keys to reset and wraps it in a <Transition>. First used by session archive (Undo / Settings); warnings keep the bottom-right Toast stack.
Loaders fall into two categories by scenario — do not mix them:
WorkingIndicator — the 小蓝 mascot (KimiMascot, the kimi.com avatar Rive asset, with a static SVG fallback under reduced motion or when the runtime fails) plus a phase label. All other loading states use the plain Spinner.Inline text link: the default is the accent color with no underline; on hover it shows an underline and darkens. File links inside inline code use a 1.5px underline offset so the line stays clear of the chip background. The .muted variant uses the secondary text color. Used for in-text jumps, external links, "view all", and other lightweight actions.
Desktop menus inset their items by --menu-pad (3.5px, so the hairline plus inset lands exactly on the 4px grid), and item corners stay concentric with the frame at --radius-menu-item (--radius-lg − --menu-pad − the 0.5px hairline = 8px — the item arc shares the panel's corner center instead of cutting across it). Standard items pad from the shared --menu-item-padding-block × --menu-item-padding-inline tokens (5px × 9px) with a 7px icon gap. Their three-layer neutral shadow stays below 4% opacity.
Dropdown menu panel: frosted glass — the translucent --color-menu-bg fill over a blurred, saturated page backdrop (--p-menu-backdrop) — plus hairline + light shadow (--shadow-menu, a three-layer neutral ramp). This is the one place glassmorphism is the design language rather than an exception (§06); every floating menu surface (Menu.vue, the Select listbox, composer dropdowns, slash/mention popups) uses the token pair, never ad-hoc blur values. Menu items support icons, the current (active) state, the danger state, and the disabled state, with separators grouping items. All menu actions use 13px labels at weight 475 with 16px leading icons; both share a 16px line box for vertical alignment. Menu timestamps use the UI font. On touch / mobile, use lg (≥44px row height) while keeping the same type size. A dropdown menu pops in from its trigger corner — fade plus a slight 0.97 scale over --duration-base (exit --duration-fast), the composer model dropdown's motion language; the transform origin and the nudge direction follow the anchoring, including the upward flip near the viewport edge.
Row states: hover uses the mode-aware --color-hover wash (it lightens under dark, never darkens); a leading icon sits one rung below the label (--muted), and on hover both label and icon step up to --color-text-strong, the max foreground tier. Selection keeps the accent pair (--color-accent-soft / --color-accent-hover); danger keeps its own colour.
Mutually exclusive short option groups, commonly used for 2–5 option switches such as "light / dark / follow system" or the four font-scale steps. Options may include a 14px registry icon or a colour swatch. A single raised indicator with a soft shadow (no border — the edge stays clean) slides and resizes between options using the standard motion tokens. Three sizes: md (default, settings pages), sm (compact rows), and xs (dense menus such as the composer model dropdown — 20px items, 12px labels).
Linked model + thinking-effort picker (settings → Agent → Subagents, experimental; components/settings/SecondaryModelPicker.vue, shared by both ends). Use it whenever two choices are only valid as a pair — here an effort is meaningless without its model, and every model declares a different supported set. It is a cascading variant of the §03 Select: the trigger is the Select trigger verbatim (value renders model · effort, the unset state uses the placeholder tint), and the dropdown opens as a SINGLE-LEVEL model list (grouped by provider) on the floating menu surface (--color-menu-bg / --p-menu-backdrop / --shadow-lg). The menu teleports to <body> with position: fixed — it opens on top of the settings modal (on the --z-modal-dropdown rung), and only a body-level surface escapes the dialog's scrolling-body clip; it re-anchors to the trigger on any outside scroll and closes on window resize (the UserMenu teleport's full recipe). Hovering or clicking a model row flies its effort submenu out to the RIGHT of the row — same menu surface, anchored to the row's live position, flipping to the left only near the viewport edge per the §03 anchoring rules — with a 250ms hover-intent grace (the UserMenu flyout's recipe) so the diagonal path into the submenu doesn't collapse it. Every model row carries a trailing chevron-right affordance; clicking an effort confirms the pair and closes — one atomic write, never two staggered patches. Flyout options follow the composer's thinking-level model (segmentsFor): effort models get off + their declared levels (always-thinking ones get no off), boolean-thinking models get on/off, unsupported models get off alone; while no effort is set at all, a "Model default" entry leads (it writes the model alone — POST /config merges and cannot clear a stored effort, so the entry disappears once one is set). A configured effort the model no longer declares is appended as an extra flyout option so the current pair stays visible and re-selectable. Keyboard mirrors the Select contract (focus stays on the trigger, Esc preventDefaults so the hosting dialog does not close): ↑/↓ move within the active level (the flyout follows model moves), → opens the flyout, ← collapses it, Enter confirms, Home/End jump. ARIA: combobox trigger → dialog menu holding a model listbox plus the effort listbox flyout with option rows. The menu itself flips upward when the trigger sits near the viewport bottom.
Tabs with a bottom hairline, used for grouping and switching sibling content. The current tab is marked with accent text + an accent underline.
A two-state switch for settings that take effect immediately. The 36×20 track has a 0.5px hairline and full radius; its 16px knob uses 1.5px internal offsets so the visible inset remains 2px and symmetric after accounting for the border. On hover, the knob eases to an 18px rounded rectangle towards the track centre. When on, the track turns accent and the knob slides right.
A 17×17 checkbox. When checked it fills with the accent color and shows a white tick (inline SVG). Often paired with a text label.
A 32px default avatar with md radius; .sm is 24px. Can hold an initial or an icon; falls back to this placeholder when there is no image.
A centered placeholder for empty lists / panels: a 48px faint icon + title + hint, avoiding blank pages.
A 0.5px hairline divider (--p-line); .p-divider-v is the vertical divider, used between inline elements.
A CSS-only hover hint, wrapped in .p-tip. Inverted background (--p-text / --p-bg), single line, no wrapping — carries only short notes.
Component behavior contract (the Tooltip primitive and IconButton's tooltip prop, both backed by TooltipBubble): while any menu surface is open, every tooltip OUTSIDE it hides immediately and no new one may appear — a menu owns the screen, so a trigger's hint must never hang above its own dropdown (native menu behavior). Hints anchored INSIDE an open menu stay live, and ordinary hover behavior resumes once the last menu closes. Menu.vue and the Select listbox register as menu surfaces automatically; a bespoke menu surface (composer dropdowns, the slash/mention autocomplete popups, pickers) wires its open ref + panel element through trackMenuSurface from @moonshot-ai/app-ui.
An inline notice bar placed at the top of a content area. Three states — .info / .warning / .danger — each with a matching 18px icon.
A mobile bottom slide-up panel: xl top radius + drag handle, xl shadow. At ≤640px, dialogs become bottom-anchored Sheets.
A placeholder for loading content, using a breathing opacity animation (no gradients), following the no-gradient-text rule. Composed into titles / text lines / avatars.
An inline combination of "primary action + command text + copy", sitting between a button and a code block — used for install / onboarding / one-click execution. The primary action reuses Button primary; the command area uses a mono light-grey background.
The application top bar. Solid by default; the .frost variant is translucent + background blur, used only for sticky navigation bars. Together with the floating menu surfaces (Menu / Dropdown), it is one of the two exceptions to the no-glassmorphism rule (see §06).
The in-transcript find bar (Cmd/Ctrl+F), implemented by components/chat/TranscriptSearch.vue. A floating card pinned to the transcript's top-right (top: --panel-head-h + --space-3, right: --space-3 — equal inset on both axes), --z-sticky, raised surface + 0.5px hairline + --shadow-menu. One radius for both states: --radius-2xl is a full capsule at the collapsed height and a card once the footer expands — never animate between two radii.
| Part | Rule |
|---|---|
| Input row | Search icon (muted) + bare input — the list-style bare-input exception family (sidebar search row, inline rename), NOT the boxed Input primitive; the 38px bordered control would break the pill. Circular close IconButton sm (concentric with the capsule end); a 0.5px hairline separator before it. Height comes from the grid: 32px control (--space-8) + 2× --space-1 padding = 40px — at which --radius-2xl is exactly the half-height capsule. |
| Footer (results) | Expands via the 0fr→1fr grid fold (--duration-slow), hairline top separator, prev/next IconButton sm left, right-aligned muted count (N/M results · --ui-font-size-sm). Only exists once a query has settled — while typing or empty, the bar stays a bare pill. |
| States | collapsed (empty query) / searching (Spinner sm in the input row during the ~800ms debounce) / results / no-results (count reads "No results", nav disabled). Disabled is uniformly opacity:.5. |
| Focus | Composer-style: a neutral hairline overlay (::after + --color-composer-focus-line) fading in on :focus-within. No accent ring. |
| Match ink | CSS Custom Highlight API — the bar mutates no transcript DOM. All matches: --color-search-match (yellow); current: --color-search-match-current + a 2px --color-warning outline ring (a positioned overlay — highlight pseudos can't paint box outlines). Tokens live in app-ui/style.css with light/dark pairs. |
| Keyboard | Cmd/Ctrl+F opens + focuses (repeat = re-focus + select-all; hardcoded, reserved in the desktop keymap), Enter / Shift+Enter steps matches (wrapping), Esc closes from ANY control inside (container-level, so it never reaches the conversation's Esc-abort). |
| Matching semantics | Rendered transcript DOM only (unloaded older pages are out of scope), capped at 1000 matches (count reads N/1000+). Matches span inline nodes within one block, never cross block breaks; inert and display:none content is excluded. Stepping scrolls the match's own rect into view, not its parent element. |
A small group title for sidebar lists, used to section the content below (such as Workspaces in the sidebar). Spec: 13px / 700 / uppercase / letter-spacing .08em, color --color-fg-faint; left-aligned to the row's starting padding (--sb-pad-x), keeping the same indent as the group rows below. For scripts without case (such as Chinese), text-transform:uppercase simply has no effect — no special handling needed.
The message stream is the core of Kimi Web. Tool calls render as quiet activity lines — one borderless line per call, bespoke per tool kind, auto-grouped, expanding on demand — while Question / Approval elevate to a floating neutral surface because they need a decision, and the Swarm composite keeps a card; the Composer collapses into a single rounded container.
User-message bubbles follow the kimiwork production recipe (MessageItem .user-bubble): a neutral --color-user-bubble-bg fill (BubbleGray — #f5f5f5 light / #292929 dark), uniform --radius-lg corners, no border, no shadow.
Message timestamps use 12px UI text at weight 500, matching the compact metadata scale without switching to a monospace face.
The user-message metadata row sits one 8px spacing step below the bubble, so its actions and timestamp read as supporting information rather than part of the bubble edge.
Overlong user messages clamp at 10 measured lines, the tail dissolving through an alpha mask rather than a tint overlay (the translucent accent fill would double-composite); a floating pill toggle centred on the fade expands in place and collapses back, and the collapse pins the toggle itself so the reading position survives. Skill / plugin command args clamp through the same wrapper, beside the card head. Like the transcript's other disclosure controls (thinking row, turn fold, tool lines), the toggle is a bare native button carrying aria-expanded — chat-surface disclosure controls do not use the §03 Button primitive.
The floating jump-to-latest control uses 12px UI text at weight 525, led by the full down-arrow icon rather than a disclosure caret.
Thinking is an inline, borderless disclosure row in the message stream — never a side panel. The k15 bulb (the thinking registry icon) leads the row in every state; while streaming the "Thinking…" label breathes (opacity only, never a gradient shimmer) and whole elapsed seconds tick beside it, afterwards the label settles to "Thinking process" with the final span as · Ns (renderer-measured, live sessions only — history shows no seconds). Collapsed by default, it expands in place with the standard grid-rows animation and a 90° chevron rotation, and it folds itself back once the stream moves past it, even if the user expanded mid-stream. The header only animates its text colour on hover (standard duration and easing tokens), no card shell.
I looked at the structure of src/auth; it is currently based on a session cookie. The scope of the change is below — once you confirm, I'll start.
src/auth/middleware.ts, 42 lines changed. Allow?Wide markdown tables (desktop): regular chat prose stays within the 760px reading column (--p-content-max), and tables stay there too by default — an overflowing table scrolls horizontally inside its own wrapper, so the page and the chat area never scroll sideways. A clipped table shows a gradient fade at its truncated right edge, and hovering the table reveals a small widen button at its top-right corner; clicking it lets the table grow naturally with its content up to 1040px (--p-table-max), centred within the conversation pane, and clicking again restores the default width. At the default width a single column is capped at 36% of the pane; once widened the cap relaxes to 700px (--p-table-cell-max), so long cell content wraps inside the cell instead of stretching the table. The conversation outline (TOC) keeps its usual position just outside the reading column; when a widened table grows past it and scrolls under the rail, the TOC is hidden temporarily and returns as soon as the table leaves, without touching the user's TOC setting. On mobile a table never breaks out of the reading column.
High-frequency calls like read / bash / grep are "operational noise" — boxed, collapsible cards quickly drown out the conversation. Tool calls therefore render as one quiet borderless line in the message stream — never a card — and each tool kind composes that line for its own content, so the stream reads like an activity log rather than a pile of widgets. The three visual-weight tiers:
src/auth/middleware.ts, 42 lines changed.<button> carrying aria-expanded (keyboard path); the head itself is a plain click target (mouse path), so trailing slots may hold genuine buttons of their own (e.g. Agent's "open detail").--color-text — the one interactive place to go); the action label (Run / Read / Edit…), the mono command / pattern and secondary context all sit at --color-text-muted; auxiliary elements (glyphs, chevrons, trailing meta) stay --color-text-faint. The stream thus reads in three quiet tiers: prose in text, tool lines in muted, thinking / captions in faint. Line content is centre-aligned so mono-only rows (Bash) sit level with the icon and chevron. Truncating line content (the CSS-ellipsis spans) sets --leading-tight rather than the row's line-height: 1 — a 1em line box is shorter than the font's ascent + descent, so overflow: hidden would clip descenders (j / p / g / y); mono runs take the font's own normal leading instead, since JetBrains Mono's ≈1.32em metrics exceed --leading-tight. The 16px chevron still drives the ~24px row height.:line-range or a +N −M stat with a mini segmented bar; Grep shows the pattern in mono plus a match count; Glob / Ls list paths; Todo carries the active task with a done/total progress bar; goal tools show a coloured status pill; WaitFor names the waited / finished task with the task's terminal status as a §03 Badge (a timed-out wait renders as a warning, never an error) and the waited span as a chip; ExitPlanMode expands into a read-only plan receipt with its persisted review outcome. Unrecognized tools fall back to glyph + localized label + argument summary.--shadow-xs, flush with the stream's left edge, ≤560px). The card echoes only the picks, checked with the live QuestionCard's CSS glyph language one step down (14px); passed-over options are not echoed. Dismissed (or zero-answer) collapses to a slim italic one-line card; while running, and for unrecognized output (background launch / error), it stays the plain quiet disclosure line with the raw output.path:line opens the preview at that line). Code-bearing details — the Read content, the Edit diff, the Write content — are syntax-highlighted by file type (github-light / github-dark, following the colour scheme), with the Read output's real line numbers as the gutter; highlighting mounts lazily on first expand and degrades to plain text for unknown languages or oversized content.Read 2 files · Ran 5 commands (1 failed) · 26s), the failure clause hanging on its kind in danger red, the total span faint at the tail — one line, ellipsis-truncated, the full sentence in the title tooltip. Thinking items fold into the run but are not narrated in the sentence. The row shares the thinking row's language (borderless faint text row, text-colour hover only, one whole-row button with a rotating chevron) but rides a roomier 8px vertical padding — 30px against the quiet lines' 22px, so the turn-level summary keeps its presence between prose paragraphs; while the turn streams through the run the row stays expanded and the summary turns live (current action + cumulative per-kind stats + ticking whole seconds), and once every item settles it folds itself back — even if the user expanded it mid-run (the thinking block's vocabulary); a settled → running transition (the stream appending to the same run) reopens it. The glyph carries the state: the current step's own icon breathing while running, green ✓ / red ✕ once settled. A run needs ≥ 2 steps — a lone step renders standalone as the block it always was. Text never folds (it breaks the run), and neither do successful media tools (no card — inline media is the turn's output); everything else folds, cards included: Todo / Goal progress narration, the sub-agent identity card, Question / Swarm cards and unrecognized kinds (skills, MCP tools) all join the run — the stay-expanded-while-live rule keeps a card visible exactly while it is active. The expanded run is the items flat in order (thinking rows + tool rows), each with its own in-row details intact — the lines keep their own 4px row rhythm but breathe 8px apart, with a small inset below the head.TurnFold.vue): when an assistant turn settles, every block before the LAST text block — thinking segments, activity runs, interim text paragraphs, Todo / Goal / sub-agent cards — folds into a single bare row reading Worked 4m57s (whole seconds, no glyph, no summary sentence), expanding into the folded blocks in order, each with its own rendering intact. The span is the turn's ELAPSED time (turnWorkMs): it ticks from the stamped start while the turn is open — approval/question waits included by design, so no park bookkeeping exists — then reads the daemon's own durationMs once settled (the server message stamps for history turns); the wall clock only feeds the live tick, so throttled tabs, session switches and remounts cannot corrupt the settled value. Without any stamp the row falls back to the generic Work details. Streaming turns show no row and a forced-open body — the live transcript is untouched, the fold lands only when the stream moves past the turn (or the turn parks). The split never hides the turn's output: the final text block and any trailing blocks (inline media, standalone cards) stay visible, and a text-only turn folds nothing. Fold state is a plain component ref — nothing persists, switching sessions resets to folded. Inside the right-side sub-agent transcript (ChatPane's inspector mode), the turn fold and the run-end footer are suppressed entirely and activity runs and thinking blocks stay pinned open (their heads demoted to plain captions) — an inspection view exists to show the whole trajectory, so nothing folds away — and disclosure bodies open instantly while their chevrons retain the standard rotation: animating the height of a full historical stream would relayout the entire panel on every animation frame.Question and Approval — they genuinely need the user's attention. The Swarm composite keeps one quiet card (raised surface, 0.5px hairline, large radius) for its phase overview + member accordion.NotificationCard.vue): the hidden <notification> injections (background-task / sub-agent settlement) render where they landed in the turn — a 28px status chip + title/sub head tinted with the toast status token pairs (completed → success, failed / timed_out / lost → danger, killed → warning, else neutral surface), expanding in place to the fields, the body, an output-file row (copy path) and the raw payload. ≥2 CONSECUTIVE notifications merge into one neutral group card (count + per-item status dots + compact rows, each expanding on its own). Notifications break the activity run but are never turn boundaries, and they never fold — a notification is an event worth noticing, not process noise, so it punches out of the turn fold and renders right after the fold row, in order..turn-failed): the notification card's danger shell (danger-soft surface, danger hairline, 24px status chip with the warning glyph) carrying a title keyed by the wire error kind (model failure vs step-limit stop), the provider message as a muted sub, a mono diagnostics meta (code · HTTP status · request id), and exactly ONE secondary sm action — Continue, which submits a short continue prompt through the normal path. It renders only while the session sits idle on lastTurnReason === 'failed' (a turn with zero assistant output included, so it pins to the tail rather than any assistant row), it is not dismissible, and it vanishes the moment a new turn starts. While the turn is still fighting, the working indicator instead narrates the retry backoff ("retrying n/max" from the live agent.status.updated phase) — a retrying turn never shows the card. The transient error toast now fires only for background sessions; the viewed session's failure is fully covered by the card.goal_continuation trigger (goal mode's self-driven next turn — a turn boundary, unlike task notifications) never renders its machine prompt; instead the assistant turn it opens shows one faint 12px line flush with the stream's left edge — the target glyph shared with the Goal tool (this turn belongs to the goal) + a localized label — ABOVE the turn's content and OUTSIDE the turn fold, so the row survives as the turn's provenance after settling. The marker lands with the trigger (before the first assistant block), and while the newest exchange is a goal-continuation turn the undo affordances (edit-and-resend, Esc undo) are suppressed — rewinding would drop the hidden trigger while refilling the older user text.TurnFilesSummary.vue): between the turn's final text and its footer, a §03 Card (hairline border, no shadow — NOT the quiet tool line, the artifacts are worth a discrete object) lists every file the turn's Edit / Write calls touched. The head reads "N files changed" with the aggregate +A −D and the mini diffbar; the aggregate hides whenever any row's stats are incomplete (a Write or an underivable edit makes the total a lower bound, never presented as exact). Each row is one clickable workspace-relative path (short and self-locating; a file outside the cwd stays absolute) with its per-file +A −D at the right edge. The row's action keys on the tool kind, and the stats tell it apart: a Write has no per-file count (its diff is underivable) and opens the whole file in the preview; an Edit / MultiEdit carries its +A −D and opens that file's turn diff in the right-side detail layer (TurnDiffPanel.vue — the turn's own X→Y change, not the git diff), whose header keeps an open-file action. The first three files show inline; the rest collapse behind a "N more files" ghost-button row in the card's foot. Where nothing handles the row action (the BTW side chat), the card renders its file rows as plain text instead of links.The two attention cards replace the composer in the dock and share one contract: a floating neutral shell (--color-surface-raised + hairline + --radius-lg + --shadow-menu), a plain dark 16px title head, and a hairline footer whose actions read in number-key order with exactly one accent primary. There is no semantic colour band — the floating card itself is the "needs a decision" signal.
--p-chip-num over --color-inline-code-bg, the same chip vocabulary as option rows and the multi-step chip; exactly one primary action, the rest are ghost. Feedback mode swaps the whole footer for submit / cancel.Textarea primitive (no bespoke input), three rows at rest, auto-growing with its content UPWARD — the card is bottom-anchored in the dock, so the bottom edge and the submit / cancel footer never move — capped at 40% of the VISUAL viewport, then scrolling internally. Height budgets follow --app-height (the visual viewport, which shrinks with the iOS keyboard where dvh does not): the card caps at calc(var(--app-height, 100dvh) - 72px) and the dock takes over the same budget. While the feedback box grows, every body kind yields and scrolls internally by default instead of pushing the card past its cap — the plan scroll area, code previews, shell output, todo lists, invocation chips and generic text — with only the one-line plan path pinned at full height. A plan review's option rows belong to the plan region and never shrink, so once fixed chrome (options, grown feedback box) exceeds what the capped card leaves — e.g. the iOS keyboard shrinking the visual viewport — the WHOLE plan region (body and options together) becomes one scroller, keeping an approve option reachable by scrolling instead of clipped out of reach. Below even that budget — the cap under the fixed chrome alone (header plus mobile's stacked ≥46px submit / cancel buttons), as when an iOS landscape keyboard leaves a ~200px visual viewport — the body has already shrunk to zero and the CARD itself becomes the scroller of last resort (overflow-y: auto, with its own scroll seam), so the footer buttons are never clipped away.HighlightedCode (syntax-highlighted, 24-row cap with scroll); Edit approvals render the before/after hunk as a highlighted line diff. Plan / diff / file kinds get a head expand toggle that lifts the cap so the block fills the card; the card itself never exceeds the pane (only the scroll area shrinks) — with the dock work pills visible, the dock takes over the same height budget as a flex column, so an expanded card yields the pills' height instead of pushing them past the pane's top edge. Once the plan scrolls, a soft shadow fades in at the scroll area's top edge — the sidebar's scroll-linked seam language, so clipped content reads as passing under the card chrome.danger-soft filled hint row under the command — detection is a display-layer heuristic on the client.Unified into a single raised container: --radius-composer (32px) with --corner-shape-composer: superellipse(1.5) and a stable 0.5px edge. Focus crossfades a low-chroma line-and-accent edge over --duration-slow with --ease-in-out, while the neutral shadow stays unchanged — there is no added halo and no layout shift. The composer input (a ProseMirror contenteditable on desktop, a textarea on the web during the migration) uses text-autospace: normal for mixed CJK and Latin input. Toolbar controls use a quiet 32px full-round geometry with 8px edge inset; the send button remains a standard 32px circle, with its glyph at 28px (--composer-send-icon-size, the production kimi.com size; it sits outside the --p-ic-* scale on purpose).
Fill and edge tokens: the card's fill and rest border are their own tokens — --color-composer-bg and --color-composer-line — running the kimiwork / kimi.com production input recipe (.chat-input__shell): fill = groupedBackground.secondary (#ffffff light / #1f1f1f dark), rest border = separator.s1 (13% black / 12% white), focus line = fills.f4 (25% in both schemes), and --shadow-input = effect.shadow.inputDefault (0 5px 16px -4px rgba(0,0,0,0.07), kept identical in dark — the hairline carries the edge there). Only colours sit in the tokens; the 32px superellipse shape and the focus-only edge overlay are unchanged.
Send button tokens: the send circle runs on --color-send-bg / --color-send-bg-hover / --color-send-icon (+ *-disabled, --opacity-send-disabled, --shadow-send[-hover]), following the production recipe (.chat-input__send): a neutral labels.primary fill (90% black light / 84% white dark, hover #252525 / 84.8%) with the production lift shadow (0 7px 16px -13px 38% + 0 1px 2px 7%, one step larger on hover), a groupedBackground.secondary glyph, and a disabled state of the same vocabulary — fills.f2 fill with a labels.quaternary glyph at full opacity. The button is disabled exactly when submit would no-op — an empty draft with no ready attachment (image-only sends stay enabled), an upload in flight, or the starting spinner — so disabled is a first-class persistent state, never a fade.
Layering, anchors, and motion: the dock normally stays at --z-sticky so the Latest Messages pill can remain visible above its veil. While any Composer popup or work panel is open, the dock temporarily joins --z-dropdown, ensuring permission, work-mode, and model menus — and the work panel — always paint above that pill. The permission menu's left edge and the model menu's right edge each follow their own trigger pill. All three menus use --shadow-menu and the same trigger-corner pop motion as Session Row menus: 0.97 scale with a 2px shift toward the trigger, --duration-base on entry, and --duration-fast on exit.
Attachment strip: attachments hang inside the composer card above the input as two grouped rows — images/videos as shared MediaThumb rounded thumbnails, files as the shared AttachmentChip pill — the same pair the sent bubble renders, so a draft looks exactly like the sent message. File-store videos render a static play tile instead of fetching a first frame. The strip caps at two thumbnail rows and scrolls beyond that instead of pushing the input down; while overflowing, a quiet count badge pins to the bottom-left and new attachments auto-scroll into view (to the end of whichever group grew). With two or more attachments, a one-click clear-all pins to the strip's top-right corner as a quiet 22px badge (trash glyph, danger on hover). The composer's pending preview and the bubble's media clicks open the same MediaLightbox preview, which owns Escape via the shared dialog stack: images go through PhotoSwipe (@moonshot-ai/app-client's lib/mediaPreview) and zoom out of the clicked thumbnail (scrim = --color-scrim-strong, caption = --color-text-on-scrim; the slide area is inset — 24px sides matching the video modal, 56px top/bottom clearing the close button and caption — so a viewport-filling image never kisses the edges), videos keep the custom modal. Both share the --color-scrim-strong backdrop and the same close button — the raised 36px circle (.media-lightbox-close) fixed at the viewport's top-right, rendered by MediaLightbox for both (PhotoSwipe's own top bar is disabled; zoom stays on wheel / pinch / image click). ReadMedia tool cards open it too (an App-level instance fed by the openMedia chain): the image zooms out of the card's thumbnail, and videos show as a static play tile that opens the modal player — no more right-side-panel detour or inline <video>.
Mention pills: @-mentions render as inline pills in the editor — one element of the cross-surface rich-text vocabulary specified in §05 Rich Text Messages (visual recipe, wire forms, and behavior contract live there).
',12)),a("div",ye,[e[44]||(e[44]=a("div",{class:"stage-bar"},[a("span",{class:"st"},"Composer")],-1)),a("div",ke,[a("div",xe,[e[42]||(e[42]=a("div",{class:"p-composer-ta ph"},"Message Kimi, / to run a command, @ to reference a file…",-1)),a("div",Te,[a("div",Se,[e[39]||(e[39]=a("button",{class:"p-icon-btn"},[a("svg",{class:"p-ic",viewBox:"0 0 24 24",fill:"currentColor"},[a("path",{fill:"currentColor",d:"M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"})])],-1)),a("span",Ce,[d(s(o),{name:"shield-question",size:"sm"}),e[38]||(e[38]=t("yolo",-1))]),e[40]||(e[40]=a("span",{class:"p-pill"},[a("svg",{class:"p-ic",viewBox:"0 0 24 24",fill:"currentColor"},[a("path",{fill:"currentColor",d:"M8 4h13v2H8zM4.5 6.5a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3m0 7a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3m0 6.9a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3M8 11h13v2H8zm0 7h13v2H8z"})]),t("plan")],-1))]),e[41]||(e[41]=i('--color-text fill with a --color-bg glyph (never the accent), disabled while the input is empty or an upload is in flight. The transparent dock floats over the transcript, while the scrolling content receives bottom padding equal to the live dock height so its final item can still clear the composer. Composer chrome is not selectable; only the message input permits text selection. Each permission mode has its own registry icon — manual hand, yolo shield-question, auto full-access — paired with the label in the pill (collapsing to the accessible icon below a 620px composer container) and leading its dropdown row in the mode's colour, with the current row's check trailing the row's end. The right toolbar is the flexible region: the model pill shrink-wraps its content, then shrinks and truncates internally only when the toolbar runs out of room. The dock's work pills and panels — pill vocabulary, panel shell and motion, the two-tone head, per-kind bodies, filtering, and the open/cancel model — are specified in Dock · work pills & panels below. Workspace attachment card: on the empty session, the workspace picker is a separate attachment card tucked under the composer — and the composer card itself stays complete (its own 0.5px border, --radius-composer corners with --corner-shape-composer, and shadow are never altered). The attachment lives inside the composer's padding box as the card's sibling, so its width always matches; its top --space-4 slides behind the card (the card is raised to --z-sticky), its square top edge stays hidden, and only the rounded bottom (0 0 --radius-xl --radius-xl) shows. Background --color-hover at 60% via color-mix (≈0.03 black in light, self-adapting in dark), no border, no shadow. Inside sits one quiet capsule trigger: transparent, --radius-full, 16px leading icon and 12px label at weight 475 in --color-text-muted; hover deepens to --color-selected and the label turns --color-text. The dropdown follows the §03 menu spec and is viewport-aware (flips above when more room, clamps max-height to the scrollport); at --z-dropdown it outranks both the card and the fixed click-outside backdrop (--z-sticky), which renders outside the composer because the card's container-type captures position: fixed descendants.
The slash, mention, and add popups share one geometry, all on dedicated tokens: the --color-menu-bg-frost surface (the frostier recipe), frame padding --menu-row-hug, rows at --menu-row-padding-block × --menu-row-padding-inline with --radius-menu-row caps (plain corners — the frame is --radius-lg with NO corner-shape, and the row radius stays concentric: 12px frame − 6px hug = 6px), an icon-to-label gap of --menu-row-gap-icon, and a --menu-rows-seam between stacked rows. Touch rows pad to --menu-row-touch-padding-block with a hard floor of --touch-target-min. Scroll height caps at --p-slash-menu-h / --p-mention-menu-h / --p-add-menu-h; the scroll-edge fade is --menu-scroll-fade, and the overlay thumb rides --menu-scrollbar-width / --menu-scrollbar-edge / --menu-scrollbar-track-inset / --menu-scrollbar-thumb-min in --color-menu-scrollbar (hover --color-menu-scrollbar-hover) — 3px visually, with a wider invisible drag strip.
The composer's + button opens the add menu — the autocomplete family's action-list member: one column of icon + label (+ muted description) rows (Files, Goal, Plan, Swarm) on the same frost surface and row geometry as the slash/mention popups, capped at --p-add-menu-h. Semantically it is an action menu, not an autocomplete listbox: rows are menuitem commands, DOM focus moves into the menu (arrows navigate, Enter activates, Escape closes), and the + button carries aria-haspopup="menu" — the textarea's combobox ARIA never points at it.
Design decision: deliberately NOT the §03 Menu/MenuItem primitives. Those are the trigger-dropdown family (sidebar, user menu, session rows) — --color-menu-bg, --radius-lg, a min-width box. The add menu instead shares the composer menus' material (the frostier surface, the composer corner curve, rows hugging the frame at the composer's text column, muted description sub-lines). Bending MenuItem into that material would require exactly the per-screen appearance overrides the primitive contract forbids, so the add menu keeps bespoke rows built directly on the shared --menu-row-* tokens. This paragraph is the canonical record of that choice — reviews should not re-litigate it.
Plan and Goal are the primary work modes — mutually exclusive, at most one armed at a time. Arming happens only through the slash commands (/plan, /goal) or the add menu; an armed mode renders as the input row's leading pill — a neutral --color-surface chip with the mode's 14px icon, its label, and a × that disarms. The × is an IconButton sized --wm-x-size (below the sm default, so its hover wash never outgrows the pill's rounded end) with a --wm-x-ring hit reserve that stays inside the textarea's indent; on touch it meets --touch-target-min. The input's first-line text indent reserves exactly the pill's width (the desktop ProseMirror editor applies it to the first paragraph), so the caret and placeholder slide right rather than colliding; arming Goal also swaps the placeholder to its objective prompt. Sending with Goal armed creates the goal and the pill hands off to the dock's goal pill; a live goal does NOT lock the primary modes — /goal then just focuses the goal's panel, and /plan still arms (the only hard rule: one message cannot both create a goal and enter plan — the goal write carries the plan disarm atomically — and a failed mode write is treated as a send failure: the optimistic message rolls back, the error toasts). Swarm is deliberately NOT a mode: an orthogonal toolbar chip with its own enable confirmation, disarmed from the chip's ×.
Work pills (WorkPill.vue): the dock's workbar above the composer carries one pill vocabulary — font-driven by flex computation: the 1.5em leading icon and the label's own line box (--text-base at --leading-normal) both come to 21px at the 14px UI text, and 8px block padding wraps them to 37px, nothing pinning pixels, so the pills rescale with the font; borderless, with --radius-lg rounded-square corners, filled at the fills ladder's --color-selected rung over the shared --p-menu-backdrop blur — the panel's frosted recipe scaled down to a chip, so transcript text never reads through the light fill (the neutral hover wash layers on top — one layer, never two), the icon at full text colour, --space-2 / --space-3 block / inline padding — the trailing side adds a --space-05 optical compensation against the leading glyph — and a --space-1-5 content gap. Every pill stays expanded — the Code client does not collapse status chips — carrying icon + label and a trailing meta that answers one question — what is live on this surface right now — for background bash tasks, background sub-agents, todos, and the goal alike; the row insets to the composer's text column — dock inline inset + the card's 0.5px border + the input wrap's 16px inline padding — not the card edge. The active pill keeps the wash on permanently — one fills-ladder step deeper, neutral. The meta is always muted ink and surface-specific: the goal carries its status word, colour-coded by state (active --color-success, paused --color-warning, blocked --color-danger); the bash and sub-agent pills carry a pulsing dot and the running count, only while something runs (a quiet absence otherwise); the todos carry the done/total fraction. The plan pill joins the row only once plan mode is live server-side or a persisted plan exists — a merely armed directive stays in the composer's inline work-mode pill (its × cancels) and never reaches this bar, so every pill here reads as live server truth rather than local intent. Narrow panes wrap the row instead of clipping (the dock height observes); below the --p-bp-sm breakpoint the pills collapse to their icons — label and meta hide (the threshold is read from the token, never a hardcoded width).
Panel shell & motion: a pill toggles the shared work panel — the menu family material: 70% page background via color-mix over the shared --p-menu-backdrop blur (the frostier recipe, see §06 Glassmorphism exemption), a 0.5px --color-line edge, --radius-2xl, and the menu panel's --shadow-menu. The panel pops from the clicked pill along the trigger-corner motion tokens (--motion-panel-scale / --motion-panel-shift), and switching pills replays the pop from the newly clicked pill (the shell is keyed by panel kind); while it is open the dock raises to --z-dropdown so it clears the transcript's new-message pill, and every window-drag strip pauses so an outside press reaches the page and dismisses. The panel owns Escape while open (a document-capture handler guarded by the shared IME latch, so a candidate-cancelling Escape is never swallowed), and a scrolled body dissolves toward the head through the --menu-scroll-fade alpha mask instead of hard-clipping. Height is content-sized up to min(360px, 50vh); the filtered panels pin instead — see Filtering & sizing. The panel is chrome: nothing inside it is text-selectable, the head's filter chips included.
Panel head (WorkPanelHead.vue): every work panel head is one tab row — a leading icon at the pill's 1.5em glyph size, the panel title at full text colour, and a muted trailing meta (--color-text-muted) carrying that panel's one live number, joined by the row's --space-2 gap with no separator glyph: the goal's wall-clock time, the bash / sub-agent running count, the todos' done/total. Actions right-align in the head: the goal's pause / resume / cancel / close as quiet neutral IconButtons (cancel deliberately shares that neutral vocabulary), the plan's open-in-side-panel, dismiss-directive (shown only while the directive is live), and close, and the bash / sub-agent filter chips as a SegmentedControl. The plan head's meta carries the latest plan's review outcome. On touch these meet the 44px minimum (--touch-target-min) via the hover: none capability query — a width-only gate would miss tablets — and below 480px the head wraps so the actions take a full row.
Panel bodies: todos read as a quiet list — a green circle-check for done, a hollow ring for pending, an xs Spinner for in-progress. The goal's detail (full objective, completion criterion, rendered with the chat Markdown renderer) fills the body — no footer strip. Bash tasks are long rows (TasksPane.vue): a StatusDot while running, circle-check when done, a close glyph for failed and for cancelled (a user stop is neutral, never reported as a failure), the command as the meta line, and a bare duration that renders only when computable — never the raw protocol status word. Sub-agents form a card grid (SubagentGrid.vue) — an auto-fill grid (minmax --p-subagent-card-min) of cards at the --color-selected rung with --radius-lg corners; each card carries the task name, a stable session-wide number (creation order across the session's background sub-agents — unique even across swarms; once shown it sticks, so late-arriving history takes the next tail number), an optional prompt meta line one size down (--text-sm), an icon-led model · effort line and the status row a further size down (--text-xs) — the status row pairs the task-row state glyph with the localized label, plus a clock-led bare duration that renders only when computable; the full-bleed circle-check (drawn for the todo rows' ring family) is scaled onto the shared icon grid wherever it sits beside other glyphs (task rows, cards, filter chips). A failed row tints its name --color-danger; a cancelled one stays neutral. Rows hover with a rounded strip that grows by padding and a matching negative margin — the row metrics never shift. The row stop is danger-coloured; the card cancel reveals on hover and is always visible on touch as a --touch-target-min corner target. Every pane's empty state is centered and muted. State and time labels — the head meta included — set text-autospace: normal, the transcript's mixed CJK/numeric spacing rule, so durations like 9小时2分 breathe.
State vocabulary: two glyph families meet here by design — the activity dot (shared with the transcript's tool rows and swarm members) backs the task rows, the todo rows speak the default loader ring, and the sub-agent cards skip glyphs in favour of a localized text label:
',8)),a("table",De,[e[89]||(e[89]=a("thead",null,[a("tr",null,[a("th",null,"Surface"),a("th",null,"State"),a("th",null,"Meaning"),a("th",null,"Glyph"),a("th",null,"Ink")])],-1)),a("tbody",null,[a("tr",null,[e[54]||(e[54]=a("td",null,"Bash rows",-1)),e[55]||(e[55]=a("td",{class:"tk"},"running",-1)),e[56]||(e[56]=a("td",null,"Task in flight",-1)),a("td",null,[d(s(u),{status:"running"}),e[53]||(e[53]=t(" pulsing dot",-1))]),e[57]||(e[57]=a("td",null,[a("code",null,"--color-accent")],-1))]),a("tr",null,[e[60]||(e[60]=a("td",null,null,-1)),e[61]||(e[61]=a("td",{class:"tk"},"done",-1)),e[62]||(e[62]=a("td",null,"Finished cleanly",-1)),a("td",null,[d(s(o),{name:"circle-check",size:"sm"}),e[58]||(e[58]=t()),e[59]||(e[59]=a("code",null,"circle-check",-1))]),e[63]||(e[63]=a("td",null,[a("code",null,"--color-success")],-1))]),a("tr",null,[e[66]||(e[66]=a("td",null,null,-1)),e[67]||(e[67]=a("td",{class:"tk"},"failed",-1)),e[68]||(e[68]=a("td",null,"Errored — the row's name tints to match",-1)),a("td",null,[d(s(o),{name:"close",size:"sm"}),e[64]||(e[64]=t()),e[65]||(e[65]=a("code",null,"close",-1))]),e[69]||(e[69]=a("td",null,[a("code",null,"--color-danger")],-1))]),a("tr",null,[e[72]||(e[72]=a("td",null,null,-1)),e[73]||(e[73]=a("td",{class:"tk"},"cancelled",-1)),e[74]||(e[74]=a("td",null,"User stop — neutral, never a failure",-1)),a("td",null,[d(s(o),{name:"close",size:"sm"}),e[70]||(e[70]=t()),e[71]||(e[71]=a("code",null,"close",-1))]),e[75]||(e[75]=a("td",null,[a("code",null,"--color-text-muted")],-1))]),a("tr",null,[e[77]||(e[77]=a("td",null,"Todo rows",-1)),e[78]||(e[78]=a("td",{class:"tk"},"in_progress",-1)),e[79]||(e[79]=a("td",null,"Being worked on",-1)),a("td",null,[a("span",He,[d(s(D),{size:"xs"})]),e[76]||(e[76]=t(" xs Spinner ring",-1))]),e[80]||(e[80]=a("td",null,[t("the row's own ink ("),a("code",null,"--color-text"),t(")")],-1))]),e[87]||(e[87]=a("tr",null,[a("td"),a("td",{class:"tk"},"pending"),a("td",null,"Not started"),a("td",null,[a("span",{class:"dw-ring"}),t(" hollow ring")]),a("td",null,[a("code",null,"--color-line-strong"),t(" stroke")])],-1)),a("tr",null,[e[83]||(e[83]=a("td",null,null,-1)),e[84]||(e[84]=a("td",{class:"tk"},"done",-1)),e[85]||(e[85]=a("td",null,"Completed",-1)),a("td",null,[d(s(o),{name:"circle-check",size:"md"}),e[81]||(e[81]=t()),e[82]||(e[82]=a("code",null,"circle-check",-1))]),e[86]||(e[86]=a("td",null,[a("code",null,"--color-success")],-1))]),e[88]||(e[88]=a("tr",null,[a("td",null,"Sub-agent cards"),a("td",{class:"tk"},"all"),a("td",null,"The task-row glyph plus a localized label — failed alone tints danger — with a clock-led bare duration trailing"),a("td",null,"glyph + label"),a("td",null,"muted / danger")],-1))])]),e[146]||(e[146]=i("Filtering & sizing: the bash and sub-agent heads carry the same four icon-led filter chips — recent (clock; running + the five most recently finished), running (play), done (circle-check; every terminal state), all (list) — and both filtered panels pin to --p-dock-panel-h so filtering never resizes the panel; the body scrolls inside. When the head runs out of room the chips collapse into a dropdown menu (FilterControl.vue measures the head itself, so the switch follows the panel's own width, not the viewport; the menu teleports to <body> and anchors to the trigger, so the panel's clip and backdrop-filter never squeeze it); the head title never wraps and is never clipped — once the filter is a dropdown the head always fits. An empty pane names its filter (no completed tasks is not no tasks).
Opening & cancelling: clicking a row or card opens the task's detail in the right-side panel. Opening is gated: the overlay navigation button renders only when a stable agentId (on-demand transcript) or locally held output exists, so a REST-only cold-loaded row stays inert instead of opening an empty panel — and says so with the not-allowed cursor. Open and cancel are sibling controls (a full-cover overlay button, the stop IconButton floating above it): no nested interactives, and the stop action exists only while the task runs.
Structured references travel as plain text on the wire and render as pills at both ends: the composer's editing surface and the rendered message stream share one pill vocabulary, so what you type is what the message shows. This section is the category spec — every future rich-text element (marks, embeds, interactive chips) joins it here.
The mention is the first rich-text element. It exists in two synchronized forms: an atom inside the desktop composer's ProseMirror document, and a pill in the message stream — assistant Markdown decorates local links with the same classes, and user/queue bubbles (verbatim wire text, never Markdown) render through the declarative ComposerText component. Both are the same mark: not a filled chip — no background, no vertical padding — just body text in a heavier weight (--weight-ui-strong) on a lighter ink (--color-text-muted), so the baseline stays flush with the surrounding text; a 2px horizontal inset (padding-inline: var(--space-05)) supplies the CJK/Latin autospacing that can't cross element boundaries and sets the mention apart from plain text; a 13px muted kind glyph leads, and the label is the basename — never the full path, which lives on the tooltip. Hover deepens the ink (glyph included) toward --color-text on every surface; in the message stream the clickable kinds (file, skill) also take the pointer cursor and the link underline, while the composer keeps pure editing semantics — I-beam, no underline, a click just places the caret. The shared classes live in app-ui's global sheet; the kind → glyph mapping is single-sourced in app-composer (mentionIcons) and also drives the mention menu rows, so a file looks identical in the menu, the editor, and the sent bubble. Copying from a bubble re-serializes the selection back to the wire text — pills carry their full attrs in data-mention-*, so a copy/paste round trip restores the link instead of a truncated basename.
Kinds and wire forms (serialization is a Markdown link, so the daemon payload stays plain text and the TUI needs nothing new):
| Kind | Glyph | Wire form | Click (in messages) |
|---|---|---|---|
| File | single folded-corner file glyph for every file (no per-extension variants) | [name](path) — the dest is canonically encoded per path segment (every non-unreserved ASCII character → %XX; non-ASCII stays literal, so a CJK path reads as itself — one decodeURIComponent restores it on every surface) | opens the file preview |
| Folder | folder glyph | [name](path/) — trailing slash marks the kind (dest % → %25 as above) | inert (no target yet) |
| Skill | sparkling glyph | [name](kimi-code://skill/<name>) — the app's deep-link protocol | opens the skill's SKILL.md in the preview panel |
Hover tooltip: one document-level singleton (mentionTooltip) serves every pill — composer NodeViews, ComposerText-rendered bubbles, and Markdown anchors are all raw DOM a Vue wrapper can't reach, so it delegates on document mouseover into a single shared bubble. The bubble keeps the design-system tooltip's dark skin but is interactive. File and folder pills show the full path, wrapping anywhere within a fixed max width: every / separator muted, the basename bold (--weight-semibold). Skill pills show a card — the name with an open button at the right (opens the skill's SKILL.md in the preview panel; the path rides the wire skill descriptor through AppSkill.path), the description below, clamped to four lines; an unresolvable skill degrades to the name alone. Timing mirrors TooltipBubble (150ms show delay, top placement with flip, viewport clamping) and the bubble stays open while hovered so the button is reachable; native title tooltips are removed wherever a pill appears. The bubble is a documented structural exception to the component-primitive rule (like the dock overlay): its anchors are ProseMirror NodeViews and pillified spans a Vue wrapper can't reach, so the open/copy buttons re-implement the Button primitive's contract (size, hover, :focus-visible ring) by hand instead of importing it.
Edge cases: labels cap at 32 chars — a longer name takes a middle ellipsis that keeps the head of the base name and the whole extension (the full name stays in the data attributes, the full path on the tooltip). A pill whose target was deleted after the fact: hovering fires a one-byte existence probe (an inline spinner sits at the tail of the tooltip's path text while in flight), and a definitive not-found fades the pill and strikes it through — in messages and in the composer alike. Clicks are never gated on the verdict (the preview's own not-found state is the final answer); only confirmed-existing verdicts are cached, scoped to the session, so a recreated file recovers on the next hover and a flaky daemon can never strike a pill by mistake.
Behavior contract: a bare @ opens the menu instantly with the workspace root listing (Esc dismisses); with a query, the menu is ONE merged list — no sections — ranked by match strength: exact skill > prefix skill > strong file hits (substring-or-better on the basename; a query containing / matches path segments and is always strong) > substring skill > subsequence skill (tokens of 3+ chars, separator-bridging — larkim matches lark-im) > weak file hits (a bare subsequence), with the search firing per keystroke (rg-backed fs:suggest, no debounce; older daemons fall back to fs:search). An in-flight search never hides the current rows (a corner spinner marks it; superseded rows dim as stale and stay unselectable until fresh results land). File and skill rows highlight the matched characters with ink emphasis only — semibold strong ink in the name, body ink in the muted directory, never a background, so the row rhythm never shifts. The default highlight is simply the top row of the ranking — skill rows included — and follows its row by identity across async landings. Full-width @ from IMEs triggers identically. Insertion replaces the @token and adds a separating trailing space; the pill is one atom — Backspace removes it whole, and a zero-width caret anchor keeps the caret on the pill's line when it ends a paragraph. Drafts, history recall, and queue reloads revive pills from their link form on load, and pasting mention-link text (e.g. a copied pill) revives them too — the serializer round-trips both ways. Skill activation: sending a message with exactly one skill pill activates that skill via the existing channel (the pill form of /skill:<name>, the full text — the pill traveling as its mention link — becomes the args, attachments ride along), and the sent bubble shows the original message verbatim with the pill revived in place (a slash-typed activation, whose bare args carry no pill, keeps the identity card instead); two or more skill pills degrade to plain references, because each activation is its own turn.
Implementation map: schema/serialization/offset mapping in app-composer's composerTextDoc (pure, node-tested); the editor surface in composerEditor; user/queue bubbles render via app-composer's ComposerText (one segment pass, declarative tree — no post-processing); assistant-side classification (classifyMentionHref) and decoration in app-markdown's Markdown.vue link pass; hover + skill-click routing in app-composer's mentionTooltip singleton, wired per app shell. When editing these, keep the two surfaces in lockstep — a pill that serializes one way in the composer must read the same way in a message.
Kimi Web uses one unified theme: the same components, fonts, radii, shadows, and surfaces — theming only swaps color values. Every semantic color token ships a light value in :root and a dark override in the data-color-scheme blocks; the semantic status colors (success / warning / danger) are independent palettes, one set each for light / dark.
The app has one accent: the brand blue (--color-accent, #1783ff light / #58a6ff dark). Use it sparingly — the accent is reserved for the primary action, focus rings, links, and active marks (current tab, toggles); large fills always come from the neutral surface tokens. Selection that means "where I am" (sidebar rows, list pickers) is deliberately NOT accent-tinted — it uses --color-selected so it reads as location, not as an action.
Each semantic token ships a light value in :root and a dark override in the two data-color-scheme blocks (explicit choice, or following the OS preference via prefers-color-scheme). Switching light / dark simply swaps between these two sets of derived tokens, with zero structural change.
Anti-pattern rules that all UI code must follow. These rules are also the basis of the check-style detection script, one-to-one with a warning.
| Rule ID | What it detects | Action |
|---|---|---|
| no-gradient-text | gradient text / gradient background | Forbidden |
| no-glassmorphism | backdrop-filter: blur (TopBar sticky nav bar and menu surfaces via --p-menu-backdrop are the exceptions) | TopBar + menus exempt |
| no-color-glow | colored / large-radius box-shadow glow | Forbidden |
| no-emoji-icon | using emoji as a functional icon (no exceptions). Emoji inside user content — session titles, messages — is not chrome and is out of scope (see §07 Session row's emoji icon) | Forbidden |
| no-hardcoded-hex | unregistered hex color inside a component <style> | Warning |
| no-hardcoded-font | hard-coded font-family in a component (e.g. 'Inter') instead of var(--font-ui) | Warning |
| radius-from-scale | radius value not in {4,6,8,12,16,20,999} | Warning |
| z-from-scale | z-index using an unregistered large number | Warning |
| weight-from-scale | font-weight not in {400,500} | Warning |
Every interactive primitive should define the following states where applicable; missing ones are flagged by the style rules. focus-visible always uses --p-focus-ring (appears only on keyboard focus, see §08); disabled is uniformly opacity:.5.
| State | Button | Input | Card | Menu item | Switch |
|---|---|---|---|---|---|
| default | ✓ | ✓ | ✓ | ✓ | ✓ |
| hover | ✓ | ✓ | ✓ | ✓ | — |
| active / pressed | ✓ | — | — | — | — |
| focus-visible | ✓ | ✓ | — | — | ✓ |
| disabled | ✓ | ✓ | — | ✓ | — |
| loading | ✓ | — | — | — | — |
| selected / active | — | — | — | ✓ | ✓ |
| error | — | ✓ | — | — | — |
| readonly | — | ✓ | — | — | — |
WorkingIndicator component: the 小蓝 mascot plus a phase label — "Requesting…" until the assistant's reply starts, "Working…" once it is streaming. All other loading states (including ActivityNotice) use the plain Spinner. backdrop-filter: blur is banned site-wide, with two exceptions: the .frost variant of TopBar — only in the one place of the "sticky navigation bar", used to stay readable over scrolling content — and the floating menu surfaces: Menu.vue, the Select listbox and the composer dropdowns use the --color-menu-bg token (a 95% surface), while the autocomplete family — slash/mention popups and the dock work panel — deliberately runs a frostier recipe of its own: 70% page background (single-sourced as --color-menu-bg-frost) over the shared --p-menu-backdrop blur, which stays the single-sourced blur token across all of them. The dock's work surfaces — the work panel and its pills, persistent over the scrolling transcript — ride that same frostier recipe (specified per-surface in §04 (Dock · work pills & panels)) rather than a bespoke one. No other component (card, dialog, Toast, panel) may use glassmorphism; violations are flagged under no-glassmorphism, and menu blur with ad-hoc values (anything but the token) is flagged too. The structural spec for the app shell (three-column grid + right preview panel) and the left session sidebar. These are business-agnostic "skeletons" — components, fonts, radii, and surfaces are reused from §02 / §03, but layout and alignment have their own conventions.
On web it is a single-row 5-track grid: the sidebar and the right panel each occupy a permanent auto track, with the conversation column in the middle; two 0-width tracks are for the ResizeHandles. (The desktop app adds a second row for its terminal panel — desktop-only, see below.)
grid-template-columns: auto 0 minmax(0, 1fr) 0 auto;\n /* sidebar ↑ ↑handle ↑conversation ↑handle ↑right panel (auto) */
| Token | Value | Usage |
|---|---|---|
| sidebar width | 270px default (adjustable) | expanded sidebar width, changed by dragging the ResizeHandle; should approach §02's --p-sidebar-w (264px) |
| --preview-w | 460px | width of the right preview panel when open |
| --panel-head-h | 48px | unified height for all right panel heads + the conversation column head; both use a 0.5px bottom hairline |
| --p-bp-sm | 640px | ≤640 switches to a mobile single column (top bar + conversation), no sidebar / handle / right panel |
0 ↔ var(--preview-w) and no transition — animating a grid track would relayout the whole app grid every frame (when open it squeezes the conversation column, rather than switching templates).min-height:0; min-width:0, so only the inner scroll containers scroll and the page itself does not scroll.--sb-*)All sidebar rows (group head, session row, New chat, search, and Settings buttons) share 5 custom properties. Their 16px icon slots and --sb-gap place every label on the same x-axis as the workspace name.
| Token | Value | Usage |
|---|---|---|
| --sb-inset | 12px | row box (hover/selected pill) inset from the sidebar edges — matches the brand header's 12px padding |
| --sb-pad-x | 20px | content start x (= --sb-inset + 8px row padding) |
| --sb-gutter | 16px | leading icon slot width — matches the workspace folder icon so the session title aligns under the workspace name |
| --sb-gap | 8px | gap between the icon slot and the text |
| --sb-action-inset | calc((max(--ui-font-size-sm × --leading-tight, --p-ic-md) + 2 × --space-2 − --icon-button-sm) / 2) ≈ 3px | trailing action buttons sit this far inside the row box's right edge — exactly the buttons' vertical inset: half the font-driven row height's slack over the fixed IconButton sm box. The row height is the title line box floored at the group-head / directory rows' 16px folder icon, plus the vertical row padding, so the inset tracks the user's font scale without dropping below the icon-floored rows' slack; the session row's hover cluster, the group head's ⋯/+, and the section labels' buttons all share this one right edge |
--sb-pad-x + --sb-gutter + --sb-gap. The group head has a folder icon and the session row has a status slot; both icons are the same width and position, so the titles align naturally. On the trailing side, the list's custom 4px scrollbar is classic — it reserves layout width whenever it shows — so .sessions (and the pinned rows' scroller) keeps its gutter reserved at all times (scrollbar-gutter: stable), and the section labels' right padding is --space-1 (the track width) + --sb-action-inset: every trailing button — section-label buttons included — stays on the same right line whether or not the list scrolls, and the compensation lives on the labels (not .sessions-head) so the pinned section's drop-active frame stays symmetric. On Firefox (no ::-webkit-scrollbar) the global thin scrollbar stays — users keep a draggable bar — and since its gutter width is browser-owned and unmeasurable from CSS, the two section labels get the SAME gutter environment (overflow: hidden + scrollbar-gutter: stable, which the browser sizes identically to the list's at any thin width) and drop the track-width padding: every trailing button shares the right line with nothing measured or hard-coded. The labels' padding/negative-margin pairs reserve the focus ring's spread (--p-focus-ring-w) against the overflow clip — always on top, and on the right only when the font scale shrinks --sb-action-inset below the ring (max/min clamped, so the button's right edge never leaves the shared line). The sidebar from top to bottom: brand header → action group → pinned head (pinned section + "Workspaces" label) → scrolling grouped list (workspace head + session rows) → user-menu footer. New chat and Search are direct sibling controls in the same grid container; the optional new-workspace action shares the first row, while Search spans the next row. A 4px gap keeps Search clear of the scroll boundary. The pinned head sits OUTSIDE the scroll container (the action-group / footer pattern — never position: sticky, which would need an opaque plate over the frosted tint), so the pinned sessions and the "Workspaces" label stay put while the workspace groups scroll beneath; the pinned section is collapsible (a chevron on its label, revealed on hover/focus and kept visible while folded; state persisted) so a long pinned set can't eat the sidebar, and it re-expands when a new session is pinned. Both pinned edges use three light near, middle and far fades across --p-sidebar-seam-h (13px), entering over 260ms only while more session content exists beyond that edge — the top seam lives at the pinned head's bottom border. The footer seam is a 0.5px hairline. Controls reuse the §03 primitives as much as possible. The sidebar sits on --color-sidebar-bg (one step off --color-bg: warm off-white just under white in light, one step BELOW the page in dark — the session column reads as its own plane, and with dark elevation = lighter the chrome never sits brighter than the conversation pane; the hairline still separates it from the pane). Vertical rhythm: the brand header keeps 12px padding (on macOS desktop the left padding grows to 80px to clear the traffic lights); rows inside the action group stack flush (0 gap, same rhythm as the list rows); adjacent groups are separated by 12px. The search glyph has a -0.5px optical correction to align its visual centre with the label. Row hover uses --sb-hover (= the global --color-hover wash); the selected row uses the lighter --sb-selected wash derived from --color-selected — On macOS desktop the sidebar is instead frosted: the window carries a native NSVisualEffectView ('menu' vibrancy, following the in-app scheme via the nativeTheme mirror, its state pinned to inactive so the material keeps its flat pressed-down colour — ≈ #282829 dark / #E7E7E7 light — with no active/inactive drift) and the sidebar column drops --color-sidebar-bg for a single translucent --color-sidebar-tint wash that presses the pinned material one step — ≈ #282829 → ≈ #1e1e1f in dark (rgba(0,0,0,0.25)), ≈ #E7E7E7 → ≈ #f1f1f1 in light (rgba(255,255,255,0.4)) — with header and footer staying transparent so the tint reads as one uniform pane; the root chain (html/body/#app/.app) stays unpainted only under the macos-desktop + vibrancy flags — the latter is the Settings → Appearance accessibility switch (default on; persisted main-side so the window is created with the right material, and live-applied on toggle): off repaints the root chain and the sidebar falls back to opaque --color-sidebar-bg, while the traffic-light layout keeps keying off macos-desktop alone — while the conversation pane, chat header and right preview keep their own opaque surfaces. The list's hover-icon clusters (session-row kebab, group-head actions) paint NOTHING there — no plate, no wash, no blur (real backdrop blur does not even render over this window: Chromium's backdrop sampler returns a flat wash above the transparent BrowserWindow + vibrancy view). Instead the row's title/name dissolves before it ever reaches the buttons: a two-stage mask-image fade — a subtle 16px dissolve at rest, extending over the cluster zone only while the actions are revealed (row hover / keyboard focus / menu open): 34px on session rows (the pin+kebab cluster overhangs the title by ≈25px), 68px on group heads (the floating cluster is ≈60px wide). The fade is zone-based, so short rows render untouched, and text-overflow becomes clip so a long tail dissolves instead of dotting.
| Block | Use | Note |
|---|---|---|
| Brand header | logo + name + collapse IconButton (right-aligned) | on Windows / web the brand is left and the collapse IconButton sm is right-aligned inside the header; the dev-only backend version/address pill uses the UI font, not monospace; the logo is animated (a blinking eye). On macOS desktop the header is a bare drag strip (brand hidden, traffic lights + resident floating toggle over it) |
| New chat | full-width left-aligned button (custom) | 500-weight label; same rhythm as the session rows in the list (left-aligned, hover = --sb-hover). Do not use Button (centered, breaks the rhythm) |
| Search | bare search row (custom) | 500-weight label; no border, hover/focus shows the faint --color-hover wash; icon + label, with the Kbd keycaps (⌘K / Ctrl K) pushed to the trailing edge — label and shortcut are justified apart. Do not use Input (the 38px bordered version is too heavy). It is a direct sibling of New chat in the action group |
| Section label | .p-section-label | uppercase muted small titles like "Workspaces", using --weight-section-label (600) |
| Pinned head | fixed block above the scroll container (.sessions-head): the pinned section (PinnedSessionList.vue) + the "Workspaces" section label | stays put while the workspace groups scroll; owns the top scroll-linked seam (hairline + fade, only while scrolled). Pinned rows render in pure recency order (updatedAt desc — no manual ordering, no attention tiering): drag a session row in to pin it (the drop spot carries no position meaning), drag a pinned row out to unpin. The pinned section folds via its label chevron (persisted, kimi-web.pinned-collapsed) and re-expands only on an explicit pin (never on load backfill); the expanded rows are capped (40vh at rest) with their own scroll so a long pinned set can't push the list or footer out of view. Once the pinned content exceeds a few rows, a horizontal ResizeHandle twin renders between the rows and the section label (a shorter set keeps its natural height and no handle): dragging it re-caps the rows (two rows min; the max is 60% of the viewport, narrowed further on short windows so the list below always keeps ~3 rows — both bounds measured off the rendered rows, so they track the font-scale setting — and a gesture never targets positions past the content's natural height so the separator always tracks what renders; 40vh default; persisted per device, kimi-web.sidebar-pinned-height) and the list below takes what remains — same 4px strip / centred 2px bar, neutral f2/f3 ramp, row-resize with directional hints at the limits, useResizable axis: 'y' with imperative height writes, and the §08 separator keyboard model (↑/↓). While the rows scroll internally, their edges carry the session list's scroll-linked seam language: a --p-sidebar-seam-h three-layer text-tint veil plus a 0.5px --line hairline at the content edge, at the top once scrolled and at the bottom while more rows remain — absolutely positioned (no layout shift), --duration-slow opacity fade, below the resize handle so its hover/drag bar always wins. The rows scroller owns the same inset as .sessions (the wrapper stretches to the full column width), so the rows' right edge and the scrollbar track land exactly where the session list's do |
| Workspace head / session row | see next two sections | share --sb-* alignment |
| User-menu footer | account area (components/UserMenu.vue) opening an upward §03 menu | pinned row under the session list, separated by a 0.5px --line hairline; trigger keeps the same list-style family as New chat (24px round avatar + nickname when signed in, user icon + sign-in hint otherwise). The menu box follows the trigger's left edge and width (ResizeObserver-tracked, so it survives a sidebar resize) and is teleported to body because the column's container-type would capture position:fixed. Rows: plan usage / theme / language are macOS-style hover flyout submenus — the parent row carries the module icon, a faint current value and a fixed chevron-right, and hovering (or moving focus to the parent row, or pressing Enter / Space / → on it) opens a teleported panel anchored to the parent menu's right edge (content-adaptive width floored by the menu's own min-width and capped at the parent menu's width; flips left near the viewport edge) with a 250ms hover-intent close grace; the usage panel shows weekly + 5h rows (used-percent values via settings.planUsage.usedPct, with severity colours), while the theme (three schemes) and language (two locales) panels move the check to the picked option without closing the menu — then the upgrade entry below the top plan level, settings (with an always-visible Kbd keycap shortcut hint on desktop) and a confirming sign-out; all menu icons come from the Kimi set, and the whole menu (flyouts included) runs at the §03 default density — same row inset, label size and separator rhythm as every other menu; the usage flyout's custom rows read the same --menu-item-padding-* inset tokens as the primitive, with the reset hint one rung down at --text-xs. On macOS desktop the trigger's bottom-left corner rounds at --radius-window-chip — concentric with the window's corner (the window rounds at the measured 14px --radius-window, and the footer's --sb-inset row inset and --space-2 block padding both resolve to 8px, so the chip hugs 8px from the window's left and bottom edges and 14px − 8px lands exactly on --radius-sm); web and other platforms have no rounded container corner there and keep the uniform --radius-sm |
kimi-web.sidebar-multi-tab, default OFF) forks the sidebar into two forms. OFF = the legacy single session list: no 进行中/已完成/工作空间 tabs (statusTab pinned to open, tab shortcuts inert), no session-admin entries (the 列表管理 menu item hides and the workspace home itself falls back to the classic 新建会话 doodle hero — no workspace head, no recent-sessions list), the row hover action + context menu and the chat-header ⋯ menu read 归档 with the archive glyph (no success-hover variant, the header's Done pill + reopen button hide), and the archive ActionToast is the legacy «撤销 · 或到 · 设置 · 查看已归档的会话» linking to Settings → Archived. ON = the status-tabs form documented in this section (tabs, session admin page, the complete/reopen relabel, «已完成 · 撤销» toast). The preference is an app-core singleton (useSidebarTabs) consumed directly by Sidebar / SessionRow / ConversationPane / App.vue — no prop threading. A session row is an inset rounded pill, structured as: status slot → title → time → attention Badge → hover actions (pin / archive).
| Part | Rule |
|---|---|
| Container | padding: 8px 8px inside the list's --sb-inset gutter, radius-sm; no fixed/min height — row height is font-driven (title line-height: --leading-tight, ≈16px) → ≈32px total, the sidebar-wide row rhythm. The hover actions are absolutely positioned so they never force the row taller (no hover jitter). hover = --sb-hover (the global --color-hover wash); active = --sb-selected (75% of the global selected wash) — neutral, no accent tint, no border, no weight change |
| Status slot (lead) | fixed --sb-gutter width; running = Spinner sm, otherwise unread = 7px accent dot; empty while an attention Badge owns the row — one status at a time, decided by the shared SessionDisplayStatus enum (app-core sessionDisplayStatus.ts: approval › question › running › aborted › unread) |
| Title | flex:1 with truncation and user-select:none; double-click enters inline rename (compact input, not Input), whose text remains selectable |
| Emoji icon | the session icon is the title's LEADING emoji cluster (app-core splitSessionEmoji — no icon field; every client renders the title as-is). The emoji is an ordinary title character — no decoration at rest or on hover (it stays a <button> for a11y), and clicking it opens SessionEmojiPicker — a Menu-shelled panel (bare list-style search row → scrollable sections: Recently used persisted in localStorage (cap 8) + the grouped emoji dataset, with remove/random as MenuItems in the footer; a query swaps the sections for keyword-search results), teleported + fixed + --z-dropdown, popping from the trigger corner like the right-click menu. The menu's "Set Emoji…" opens the same picker and is the discoverable path. Inline rename edits the whole title — the emoji is an ordinary character in the input |
| Time | mono xs, fg-faint; yields to the hover actions on hover |
| Attention Badge | Badge sm: info (needs answer) / warning (needs approval) / danger (aborted) |
| Hover actions | IconButton sm × 2 — pin + archive — cross-faded over the time on row hover (no kebab button). Right-clicking the row opens the full menu (copy ID / rename / emoji / fork / export / pin / archive + timestamp) anchored to the cursor, except over the inline rename input, where the native text-editing menu stays |
| Flat-style variant (flat list + pinned section) | the sidebar's flat list rows AND — always, regardless of view mode — the pinned section's rows differ from the grouped row in three ways (all keyed off the facade projecting cwdLabel): ① no leading status slot — the title is left-aligned at the row's content edge; ② a second line under the title: folder-closed icon sm + the cwd's final directory name (- when the session has no cwd), xs faint like the time — except the icon, which takes --color-text-muted (one rung stronger, the same optical compensation as the group head's folder; the open-folder glyph's thin back-flap washed out at 14px) — rest-width tail mask fade; when the session has an associated PR (v2 git domain), a small tag (git-pull-request icon + #number, 2xs medium on a soft ground with a hairline edge and radius-sm corners — a mini §03 Badge) sits at the line's right edge, state-colored the GitHub way (open = --color-success-soft ground + --color-success text, merged = --color-done-soft + --color-done purple, closed = neutral sunken) and opens the PR on click; ③ the first line's right side shows status — attention Badges anchored to the row's right edge, running Spinner, unread dot — INSTEAD of the time, which only renders when there is nothing to report (the Spinner yields to the attention pills: a session waiting for approval/answer never shows both); on hover the actions cross-fade IN as the whole status cluster fades OUT — pills and pin/archive never co-exist (grouped rows keep pills visible on hover). Height stays font-driven — the pill just grows the line. Grouped rows never set cwdLabel and keep the classic structure. The flat ↔ grouped switch lives in a dropdown on the SESSIONS section label (fixed list-settings icon + hover tooltip; the menu opens with a muted group label, per-view icons, and the current view checked at the row's right edge; mode persisted per device) |
| Archive | no confirm — the hover archive button / menu item archives immediately, then App.vue shows the §03 ActionToast (top-center) with Undo (restores the session) and Settings (opens the archived list) |
The group head and session rows share --sb-*: folder icon (open/closed) → name, with the kebab and "+" revealed on hover.
--sb-gap before the name — it does not pad out the --sb-gutter slot.--color-text-muted, one step lighter than session titles), so group heads remain clear without competing with list content. No path subtitle; hovering the name shows the full root path in a Tooltip.IconButton sm inside a floating actions layer anchored to the row's right edge — no reserved layout space, so the name uses the full row width when idle. Shown on hover, keyboard focus, or while the menu is open; the layer backs itself with the sidebar surface (container background) plus the row hover wash (an ::after shown only while the row is hovered), so its color exactly equals the row's current background and the overlapped name tail doesn't bleed through (hidden via opacity:0, staying in the tab order). On macOS desktop the layer paints nothing at all — the name's mask-image fade (see the sidebar section above) dissolves the tail before it reaches the buttons--sb-selected fill as a selected session row (selection reads as "where I am"; the fill wins over hover). Once a session is selected or created, the fill moves to that session row.The "expand / collapse" controls at the bottom of each workspace group are compact list controls (same family as search, New chat, inline rename — not Buttons) sharing one row: expand (chevron-down) first, collapse (chevron-up) after a faint middot when both are present. Expanding reveals the next batch of sessions, fetching the next page from the server only when the locally loaded rows can't cover it — the control never exposes whether a reveal came from memory or the network.
| Part | Rule |
|---|---|
| Row | a single flex row holding the controls, all content-width — hover washes just the button as a snug pill, never the full row. Font-driven height (≈32px like a session row), radius-sm; hover = --sb-hover (no text recolor); :focus-visible uses --p-focus-ring |
| Chevron | sm (down = expand, up = collapse); the row indents by --sb-gutter + --sb-gap so the first button's chevron starts exactly at the session-title x, lining the control's leading edge up with the titles above |
| Label | font-ui, text-xs, --color-text-muted; truncated |
| Separator | faint middot (--color-text-faint) with --space-1 side margins, rendered only when both controls are present |
| Behavior | each group keeps a display cap starting at the first page; "Show more" steps it up by one batch (5) and fetches the next page only when the loaded rows fall short (busy = "Loading…", disabled); "Show less" resets the cap to the first page (view-layer trim — data is kept, no refetch). "Show more" exists while undisplayed loaded rows remain or the server has more; "Show less" appears once past the first page |
A 4px grab strip layered over the 1px column border (margin: 0 -2px makes the whole 4px grabbable) with a centred 2px indicator bar. The bar stays transparent at rest and shows the neutral fills one step up the ramp — f2 on hover, f3 while the drag is live (the sidebar column is translucent on macOS, so f1 read too faint) — never the accent.
| Rule | Value |
|---|---|
| Width / cursor | 4px strip, 2px bar / col-resize mid-range; w-resize / e-resize at the drag limits (hints the direction that still resizes) |
| Normal / hover / drag | transparent / --color-selected (f2) / --color-line-strong (f3) — the neutral ramp one step up, never accent |
| Layer | --z-dropdown, above pane-level sticky chrome (chat dock at --z-sticky) so the overhang stays visible and grabbable |
| Behavior | panel width follows the pointer 1:1 while dragging (the parent disables transitions to avoid lag); on release it is persisted to localStorage |
The right panels (file preview / Diff / compaction summary / sub-agent / side chat) share one track and one head primitive.
PanelHeader primitive (48px = --panel-head-h), the same height as the conversation column head, so the hairline runs as one line.0 → var(--preview-w) with no animation, squeezing the conversation column in a single layout.position:fixed; inset:0).The native terminal (components/terminal/) sits in the conversation column's own bottom grid slot — the sidebar and the right panel span BOTH rows and keep full height (the VS Code layout: the panel belongs to the editor area, not to the whole window). Its height transitions 0 ↔ var(--terminal-h) (260px default, 120 min, 60% viewport max; persisted), squeezing the conversation column above instead of overlaying it. The panel mounts lazily on first open and then stays mounted so xterm scrollback survives a collapse.
row-resize mid-range, n/s-resize at the limits, same neutral f2/f3 ramp, never accent). The shared useResizable hook owns it via axis: 'y'; the height var is written imperatively during a drag (same no-Vue-rerender rule as --preview-w).radius-sm pill (leading terminal glyph, muted while exited + shell label + hover close affordance), the active tab uses --color-selected, hover --color-hover; a "+" action appends a tab. Tabs follow the §08 tablist keyboard model (roving tabindex, ←/→/Home/End), the close affordance is its own button (no nested interactives), and the height separator is keyboard-operable (↑/↓ in steps, value exposed). Trailing actions: restart (only while the active tab exited) and a collapse chevron. Collapsing sets inert on the region — the xterm instances and their scrollback stay mounted but leave the tab order.--color-* tokens at runtime (re-read on scheme flips; the ANSI hues the status ramp doesn't cover use dedicated --color-term-magenta/cyan tokens); the font is the app JetBrains Mono stack sized off the content token scale. While focused, the panel owns every key except the registered app shortcuts (chat-level Esc / find / select-all chords stay inert inside it).ctrl+` (⌃` on macOS — VS Code's binding; ⌘` stays free for the OS window switcher — customizable in the shortcut registry), and the View menu's Toggle Terminal item. New tabs spawn in the visible workspace root. Terminal state is per session: switching sessions swaps the visible bucket while the others keep their PTYs and xterm views alive (scrollback survives a round trip; the ten most recent sessions are kept, LRU). The panel never renders on mobile / web.Kimi Web is a local developer tool; it does not target a specific WCAG conformance level, nor maintain a full screen-reader QA matrix. This section collects only the rules that are "low-cost, don't hurt the look, and directly benefit keyboard-heavy users", as the baseline contract for each primitive; the more expensive, lower-ROI parts (such as real-time announcement orchestration for streaming output) are not mandatory for now.
:focus-visible (not :focus). It appears only on keyboard focus; mouse clicks don't trigger it, so it doesn't pollute the mouse-driven visual; the ring's strength is tuned uniformly with --p-focus-ring, not overridden per place. Anything doable with a mouse must also be doable with a keyboard; Tab order follows the DOM, with no invented skipping. Composite controls define their keyboard model per the table below; a missing model is treated as incomplete:
| Control | Keyboard behavior |
|---|---|
| Dialog | Tab cycles within the dialog (focus trap); Esc closes; focus returns to the trigger element after closing. |
| Menu | ↑ / ↓ move the highlight, Enter selects, Esc closes. |
| Tabs | ← / → switch tabs (roving tabindex); only the current tab is in the Tab sequence. |
| Switch / Segmented | ← / → or Space / Enter to toggle. |
:focus-visible + --p-focus-ring (primary actions may use --p-focus-ring-strong).outline: none is forbidden. To remove the default outline, you must provide an equivalent replacement style.aria-label — IconButton already enforces this with a required label prop.role="dialog" + aria-modal="true", with the title as the dialog's accessible name.aria-hidden="true" to avoid being read out by screen readers.Desktop click targets ≥ 32px; touch devices ≥ 44px (consistent with the §01 principle and the IconButton lg tier).
Handled uniformly in the global styles per §02's @media (prefers-reduced-motion: reduce); components do not check this individually. The chat working indicator's mascot renders its static fallback.
Screen-reader announcements are not a mandatory contract in this product. Short hints like Toast can use role="status" / aria-live; chat streaming output is currently not announced word-by-word, which is an acceptable trade-off, to be added later if a real need arises.
Every overlay in the app — pickers, browsers, managers, confirmations — is built on the single §03 Dialog primitive. This chapter fixes the two layout anatomies allowed inside that frame, plus the row and footer contracts that make all dialogs read as one family. Do not hand-roll a third anatomy.
All dialogs share the §03 primitive: --radius-xl radius, --shadow-xl shadow, a restrained 28% neutral backdrop, a head (title + IconButton close), a body, and a right-aligned foot. Widths sm 360 / md 440 / lg 640 / xl 760 and auto / fixed height are chosen per §03. One interruptive overlay at a time; Esc closes; focus is trapped and restored. A blocking flow that must be resolved rather than dismissed (server token) uses hideClose with closeOnOverlay/closeOnEsc off — never a hand-written overlay.
The default: the body carries its own padding and the caller drops content straight in. Confirmations put their Buttons in the #foot slot (right-aligned, cancel → confirm). Used by: confirm, login, status panel, server token.
:padded="false" with height="fixed"; the consumer owns the zone layout inside a full-height column. The zones below are the whole vocabulary — a picker dialog composes them and adds nothing else. Used by: model picker, session search, folder browser, provider manager.
| Zone | Contract |
|---|---|
| Search | The boxed §03 Input, inset 22px so its edge aligns with the head title. Autofocus on open. No leading icon, no borderless variant. |
| Filter chips | Optional. 28px pill: transparent + muted text by default, --color-hover on hover, --color-selected + medium --color-text when active. Horizontally scrollable with the scrollbar hidden. Never a row of Buttons. |
| List | flex:1, owns the vertical scrolling, padded 4px 8px so rows bleed near the dialog edge. role="listbox"; rows carry role="option" + aria-selected. |
| Row | 8px 12px padding, --radius-md. Two quiet lines: name 14/20 (medium when current) and a meta line 12/18 in --color-text-faint — provider · context · capability labels, dot-separated. No badge rows, no raw-id line (search still matches them). Trailing slot: check icon (current row only), then the star IconButton. |
| Row states | Hover / keyboard-selected → --color-hover; current → --color-selected — a neutral "where I am" fill, never an accent tint, never an inset stroke. The star stays hidden until row hover, keyboard selection, or starred; it is always visible on touch devices and colored --star when starred. |
| State rows | Loading / unavailable / empty: centered on both axes, muted 14px; warning color only for the unavailable case. |
| Shortcut bar | The footer: full-bleed, padding 8px 16px, border-top --color-line, left-aligned. Keyboard hints are Kbd keycaps + 12px --color-text-faint labels, groups separated by "·", the whole bar aria-hidden. An instructional sentence (folder browser) reuses the same bar without keycaps. |
↑/↓ move a keyboard selection (rendered identical to hover) and always scrollIntoView({ block: 'nearest' }); Enter selects and closes; Esc closes.background only (--duration-fast ease-out); the open/close animation lives in the primitive, not in the consumer.| Dialog | Anatomy | Composition |
|---|---|---|
| Model picker | flush · lg · fixed | search + provider chips + model rows + shortcut bar |
| Session search | flush · lg · fixed | search + result rows (sidebar-style alignment: one icon gutter, shared left text edge, shared right meta edge; workspace rows single-line name + right-aligned path, session rows title + time over a workspace · snippet meta line; quiet uppercase section heads with counts; empty query shows a few top workspaces + recent sessions) + shortcut bar |
| Folder browser | flush · lg · fixed | breadcrumb bar + filter bar + folder rows + actions + hint bar |
| Provider manager | flush · xl · fixed | management rows with inset dividers (rows are not selectable) + add section + shortcut bar |
| Confirm / Login / Status | padded · md · auto | title + message or form + right-aligned foot |
| App update (desktop) | padded · lg · auto | version title (stays "发现新版本 vX" even while downloading) + quiet meta line (release date · current version) + height-capped scrolling what's-new list + right-aligned action row (skip → download; downloading → background + disabled live-percent button; later → restart) with the auto-download checkbox right-aligned on its own foot row below (a pure preference for future checks) |
| Server token | padded · md · auto | hideClose, no Esc/overlay close — resolved only by a valid token |
| Settings | flush · xl · fixed | page-like exception: side-nav region, per §03 |
| Onboarding wizard | not a Dialog | full-page takeover (not built on §03): one centered column (brand lockup → step content → ghost actions + centered primary CTA); selectable options share the option-card pattern — 0.5px --color-line hairline, --color-accent border + --color-accent-soft fill when selected |
The session admin page (/admin/sessions, opened from the sidebar's list-management menu) is a full-pane management view for cross-workspace session triage: filters, a server-side paged table, and batch lifecycle actions. It is a main-view peer of the conversation pane (mainView in the facade, switched with v-show so the chat stays alive) and page-private by decision — everything lives under components/admin/ (SessionAdminView/Table/Pagination, FilterSelect, MultiSelectMenu, SessionAdminMenu, useAnchoredMenu); nothing here promotes to §03 until a second consumer exists. Data is one GET /api/v2/sessions page-mode call per filter/page change (all conditions pushed down, no client-side aggregation); batch archive/restore go through the v2 batch endpoints with per-item outcomes.
A 48px title bar (a back IconButton — chevron-left, tooltip 返回, closing the page back to the chat underneath via closeSessionAdmin — then the page title, semibold base; hairline bottom edge; on macOS desktop it doubles as the window-drag region and takes the chat header's collapsed-sidebar clearance — 146px / 78px / Windows fallback — so ONLY the bar insets, the body never does) → muted subtitle → query-form filter bar → table card → pager. The title bar and the scroll container are siblings (bar fixed, body scrolls). The page wrapper spans the whole conversation column — do not apply the chat content measure (--p-content-max) here: under table-layout: fixed a narrow wrapper crushes the table's flexible columns to zero width (the title/prompt columns collapsed in practice). An admin table surface owns the pane width. The status filter defaults to 全部 (all) — the page is the whole inventory, 重置 restores that same default.
--p-content-max is a READING measure for prose-like content (chat, dialogs). Full-bleed work surfaces — tables, grids, dashboards — span the pane instead. Pick one deliberately; inheriting the chat measure by default is the bug. The card is the flat shell: 0.5px --color-line hairline, --radius-lg, no shadow. Inside, table-layout: fixed with a colgroup: fixed-width utility columns (checkbox, workspace, status, the two time columns, actions) and two flexible content columns (title at max(200px, 20%) — a floor, not a bare percentage — last prompt taking the rest). The head row is a pinned-height 32px box (it hosts the batch transform below); body rows are 40px. Rows are separated by 0.5px --color-subtle hairlines (none after the last row), with a --color-hover wash on hover. Every cell truncates single-line with ellipsis and carries a title tooltip. Time columns are absolute (YYYY-MM-DD HH:mm) in --font-mono xs with tabular-nums — the admin page is an audit view, so no relative times; empty values render a faint —. The status column sits right after the workspace column and reuses the sidebar row's lifecycle glyphs: state-open (dashed ring, --color-success) for 进行中 and state-done (checked ring, --color-done) for 已完成, icon + label. First load swaps the body for a centered §03 Spinner; refetches keep the stale rows and dim the card (opacity + pointer-events: none) rather than flashing it away; filters with no matches render the centered faint empty line.
Responsive steps (the card is the query container; web's narrow panes matter too): the time columns give ground FIRST — at ≤1020px their values swap to the compact MM-DD HH:mm at 108px (each time cell carries both spans, CSS toggles them — no JS measuring); at ≤760px the time columns hide outright (the col and the cells share sa-c-time/sa-col-time classes) and the workspace column drops its folder icon. The table itself floors at 640px and the card takes overflow-x: auto — past the floor the card scrolls horizontally instead of crushing title/prompt to zero, and auto still clips the rounded corners like hidden did.
| Column | Width | Content |
|---|---|---|
| checkbox | 36px | header select-this-page (indeterminate) + row checkboxes |
| 会话名 | 20% | title (emoji verbatim), weight 475, ellipsis; hosts inline rename |
| 工作空间 | 116px | folder-closed icon + workspace name (cwd basename fallback) |
| 状态 | 88px | state-open/state-done glyph + label |
| 最后一条 prompt | flex | muted, ellipsis, faint — when null |
| 最后更新 / 完成时间 | 140px ×2 | mono tabular-nums absolute time; completed shows — while open |
| 操作 | 84px | lifecycle IconButton (state-done completes an open row, undo reopens a done one; tooltips carry 标记完成/恢复进行中) + ⋯ IconButton dropdown (Rename… / Fork / Export) |
GitHub issues/PR semantics: while a selection exists, the head row transforms IN PLACE — the checkbox column stays put, and the remaining column headers swap for a single <th colspan> batch bar ("已选 n 项" + Mark-as-done / Reopen, each disabled when the selection holds no row of that lifecycle) inside the SAME pinned 32px box, so the table body does not move a pixel. Mark-as-done is the bar's one accent-primary button (fill + on-accent icon/label); Reopen stays a quiet hairline button. The selection itself is facade-owned (a reactive id set plus a per-id lifecycle map, reconciled against fresh rows on every landing) and survives page and filter changes — the batch count deliberately includes rows no longer visible. Successful batch items leave the selection and the current page is silently re-pulled; failures stay selected. There is deliberately no "clear selection" button for ordinary selections: unchecking rows or the header checkbox is the way out. Toasts ride App.vue's shared ActionToast channel (succeeded count, failed count when partial, Undo through the inverse batch endpoint); an all-failed batch has nothing to undo and surfaces as a WarningToast instead.
Page-size caps (≤100) make "mark everything done" hopeless against thousands of rows, so the batch bar borrows Gmail's escalation: once the header checkbox has the whole page selected and total says more rows exist, a link-style button appears IN the bar (zero-offset — no banner pushing the table down): "选中当前条件下的全部 N 项" (busy-label 正在选中… while fetching). Activating it materializes every matching id into the selection via the ids projection (GET /api/v2/sessions?fields=id,archived — the one cheap shape whose page_size ceiling relaxes to 10000; cursor-walked for larger sets), merging atomically: a filter change mid-flight discards the fetch entirely. While active the count reads "已选中全部 n 项" and the link becomes 清除选择. Exclusions are free — unchecking a row just drops it from the set; emptying the selection drops the mode, as does a right-click single-row collapse. The mode ties itself to the filter fingerprint it was built from: a real condition change (query-form apply with different values, or any granular setter) clears the whole selection — re-applying the SAME conditions keeps it. Batch executions chunk at the wire's 5000-unique-ids ceiling (sequential, merged per-item outcomes; a thrown chunk aborts the rest and counts everything unexecuted as failed — the succeeded ids still reconcile).
All quiet icon/text buttons on the page (the batch bar's Reopen, the pager buttons) set border: none; background: transparent explicitly — or, for the bordered shapes (the filter triggers), an explicit 0.5px hairline. Row actions are pure §03 IconButtons (the sidebar row's language): the lifecycle glyph (state-done completes an open row, undo reopens a done one — tooltips carry the labels) plus the ⋯ dropdown; a text button per row read as visual noise once every row carried one.
<button> still inherits the UA stylesheet's outset border, which reads as a stray box around every quiet control. Any quiet button outside the §03 primitives must reset both properties. The filter bar is a query form (antd Pro semantics): the controls edit a local DRAFT only — nothing is requested until an explicit apply. 查询 (a §03 Button variant=primary size=sm) applies the whole draft through the facade in one shot (applySessionAdminFilters — atomic write + page reset + exactly one request, never the per-setter debounce dribble), 重置 (a ghost sibling) restores defaults the same way, and Enter inside the bar queries too — except while any overlay (a select dropdown) is open, where Enter belongs to the overlay. Pagination is exempt: page/page-size changes still fetch immediately. Entry paths can pre-seed the conditions: the workspace home's 查看更多 opens the page with its workspace already selected in the filter (openSessionAdmin(workspaceId) applies the filter atomically with the navigation), and because the page is v-show-kept, the draft re-seeds from the applied filters on EVERY entry so the controls always show the true conditions. The controls themselves are muted sm labels plus a family of quiet 30px hairline controls (0.5px --color-line, --radius-md, transparent ground, --color-hover on hover/open) — deliberately NOT the §03 Select (a 32px+ form control with an accent focus ring, too heavy for a filter strip):
--color-success open / --color-done done).updatedTo bound as the local calendar day N days back (computed at apply time, so a saved draft never goes stale), and the facade's day-end mapping turns it into updatedBefore. Deliberately not a calendar range picker — triage asks "older than X", not "between two dates".useAnchoredMenu is the page's one menu mechanic: fixed-position §03 Menu surface, pop-from-anchor motion (fade + 0.97 scale on the menu tokens, origin and nudge following the upward flip at the viewport edge), closed by outside mousedown / Esc / scroll / resize. Three anchor modes — left-aligned under a trigger (filter selects), right-edge under a trigger (the row ⋯), and openAt(x, y) at a raw viewport point for the row contextmenu. The contextmenu has two shapes: on a row outside the multi-selection the selection first collapses to just that row and the menu is the single shape (Open session / Rename… / Fork / Export / — / the lifecycle action); on a row inside it, the multi shape (a muted count head + Mark-as-done (n) / Reopen (n), disabled per availability). Open session is the facade's selectSession — a user navigation that also leaves the admin page back to chat.
The title cell swaps for an accent-ringed input (--color-accent border + --color-accent-bd ring, --radius-sm) — Enter commits, Esc cancels, blur commits, settled once. The table is server-fed, so a commit is followed by a silent re-pull of the current page (the facade's pool update never reaches it).
C&&(l=C),Tw&&(g=w),d
=this.nodes.length){var V=0;T.forEach(function(M){M.owner==c&&V++}),V==this.nodes.length&&(this.isConnected=!0)}},A.exports=s}),(function(A,G,N){var v,h=N(1);function i(r){v=N(6),this.layout=r,this.graphs=[],this.edges=[]}i.prototype.addRoot=function(){var r=this.layout.newGraph(),a=this.layout.newNode(null),f=this.add(r,a);return this.setRootGraph(f),this.rootGraph},i.prototype.add=function(r,a,f,e,u){if(f==null&&e==null&&u==null){if(r==null)throw"Graph is null!";if(a==null)throw"Parent node is null!";if(this.graphs.indexOf(r)>-1)throw"Graph already in this graph mgr!";if(this.graphs.push(r),r.parent!=null)throw"Already has a parent!";if(a.child!=null)throw"Already has a child!";return r.parent=a,a.child=r,r}else{u=f,e=a,f=r;var t=e.getOwner(),s=u.getOwner();if(!(t!=null&&t.getGraphManager()==this))throw"Source not in this graph mgr!";if(!(s!=null&&s.getGraphManager()==this))throw"Target not in this graph mgr!";if(t==s)return f.isInterGraph=!1,t.add(f,e,u);if(f.isInterGraph=!0,f.source=e,f.target=u,this.edges.indexOf(f)>-1)throw"Edge already in inter-graph edge list!";if(this.edges.push(f),!(f.source!=null&&f.target!=null))throw"Edge source and/or target is null!";if(!(f.source.edges.indexOf(f)==-1&&f.target.edges.indexOf(f)==-1))throw"Edge already in source and/or target incidency list!";return f.source.edges.push(f),f.target.edges.push(f),f}},i.prototype.remove=function(r){if(r instanceof v){var a=r;if(a.getGraphManager()!=this)throw"Graph not in this graph mgr";if(!(a==this.rootGraph||a.parent!=null&&a.parent.graphManager==this))throw"Invalid parent node!";var f=[];f=f.concat(a.getEdges());for(var e,u=f.length,t=0;t=r.getRight()?a[0]+=Math.min(r.getX()-i.getX(),i.getRight()-r.getRight()):r.getX()<=i.getX()&&r.getRight()>=i.getRight()&&(a[0]+=Math.min(i.getX()-r.getX(),r.getRight()-i.getRight())),i.getY()<=r.getY()&&i.getBottom()>=r.getBottom()?a[1]+=Math.min(r.getY()-i.getY(),i.getBottom()-r.getBottom()):r.getY()<=i.getY()&&r.getBottom()>=i.getBottom()&&(a[1]+=Math.min(i.getY()-r.getY(),r.getBottom()-i.getBottom()));var u=Math.abs((r.getCenterY()-i.getCenterY())/(r.getCenterX()-i.getCenterX()));r.getCenterY()===i.getCenterY()&&r.getCenterX()===i.getCenterX()&&(u=1);var t=u*a[0],s=a[1]/u;a[0] 0&&(R=p+n.verticalPadding-n.rowHeight[I]);var W;n.width-O>=E+n.horizontalPadding?W=(n.height+R)/(O+E+n.horizontalPadding):W=(n.height+R)/n.width,R=p+n.verticalPadding;var x;return n.width {var f=a(548),e=a(140).CoSELayout,u=a(140).CoSENode,t=a(140).layoutBase.PointD,s=a(140).layoutBase.DimensionD,o=a(140).layoutBase.LayoutConstants,c=a(140).layoutBase.FDLayoutConstants,l=a(140).CoSEConstants,T=function(d,C){var S=d.cy,w=d.eles,P=w.nodes(),B=w.edges(),U=void 0,V=void 0,M=void 0,_={};d.randomize&&(U=C.nodeIndexes,V=C.xCoords,M=C.yCoords);var n=function(x){return typeof x=="function"},E=function(x,Q){return n(x)?x(Q):x},p=f.calcParentsWithoutChildren(S,w),m=function W(x,Q,z,X){for(var rt=Q.length,$=0;$t)return a[0]=f,a[1]=o,a[2]=u,a[3]=U,!1;if(e0&&c.set("dummy"+(c.size+1),B),U},u.relocateComponent=function(t,s,o){if(!o.fixedNodeConstraint){var c=Number.POSITIVE_INFINITY,l=Number.NEGATIVE_INFINITY,T=Number.POSITIVE_INFINITY,g=Number.NEGATIVE_INFINITY;if(o.quality=="draft"){var d=!0,C=!1,S=void 0;try{for(var w=s.nodeIndexes[Symbol.iterator](),P;!(d=(P=w.next()).done);d=!0){var B=P.value,U=f(B,2),V=U[0],M=U[1],_=o.cy.getElementById(V);if(_){var n=_.boundingBox(),E=s.xCoords[M]-n.w/2,p=s.xCoords[M]+n.w/2,m=s.yCoords[M]-n.h/2,y=s.yCoords[M]+n.h/2;Ew&&(g=w),d