@alwith/module-chat 0.1.3 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -11,8 +11,8 @@ reduces rights granted by those licenses or by mandatory applicable law.
11
11
  2. Permission to develop and distribute applications
12
12
  Subject to this license, ALwith grants you a worldwide, non-exclusive, royalty-free
13
13
  copyright license to use, reproduce, integrate and modify the supplied Software
14
- to develop, test, host and operate your own applications, including WorkBuddy
15
- applications, for personal, internal business or commercial use. You may brand,
14
+ to develop, test, host and operate your own applications, for personal,
15
+ internal business or commercial use. You may brand,
16
16
  sell and distribute those applications, including the Software embedded in them,
17
17
  to your customers and end users. No per-application registration, approval or
18
18
  royalty is required by this software license.
package/README.md CHANGED
@@ -21,6 +21,16 @@ Run bun install at the workspace root, then bun run --cwd packages/chat typechec
21
21
 
22
22
  ACP session folding stays in @alwith/api. This package owns chat presentation and interaction, not Agent processes or a second session database.
23
23
 
24
+ `update-scheduler` exports the optional, framework-independent `ChatUpdateScheduler<Snapshot>`.
25
+ The host folds every protocol update immediately, then calls `enqueue(sessionId, snapshot)`
26
+ with the complete immutable snapshot. One 16ms window publishes the latest snapshot for each
27
+ pending session in a single callback; incoming chunks never reset the deadline. Call `flush()`
28
+ after enqueueing a lifecycle/configuration change or finishing history replay, `remove(sessionId)`
29
+ when dropping a session, and `cancel()` on disconnect/disposal. The callback should publish the
30
+ batch to the UI store once. This schedules store notifications, not protocol processing or
31
+ Markdown parsing. It retains only pending snapshot references, has no React/DOM/ACP dependency,
32
+ and does not change existing components or enable batching unless the host opts in.
33
+
24
34
  `search` provides Desktop's complete find bar, DOM matching, highlights and navigation. The host supplies its viewport, labels and native-menu subscription; virtualized callers also supply source matches and an async turn-reveal callback. Key the component by session identity to discard the previous search on a session switch. Highlight ownership is per mounted search, and cancelled navigation cannot repaint after close. Source projection stays with the application's message model; the search component never reads a private store.
25
35
 
26
36
  `search-source` shares Unicode-aware literal matching and original UTF-16 offsets with DOM matching. This does **not** make raw tool fields, Markdown source or folded content equivalent to rendered text: the current per-turn occurrence index still requires a complete source-to-rendered mapping. Do not treat the current search counter as proof that every indexed match can be revealed. Automatic expansion of folded matches has not been agreed.
@@ -47,7 +57,7 @@ The composer form comes from `@alwith/module-chat/composer`, with the controller
47
57
 
48
58
  See [LICENSE](LICENSE) and [third-party notices](THIRD_PARTY_NOTICES.md).
49
59
  ALwith-owned code is proprietary, not open source. You may build, customize, host,
50
- charge for and distribute your own WorkBuddy applications under the included
60
+ charge for and distribute your own applications under the included
51
61
  license, without per-application registration. This does not grant hosted-service
52
62
  access or rights to separately distributed Rust libraries or Runtime binaries.
53
63
  The npm artifact includes compiled/minified JS, declarations and necessary assets,
@@ -1 +1 @@
1
- import{jsx as r,jsxs as u}from"react/jsx-runtime";import{autoUpdate as K}from"@floating-ui/dom";import{CheckCheckIcon as S,CheckIcon as I,ShieldXIcon as z,XIcon as M}from"lucide-react";import{useCallback as O,useEffect as h,useLayoutEffect as v,useRef as U,useState as p}from"react";import{createPortal as W}from"react-dom";import{cn as N}from"cn";import{sortPermissionOptions as X}from"./permission-options.js";var B={allow_once:{icon:I},allow_always:{icon:S},reject_once:{icon:M},reject_always:{icon:z}};function Y({requestId:f,options:_,title:j,children:D,total:w=1,alwaysLabel:E="always",autoFocus:y="always",anchorRef:n,onRespond:i}){let[l,x]=p(0),[s,b]=p(null),[c,A]=p(null),g=U(null),a=X(_);h(()=>{x(0)},[f]),h(()=>{let e=document.activeElement;if(y==="always"||e===null||e===document.body)g.current?.focus()},[f,c,y]);let d=O(()=>{let e=n?.current;if(!e)return;let t=e.getBoundingClientRect();if(t.width===0){let o=Math.min(640,window.innerWidth*0.9);b({left:(window.innerWidth-o)/2,width:o,bottom:24});return}b({left:t.left,width:t.width,bottom:window.innerHeight-t.bottom+20})},[n]);if(v(()=>{if(!n)return;let e=0,t=()=>{if(n.current)d();else e=requestAnimationFrame(t)};return t(),()=>cancelAnimationFrame(e)},[f,n,d]),v(()=>{let e=n?.current;if(!e||!c)return;return K(e,c,d,{animationFrame:!0})},[n,c,d]),n&&!s)return null;let k=u("div",{ref:A,"data-overlay":"permission-request",className:N("bg-card border-border overflow-hidden rounded-lg border",n&&"fixed z-50"),style:n&&s?{left:s.left,width:s.width,bottom:s.bottom}:void 0,children:[u("div",{className:"border-border border-b px-3 py-2",children:[u("div",{className:"flex items-center justify-between gap-2",children:[r("div",{className:"text-foreground text-sm font-medium",children:j}),w>1&&r("span",{className:"text-muted-foreground shrink-0 text-xs",children:`1 / ${w}`})]}),D]}),r("div",{role:"listbox",tabIndex:0,ref:g,onKeyDown:(e)=>{let t=a.length;if(t===0&&e.key!=="Escape")return;if(e.key==="ArrowDown"||e.key==="ArrowUp")e.preventDefault(),x((o)=>e.key==="ArrowDown"?(o+1)%t:(o-1+t)%t);else if(e.key==="Enter")e.preventDefault(),i(a[l].optionId);else if(e.key==="Escape")e.preventDefault(),i(null);else if(/^[1-9]$/.test(e.key)&&Number(e.key)<=t)e.preventDefault(),i(a[Number(e.key)-1].optionId)},className:"outline-none",children:a.map((e,t)=>{let o=e.kind??"allow_once",C=B[o]?.icon??I,P=o==="allow_always"||o==="reject_always",F=o==="reject_once"||o==="reject_always";return u("div",{role:"option",tabIndex:-1,"aria-selected":t===l,title:e.description,onClick:()=>void i(e.optionId),onKeyDown:(m)=>{if(m.key==="Enter"||m.key===" ")m.preventDefault(),m.stopPropagation(),i(e.optionId)},className:N("flex cursor-pointer items-center gap-2 px-3 py-1.5 text-sm select-none",t===l?"bg-ring text-accent-foreground":"text-foreground",F&&t!==l&&"text-muted-foreground"),children:[r("span",{className:"min-w-3 text-xs opacity-70",children:t+1}),r(C,{className:"size-3.5 shrink-0 opacity-80"}),r("span",{className:"min-w-0 flex-1 truncate",children:e.name}),P&&r("span",{className:"bg-muted text-muted-foreground rounded px-1.5 py-0.5 text-[10px] tracking-wide uppercase",children:E})]},e.optionId)})})]});return n?W(k,document.body):k}export{Y as PermissionView};
1
+ import{jsx as r,jsxs as u}from"react/jsx-runtime";import{autoUpdate as S}from"@floating-ui/dom";import{CheckCheckIcon as z,CheckIcon as _,ShieldXIcon as M,XIcon as O}from"lucide-react";import{useCallback as U,useEffect as v,useId as W,useLayoutEffect as I,useRef as X,useState as p}from"react";import{createPortal as B}from"react-dom";import{cn as N}from"cn";import{sortPermissionOptions as H}from"./permission-options.js";var L={allow_once:{icon:_},allow_always:{icon:z},reject_once:{icon:O},reject_always:{icon:M}};function R({requestId:f,options:j,title:D,children:E,total:w=1,alwaysLabel:A="always",autoFocus:y="always",anchorRef:n,onRespond:i}){let x=W(),[l,b]=p(0),[s,g]=p(null),[a,C]=p(null),k=X(null),c=H(j);v(()=>{b(0)},[f]),v(()=>{let e=document.activeElement;if(y==="always"||e===null||e===document.body)k.current?.focus()},[f,a,y]);let d=U(()=>{let e=n?.current;if(!e)return;let t=e.getBoundingClientRect();if(t.width===0){let o=Math.min(640,window.innerWidth*0.9);g({left:(window.innerWidth-o)/2,width:o,bottom:24});return}g({left:t.left,width:t.width,bottom:window.innerHeight-t.bottom+20})},[n]);if(I(()=>{if(!n)return;let e=0,t=()=>{if(n.current)d();else e=requestAnimationFrame(t)};return t(),()=>cancelAnimationFrame(e)},[f,n,d]),I(()=>{let e=n?.current;if(!e||!a)return;return S(e,a,d,{animationFrame:!0})},[n,a,d]),n&&!s)return null;let h=u("div",{ref:C,"data-overlay":"permission-request",className:N("bg-card border-border overflow-hidden rounded-lg border",n&&"fixed z-50"),style:n&&s?{left:s.left,width:s.width,bottom:s.bottom}:void 0,children:[u("div",{className:"border-border border-b px-3 py-2",children:[u("div",{className:"flex items-center justify-between gap-2",children:[r("div",{id:x,className:"text-foreground text-sm font-medium",children:D}),w>1&&r("span",{className:"text-muted-foreground shrink-0 text-xs",children:`1 / ${w}`})]}),E]}),r("div",{role:"listbox","aria-labelledby":x,tabIndex:0,ref:k,onKeyDown:(e)=>{let t=c.length;if(t===0&&e.key!=="Escape")return;if(e.key==="ArrowDown"||e.key==="ArrowUp")e.preventDefault(),b((o)=>e.key==="ArrowDown"?(o+1)%t:(o-1+t)%t);else if(e.key==="Enter")e.preventDefault(),i(c[l].optionId);else if(e.key==="Escape")e.preventDefault(),i(null);else if(/^[1-9]$/.test(e.key)&&Number(e.key)<=t)e.preventDefault(),i(c[Number(e.key)-1].optionId)},className:"outline-none",children:c.map((e,t)=>{let o=e.kind??"allow_once",P=L[o]?.icon??_,F=o==="allow_always"||o==="reject_always",K=o==="reject_once"||o==="reject_always";return u("div",{role:"option",tabIndex:-1,"aria-selected":t===l,title:e.description,onClick:()=>void i(e.optionId),onKeyDown:(m)=>{if(m.key==="Enter"||m.key===" ")m.preventDefault(),m.stopPropagation(),i(e.optionId)},className:N("flex cursor-pointer items-center gap-2 px-3 py-1.5 text-sm select-none",t===l?"bg-ring text-accent-foreground":"text-foreground",K&&t!==l&&"text-muted-foreground"),children:[r("span",{className:"min-w-3 text-xs opacity-70",children:t+1}),r(P,{className:"size-3.5 shrink-0 opacity-80"}),r("span",{className:"min-w-0 flex-1 truncate",children:e.name}),F&&r("span",{className:"bg-muted text-muted-foreground rounded px-1.5 py-0.5 text-[10px] tracking-wide uppercase",children:A})]},e.optionId)})})]});return n?B(h,document.body):h}export{R as PermissionView};
@@ -1,4 +1,4 @@
1
- /* Codex 流式逐词淡入(_fadeIn_):新词 opacity 0→1,150ms cubic-bezier(.37,.55,.86,.88)。 */
1
+ /* Codex streaming word-by-word fade-in (_fadeIn_): new word opacity 0->1, 150ms cubic-bezier(.37,.55,.86,.88). */
2
2
  .codex-markdown-fade {
3
3
  opacity: 0;
4
4
  animation: codex-markdown-fade-in 0.15s cubic-bezier(0.37, 0.55, 0.86, 0.88) forwards;
@@ -1,8 +1,8 @@
1
1
  @import "./markdown.css";
2
2
 
3
3
  /* ── Codex desktop chat stream ──
4
- * 来自 ChatGPT.app Codex conversation components:宿主 chat 字号、12px turn 虚拟间距、
5
- * 16px conversation item 间距、4px grouped activity 间距、14/16px icon、77% user bubble、12×8px bubble padding。
4
+ * From the ChatGPT.app Codex conversation components: host chat font size, 12px turn virtual spacing,
5
+ * 16px conversation item spacing, 4px grouped activity spacing, 14/16px icon, 77% user bubble, 12x8px bubble padding.
6
6
  */
7
7
  [data-stream-style="compact"],
8
8
  [data-stream-style="codexUI"] {
@@ -22,8 +22,8 @@
22
22
  --codex-checkbox-background: #ffffffb3;
23
23
  --codex-checkbox-foreground: var(--codex-foreground);
24
24
  --codex-link-foreground: #339cff;
25
- /* 色链照抄 Codex app.css:description-foreground(electron) = fg 70% srgb;
26
- * conversation-header 30% / body 60% / summary-trailing 40% / reasoning 后代 50% 均为 oklab;
25
+ /* Color chain copied from Codex app.css: description-foreground(electron) = fg 70% srgb;
26
+ * conversation-header 30% / body 60% / summary-trailing 40% / reasoning descendants 50% are all oklab;
27
27
  * summary-leading = description-foreground 90% oklab;text-secondary = fg 65% srgb。 */
28
28
  --codex-description-foreground: color-mix(in srgb, var(--codex-foreground) 70%, transparent);
29
29
  --codex-text-secondary: color-mix(in srgb, var(--codex-foreground) 65%, transparent);
@@ -45,7 +45,7 @@
45
45
  --codex-border: rgb(255 255 255 / 16%);
46
46
  --codex-hover: rgb(255 255 255 / 8%);
47
47
  --codex-ring: rgb(255 255 255 / 36%);
48
- /* 深色 8% —— Codex 原文 `--user-message-background-color: var(--alpha-08)`,浅色才是 5%(alpha-05) */
48
+ /* Dark 8% -- Codex original `--user-message-background-color: var(--alpha-08)`; light is 5% (alpha-05) */
49
49
  --codex-user-background: rgb(255 255 255 / 8%);
50
50
  --codex-checkbox-background: #212121f5;
51
51
  --codex-link-foreground: #99ceff;
@@ -153,7 +153,7 @@
153
153
  height: 20px;
154
154
  }
155
155
 
156
- /* 回答进行中滚底按钮换成 3 个跳动点(Codex _waveDot_:1s ease-in-out 无限,依次延迟 0.1s)。 */
156
+ /* While an answer is in progress, the scroll-to-bottom button becomes 3 bouncing dots (Codex _waveDot_: 1s ease-in-out infinite, staggered by 0.1s). */
157
157
  .codex-scroll-to-bottom-dots {
158
158
  display: flex;
159
159
  align-items: center;
@@ -626,8 +626,8 @@
626
626
  text-decoration: underline;
627
627
  }
628
628
 
629
- /* Codex `.loading-shimmer-pure-text` 逐值照抄:基色 = description-foreground 55%(暗色 45%),
630
- * 高光亮色 #ffffffbf / 暗色 #0009,50%×200% 背景带,2s steps(48, end) 无限循环。 */
629
+ /* Codex `.loading-shimmer-pure-text` copied value by value: base color = description-foreground 55% (dark 45%),
630
+ * highlight light #ffffffbf / dark #0009, 50%x200% background band, 2s steps(48, end) infinite loop. */
631
631
  .codex-activity-running {
632
632
  --codex-shimmer-base: color-mix(in srgb, var(--codex-description-foreground) 55%, transparent);
633
633
  --codex-shimmer-contrast: #ffffffbf;
@@ -653,12 +653,12 @@
653
653
  --codex-shimmer-contrast: #0009;
654
654
  }
655
655
 
656
- /* Codex 在使用处配 `[&_.loading-shimmer-pure-text]:align-top` 修 inline-block 基线。 */
656
+ /* Codex applies `[&_.loading-shimmer-pure-text]:align-top` at the use site to fix the inline-block baseline. */
657
657
  .codex-activity-label .codex-activity-running {
658
658
  vertical-align: top;
659
659
  }
660
660
 
661
- /* Codex reasoning 标题用 conversation-header(30% fg),hover 走 header 的整体提亮。 */
661
+ /* Codex reasoning titles use conversation-header (30% fg); on hover the header brightens as a whole. */
662
662
  .codex-reasoning-label {
663
663
  color: var(--codex-conversation-header);
664
664
  }
@@ -668,8 +668,8 @@
668
668
  color: inherit;
669
669
  }
670
670
 
671
- /* Codex `.vertical-scroll-fade-mask`:滚动位置驱动上下渐隐(scroll-driven animation)。
672
- * 不支持 animation-timeline 的引擎里两端 fade 保持 0,蒙版退化为全显 —— 与 Codex 同一回退。 */
671
+ /* Codex `.vertical-scroll-fade-mask`: scroll-position-driven top/bottom fade (scroll-driven animation).
672
+ * In engines without animation-timeline both edge fades stay 0 and the mask degrades to fully visible -- same fallback as Codex. */
673
673
  @property --codex-top-fade {
674
674
  syntax: "<length-percentage>";
675
675
  inherits: false;
@@ -695,8 +695,8 @@
695
695
  );
696
696
  }
697
697
 
698
- /* 动画必须圈在 @supports 里:不支持 scroll timeline 的引擎会把它当 0 时长普通动画跑,
699
- * fill-mode both 直接定格在 to 帧 —— 顶部渐隐常驻。圈起来才是真正的"退化为全显"。 */
698
+ /* The animation must be wrapped in @supports: an engine without scroll timeline treats it as a zero-duration ordinary animation,
699
+ * and fill-mode both freezes it on the to frame -- the top fade stays on permanently. Wrapping it is what gives a true "degrade to fully visible". */
700
700
  @supports (animation-timeline: scroll(self y)) {
701
701
  .codex-scroll-fade-mask,
702
702
  .codex-activity-group-body,
@@ -723,7 +723,7 @@
723
723
  }
724
724
  }
725
725
 
726
- /* 各滚动盒的渐隐距离,与 Codex 各处 --edge-fade-distance 一致。 */
726
+ /* Fade distance of each scroll box, matching Codex's --edge-fade-distance everywhere. */
727
727
  .codex-activity-group-body {
728
728
  --codex-edge-fade-distance: 1.5rem;
729
729
  }
@@ -763,8 +763,8 @@
763
763
  padding: 8px 0 4px;
764
764
  }
765
765
 
766
- /* Codex 终端块(bml/_ml):rounded-lg 边框卡,命令区 $ 前缀 + line-clamp-2,
767
- * 输出区 col-reverse 自动贴底,footer 右对齐 Success / Exit code / Stopped。 */
766
+ /* Codex terminal block (bml/_ml): rounded-lg bordered card, command area with $ prefix + line-clamp-2,
767
+ * output area col-reverse to stick to the bottom automatically, footer right-aligned Success / Exit code / Stopped. */
768
768
  .codex-command-card {
769
769
  display: flex;
770
770
  overflow: hidden;
@@ -947,8 +947,8 @@
947
947
  align-items: flex-end;
948
948
  }
949
949
 
950
- /* Codex 原文(app.asar):用户消息整块 `flex flex-col items-end gap-2` ——
951
- 气泡/附件/动作行之间统一 8px。 */
950
+ /* Codex original (app.asar): the whole user message is `flex flex-col items-end gap-2` --
951
+ a uniform 8px between bubble / attachments / action row. */
952
952
  .codex-user-message-group {
953
953
  display: flex;
954
954
  width: 100%;
@@ -972,8 +972,8 @@
972
972
  gap: 8px;
973
973
  }
974
974
 
975
- /* 定尺寸缩略图 + 裁切 —— 对齐 Codex:消息里是小方块,不把原图铺开(2026-08-10)。
976
- 点开的放大层走本仓 full 形态那个遮罩(ImageLightbox),不学 Codex 自己的 lightbox。 */
975
+ /* Fixed-size thumbnail + crop -- aligned with Codex: small squares in the message, the original image isn't spread out (2026-08-10).
976
+ The enlarged layer that opens uses this repo's full-form overlay (ImageLightbox), not Codex's own lightbox. */
977
977
  .codex-user-image {
978
978
  display: block;
979
979
  overflow: hidden;
@@ -994,8 +994,8 @@
994
994
  margin-bottom: 6px;
995
995
  }
996
996
 
997
- /* CDP 实测 Codex 运行时:动作行 `flex items-center gap-0.5`(2px),按钮 26×26 圆角 10、
998
- icon 16 —— 后三者与 .codex-message-action 一致,gap 曾写 4px,改齐。行高对齐按钮盒 26。 */
997
+ /* Codex runtime measured via CDP: action row `flex items-center gap-0.5` (2px), buttons 26x26 radius 10,
998
+ icon 16 -- the latter three match .codex-message-action; gap was written as 4px, now aligned. Row height aligned to the 26 button box. */
999
999
  .codex-user-actions {
1000
1000
  display: flex;
1001
1001
  height: 26px;
@@ -1 +1 @@
1
- import{jsx as e,jsxs as a,Fragment as c}from"react/jsx-runtime";import{ArrowUpRight as P,ChevronUp as O,Loader2Icon as V}from"lucide-react";import{Fragment as ee,useState as j}from"react";import{cn as h}from"cn";import{MenuRadioSelect as p}from"./menu-radio.js";import{EffortSlider as ne}from"./effort-slider.js";import{Button as D}from"../ui/button.js";import{Switch as ae}from"../ui/switch.js";import{Tooltip as T,TooltipContent as A,TooltipTrigger as F}from"../ui/tooltip.js";import{DropdownMenu as I,DropdownMenuContent as R,DropdownMenuGroup as k,DropdownMenuItem as z,DropdownMenuLabel as U,DropdownMenuSeparator as d,DropdownMenuSub as ie,DropdownMenuSubContent as se,DropdownMenuSubTrigger as te,DropdownMenuTrigger as B}from"../ui/dropdown-menu.js";function we({value:u,onChange:E,models:o,modelGroups:x,providerMenu:r,header:g,className:S,condensed:N,stripLabelPrefix:f,showDescription:L,showModelList:W=!0,loading:q,switching:y,isLoading:H,effortConfig:t,booleanOptions:v,onBooleanChange:J,selectOptions:K=[],t:l,...w}){let[Q,X]=j(!1),[Y,Z]=j(!1),_=(n)=>f&&n.startsWith(f)?n.slice(f.length):n;if(q)return e("div",{className:"inline-flex min-w-0 items-center gap-1.5",children:a(D,{variant:"ghost",disabled:!0,className:h("h-7 min-w-0 shrink gap-1.5 rounded-md px-2 text-xs font-normal",S),children:[e(V,{className:"size-3 animate-spin"}),e("span",{className:"text-muted-foreground truncate",children:l("model.loadingModels")})]})});return a("div",{className:"inline-flex min-w-0 items-center gap-1.5",children:[r&&a(I,{open:Q,onOpenChange:X,children:[a(T,{children:[e(F,{render:e(B,{render:a(D,{"data-testid":"provider-trigger",variant:"ghost",disabled:w.disabled,className:h("h-7 shrink-0 gap-1 rounded-md px-1.5","hover:bg-accent"),children:[r.icon,e(O,{className:"size-2.5 shrink-0 opacity-60"})]})})}),e(A,{children:l("model.switchProvider")})]}),a(R,{side:"top",align:"end",sideOffset:4,className:"z-100 min-w-[10rem]",children:[r.groups.map((n,s)=>a(ee,{children:[s>0&&n.length>0&&e(d,{}),e(p,{value:r.value,onValueChange:r.onChange,options:n})]},s)),r.moreAction&&a(c,{children:[e(d,{}),a(z,{onClick:r.moreAction,className:"whitespace-nowrap",children:[e("span",{className:"min-w-0 flex-1 truncate",children:l("model.moreModels")}),e(P,{className:"size-4"})]})]})]})]}),a(I,{open:Y,onOpenChange:Z,children:[a(T,{children:[e(F,{render:e(B,{render:a(D,{"data-testid":"model-trigger","aria-busy":y,variant:"ghost",className:h("h-7 gap-1.5 rounded-md px-2 text-xs font-normal","hover:bg-accent","max-w-full min-w-0 shrink",N&&"h-7 gap-1 px-1.5 text-[11px]",S),...w,children:[e("span",{className:h("min-w-0 flex-1 truncate text-start",N&&"text-[11px]"),children:_(u.label)||l("model.selectModel")}),t&&a("span",{className:"text-muted-foreground grid shrink-0 justify-items-center",children:[e("span",{className:"invisible col-start-1 row-start-1",children:t.sizer??t.levels.reduce((n,s)=>s.name.length>n.length?s.name:n,"")}),e("span",{className:"col-start-1 row-start-1",children:t.levels.find((n)=>n.value===t.value)?.name??t.value})]}),y?e(V,{"data-testid":"model-switch-loading",className:"size-3 shrink-0 animate-spin"}):e(O,{className:h("size-3 shrink-0",N&&"size-2.5")})]})})}),e(A,{children:l("model.switchModel")})]}),a(R,{side:"top",align:"end",sideOffset:4,className:"z-100 w-auto max-w-[min(24rem,calc(100vw-1rem))] min-w-[14rem]",children:[g&&a(c,{children:[a("div",{className:"flex items-center gap-2 px-2 py-2",children:[g.icon,e("span",{className:"min-w-0 flex-1 truncate text-sm font-medium",children:g.name}),g.action]}),e(d,{})]}),a(k,{children:[W&&(H?e(z,{disabled:!0,className:"text-muted-foreground",children:l("model.loadingModels")}):o.length===0?e(z,{disabled:!0,className:"text-muted-foreground",children:l("model.noModelsAvailable")}):(()=>{let n=o.filter((i)=>!i.more),s=o.filter((i)=>i.more),M=n.length>0&&s.length>0,b=(i,m)=>({value:i.api_id,label:_(i.label),description:m?.noDescription?void 0:L?i.description:void 0}),C=(i)=>{let m=o.find((G)=>G.api_id===i);if(m)E(m)};return a(c,{children:[x?x.map((i)=>a(k,{children:[e(U,{className:"text-muted-foreground text-xs font-normal",children:i.name}),e(p,{value:u.api_id,onValueChange:C,itemClassName:"font-medium whitespace-nowrap",options:i.models.map((m)=>b(m))})]},i.id)):e(p,{value:u.api_id,onValueChange:C,itemClassName:"font-medium whitespace-nowrap",options:(M?n:o).map((i)=>b(i))}),!x&&M&&a(ie,{children:[e(te,{className:"font-medium whitespace-nowrap",children:l("model.moreModels")}),e(se,{children:e(p,{value:u.api_id,onValueChange:C,itemClassName:"font-medium whitespace-nowrap",options:s.map((i)=>b(i,{noDescription:!0}))})})]})]})})()),t&&a(c,{children:[e(d,{}),e(ne,{...t,disabled:w.disabled})]}),K.map((n)=>a(k,{children:[e(d,{}),e(U,{children:n.name}),e(p,{value:n.value,onValueChange:n.onChange,options:n.options,itemClassName:"whitespace-nowrap"})]},n.id)),v&&v.length>0&&a(c,{children:[!t&&e(d,{}),v.map((n)=>a("div",{className:"flex items-center justify-between gap-3 px-2 py-2.5",children:[e("span",{className:"text-sm font-medium",children:n.name}),e(ae,{"aria-label":n.name,disabled:w.disabled,checked:n.currentValue,onCheckedChange:(s)=>J?.(n.id,s)})]},n.id))]})]})]})]})]})}export{we as ModelSelect};
1
+ import{jsx as e,jsxs as a,Fragment as u}from"react/jsx-runtime";import{ArrowUpRight as se,ChevronUp as T,Loader2Icon as A}from"lucide-react";import{Fragment as te,useState as F}from"react";import{cn as g}from"cn";import{MenuRadioSelect as d}from"./menu-radio.js";import{EffortSlider as le}from"./effort-slider.js";import{Button as S}from"../ui/button.js";import{Switch as re}from"../ui/switch.js";import{Tooltip as I,TooltipContent as R,TooltipTrigger as U}from"../ui/tooltip.js";import{DropdownMenu as B,DropdownMenuContent as E,DropdownMenuGroup as y,DropdownMenuItem as z,DropdownMenuLabel as L,DropdownMenuSeparator as c,DropdownMenuSub as W,DropdownMenuSubContent as q,DropdownMenuSubTrigger as H,DropdownMenuTrigger as J}from"../ui/dropdown-menu.js";function Ne({value:h,onChange:K,models:p,modelGroups:b,providerMenu:r,header:w,className:_,condensed:C,stripLabelPrefix:D,showDescription:Q,showModelList:X=!0,loading:Y,switching:M,isLoading:Z,effortConfig:t,booleanOptions:k,onBooleanChange:G,selectOptions:P=[],t:l,...x}){let[ee,ne]=F(!1),[ae,ie]=F(!1),V=(n)=>D&&n.startsWith(D)?n.slice(D.length):n;if(Y)return e("div",{className:"inline-flex min-w-0 items-center gap-1.5",children:a(S,{variant:"ghost",disabled:!0,className:g("h-7 min-w-0 shrink gap-1.5 rounded-md px-2 text-xs font-normal",_),children:[e(A,{className:"size-3 animate-spin"}),e("span",{className:"text-muted-foreground truncate",children:l("model.loadingModels")})]})});return a("div",{className:"inline-flex min-w-0 items-center gap-1.5",children:[r&&a(B,{open:ee,onOpenChange:ne,children:[a(I,{children:[e(U,{render:e(J,{render:a(S,{"data-testid":"provider-trigger",variant:"ghost",disabled:x.disabled,className:g("h-7 shrink-0 gap-1 rounded-md px-1.5","hover:bg-accent"),children:[r.icon,e(T,{className:"size-2.5 shrink-0 opacity-60"})]})})}),e(R,{children:l("model.switchProvider")})]}),a(E,{side:"top",align:"end",sideOffset:4,className:"z-100 min-w-[10rem]",children:[r.groups.map((n,s)=>a(te,{children:[s>0&&n.length>0&&e(c,{}),e(d,{value:r.value,onValueChange:r.onChange,options:n})]},s)),r.moreAction&&a(u,{children:[e(c,{}),a(z,{onClick:r.moreAction,className:"whitespace-nowrap",children:[e("span",{className:"min-w-0 flex-1 truncate",children:l("model.moreModels")}),e(se,{className:"size-4"})]})]})]})]}),a(B,{open:ae,onOpenChange:ie,children:[a(I,{children:[e(U,{render:e(J,{render:a(S,{"data-testid":"model-trigger","aria-busy":M,variant:"ghost",className:g("h-7 gap-1.5 rounded-md px-2 text-xs font-normal","hover:bg-accent","max-w-full min-w-0 shrink",C&&"h-7 gap-1 px-1.5 text-[11px]",_),...x,children:[e("span",{className:g("min-w-0 flex-1 truncate text-start",C&&"text-[11px]"),children:V(h.label)||l("model.selectModel")}),t&&a("span",{className:"text-muted-foreground grid shrink-0 justify-items-center",children:[e("span",{className:"invisible col-start-1 row-start-1",children:t.sizer??t.levels.reduce((n,s)=>s.name.length>n.length?s.name:n,"")}),e("span",{className:"col-start-1 row-start-1",children:t.levels.find((n)=>n.value===t.value)?.name??t.value})]}),M?e(A,{"data-testid":"model-switch-loading",className:"size-3 shrink-0 animate-spin"}):e(T,{className:g("size-3 shrink-0",C&&"size-2.5")})]})})}),e(R,{children:l("model.switchModel")})]}),a(E,{side:"top",align:"end",sideOffset:4,className:"z-100 w-auto max-w-[min(24rem,calc(100vw-1rem))] min-w-[14rem]",children:[w&&a(u,{children:[a("div",{className:"flex items-center gap-2 px-2 py-2",children:[w.icon,e("span",{className:"min-w-0 flex-1 truncate text-sm font-medium",children:w.name}),w.action]}),e(c,{})]}),a(y,{children:[X&&(Z?e(z,{disabled:!0,className:"text-muted-foreground",children:l("model.loadingModels")}):p.length===0?e(z,{disabled:!0,className:"text-muted-foreground",children:l("model.noModelsAvailable")}):(()=>{let n=p.filter((i)=>!i.more),s=p.filter((i)=>i.more),O=n.length>0&&s.length>0,N=(i,m)=>({value:i.api_id,label:V(i.label),description:m?.noDescription?void 0:Q?i.description:void 0}),f=(i)=>{let m=p.find((v)=>v.api_id===i);if(m)K(m)};return a(u,{children:[b?b.map((i)=>{let m=i.models.filter((o)=>!o.more),v=i.models.filter((o)=>o.more),j=m.length>0&&v.length>0;return a(y,{children:[e(L,{className:"text-muted-foreground text-xs font-normal",children:i.name}),e(d,{value:h.api_id,onValueChange:f,itemClassName:"font-medium whitespace-nowrap",options:(j?m:i.models).map((o)=>N(o))}),j&&a(W,{children:[e(H,{className:"font-medium whitespace-nowrap",children:l("model.moreModels")}),e(q,{children:e(d,{value:h.api_id,onValueChange:f,itemClassName:"font-medium whitespace-nowrap",options:v.map((o)=>N(o,{noDescription:!0}))})})]})]},i.id)}):e(d,{value:h.api_id,onValueChange:f,itemClassName:"font-medium whitespace-nowrap",options:(O?n:p).map((i)=>N(i))}),!b&&O&&a(W,{children:[e(H,{className:"font-medium whitespace-nowrap",children:l("model.moreModels")}),e(q,{children:e(d,{value:h.api_id,onValueChange:f,itemClassName:"font-medium whitespace-nowrap",options:s.map((i)=>N(i,{noDescription:!0}))})})]})]})})()),t&&a(u,{children:[e(c,{}),e(le,{...t,disabled:x.disabled})]}),P.map((n)=>a(y,{children:[e(c,{}),e(L,{children:n.name}),e(d,{value:n.value,onValueChange:n.onChange,options:n.options,itemClassName:"whitespace-nowrap"})]},n.id)),k&&k.length>0&&a(u,{children:[!t&&e(c,{}),k.map((n)=>a("div",{className:"flex items-center justify-between gap-3 px-2 py-2.5",children:[e("span",{className:"text-sm font-medium",children:n.name}),e(re,{"aria-label":n.name,disabled:x.disabled,checked:n.currentValue,onCheckedChange:(s)=>G?.(n.id,s)})]},n.id))]})]})]})]})]})}export{Ne as ModelSelect};
@@ -0,0 +1,10 @@
1
+ export declare class ChatUpdateScheduler<Snapshot> {
2
+ private readonly publish;
3
+ private readonly pending;
4
+ private timer;
5
+ constructor(publish: (snapshots: Readonly<Record<string, Snapshot>>) => void);
6
+ enqueue(sessionId: string, snapshot: Snapshot): void;
7
+ flush(): void;
8
+ remove(sessionId: string): void;
9
+ cancel(): void;
10
+ }
@@ -0,0 +1 @@
1
+ class t{publish;pending=new Map;timer=null;constructor(i){this.publish=i}enqueue(i,e){this.pending.set(i,e),this.timer??=setTimeout(()=>this.flush(),16)}flush(){if(this.timer!==null)clearTimeout(this.timer);if(this.timer=null,this.pending.size===0)return;let i=Object.fromEntries(this.pending);this.pending.clear(),this.publish(i)}remove(i){if(this.pending.delete(i),this.pending.size===0)this.cancel()}cancel(){if(this.timer!==null)clearTimeout(this.timer);this.timer=null,this.pending.clear()}}export{t as ChatUpdateScheduler};
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@alwith/module-chat",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
7
+ "./update-scheduler": {
8
+ "types": "./dist/lib/update-scheduler.d.ts",
9
+ "import": "./dist/lib/update-scheduler.js",
10
+ "default": "./dist/lib/update-scheduler.js"
11
+ },
7
12
  "./markdown-parser.worker": {
8
13
  "types": "./dist/lib/markdown-parser.worker.d.ts",
9
14
  "import": "./dist/lib/markdown-parser.worker.js",