dsh-monkeyai-sessions 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,7 +61,7 @@ From a checkout:
61
61
  npm install
62
62
  npm run build
63
63
  npm pack
64
- dsh plugin --profile web add ./dsh-monkeyai-sessions-0.1.12.tgz
64
+ dsh plugin --profile web add ./dsh-monkeyai-sessions-0.1.14.tgz
65
65
  ```
66
66
 
67
67
  Verified against a real DSH `0.2.2-rc.1` web profile and a live MonkeyAI runtime: authorization, full history loading, prompt delivery, model switching, and turn cancellation all work end to end.
package/README.zh.md CHANGED
@@ -28,7 +28,7 @@ https://artenx.github.io/dsh-monkeyai-sessions/index.json
28
28
  npm install
29
29
  npm run build
30
30
  npm pack
31
- dsh plugin --profile web add ./dsh-monkeyai-sessions-0.1.12.tgz
31
+ dsh plugin --profile web add ./dsh-monkeyai-sessions-0.1.14.tgz
32
32
  ```
33
33
 
34
34
  已在真实的 DSH `0.2.2-rc.1` web profile 与在线 MonkeyAI runtime 上端到端验证:授权、完整历史加载、指令下发、模型切换、回合取消均正常工作。
package/client.js CHANGED
@@ -553,7 +553,7 @@ window.__ModuleLoader__.load({
553
553
  toolbarCard: { display: "grid", gap: 10, padding: "13px 15px", border: `1px solid ${t.border}`, borderRadius: 12, background: t.surface, backgroundImage: "linear-gradient(90deg, #3568e8 0%, #8baaf4 46%, #e7edfc 100%)", backgroundSize: "100% 3px", backgroundPosition: "0 0", backgroundRepeat: "no-repeat", boxShadow: "0 2px 8px rgba(16,24,40,0.035)" },
554
554
  workspace: { display: "grid", gridTemplateColumns: "minmax(248px, 292px) minmax(0, 1fr)", gap: 12, minWidth: 0, alignItems: "stretch" },
555
555
  sessionSidebar: { display: "grid", gridTemplateRows: "auto minmax(0, 1fr) auto", gap: 10, minWidth: 0, padding: 12, border: `1px solid ${t.border}`, borderRadius: 12, background: t.surface, boxShadow: "0 2px 8px rgba(16,24,40,0.035)" },
556
- sessionList: { display: "grid", gridTemplateColumns: "1fr", alignContent: "start", gap: 6, minHeight: 0, overflowY: "auto", padding: "1px 3px 2px 1px" },
556
+ sessionList: { display: "grid", alignContent: "start", gap: 6, minHeight: 0, overflowY: "auto", padding: "1px 3px 2px 1px" },
557
557
  thread: { display: "grid", gridTemplateRows: "auto minmax(0, 1fr) auto", gap: 10, minWidth: 0, height: "min(70vh, 720px)", minHeight: 440, padding: 12 },
558
558
  emptyThread: { display: "grid", placeContent: "center", justifyItems: "center", gap: 9, minWidth: 0, padding: 28, border: `1px dashed ${t.border2}`, borderRadius: 12, background: "linear-gradient(145deg, #ffffff 0%, #f8faff 100%)", textAlign: "center" },
559
559
  row: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" },
@@ -568,7 +568,6 @@ window.__ModuleLoader__.load({
568
568
  statusBadge: { display: "inline-flex", alignItems: "center", gap: 6, padding: "3px 10px", borderRadius: 999, fontSize: 11.5, fontWeight: 600, background: t.surfaceAlt, border: `1px solid ${t.border}`, color: t.text2, whiteSpace: "nowrap", maxWidth: 340, overflow: "hidden", textOverflow: "ellipsis" },
569
569
  dot: { width: 7, height: 7, borderRadius: "50%", flexShrink: 0 },
570
570
  error: { display: "flex", gap: 8, alignItems: "flex-start", color: t.danger, margin: 0, fontSize: 12.5, background: t.dangerBg, border: `1px solid ${t.dangerBorder}`, borderRadius: t.radius, padding: "8px 10px", whiteSpace: "pre-wrap", overflowWrap: "anywhere" },
571
- sessions: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(230px, 1fr))", gap: 8 },
572
571
  session: { display: "grid", gap: 4, textAlign: "left", padding: "10px 12px", borderRadius: t.radius, border: `1px solid ${t.border}`, background: t.surface, color: t.text, cursor: "pointer", transition: "border-color .15s, background .15s" },
573
572
  selected: { border: `1px solid ${t.accent}`, background: t.accentSoft, boxShadow: `0 0 0 1px ${t.accent} inset` },
574
573
  sessionTitle: { fontSize: 13, fontWeight: 650, lineHeight: 1.35, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
@@ -587,7 +586,18 @@ window.__ModuleLoader__.load({
587
586
  composer: { display: "flex", gap: 8, alignItems: "flex-end" },
588
587
  textarea: { width: "100%", minHeight: 56, maxHeight: 200, boxSizing: "border-box", padding: "8px 11px", borderRadius: t.radius, border: `1px solid ${t.border2}`, background: t.surface, color: t.text, resize: "vertical", outline: "none", fontSize: 13, lineHeight: 1.5, transition: "border-color .15s, box-shadow .15s" },
589
588
  details: { fontSize: 12, color: t.text2 },
590
- summary: { cursor: "pointer", userSelect: "none", fontSize: 12, color: t.text2 }
589
+ summary: { cursor: "pointer", userSelect: "none", fontSize: 12, color: t.text2 },
590
+ modelPicker: { position: "relative", flex: "0 1 200px", minWidth: 132, maxWidth: "100%" },
591
+ modelPickerSummary: { display: "flex", alignItems: "center", gap: 6, width: "100%", boxSizing: "border-box", padding: "7px 10px", borderRadius: t.radius, border: `1px solid ${t.border2}`, background: t.surface, color: t.text, outline: "none", fontSize: 13, cursor: "pointer", listStyle: "none", userSelect: "none" },
592
+ modelPickerCaret: { flexShrink: 0, color: t.text3, fontSize: 9 },
593
+ modelMenu: { position: "absolute", right: 0, top: "calc(100% + 6px)", zIndex: 40, width: "max(100%, 264px)", maxWidth: "94vw", boxSizing: "border-box", display: "grid", gap: 6, padding: 8, borderRadius: 10, border: `1px solid ${t.border}`, background: t.surface, boxShadow: "0 10px 30px rgba(16,24,40,0.16)" },
594
+ modelMenuList: { display: "grid", gap: 2, maxHeight: "52vh", overflowY: "auto", overscrollBehavior: "contain" },
595
+ modelItem: { display: "flex", alignItems: "baseline", gap: 8, width: "100%", boxSizing: "border-box", padding: "7px 9px", borderRadius: 7, border: "1px solid transparent", background: "transparent", color: t.text, cursor: "pointer", fontSize: 12.5, textAlign: "left", transition: "background .12s" },
596
+ modelItemActive: { background: t.accentSoft, borderColor: t.border, fontWeight: 650 },
597
+ modelItemName: { flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", textAlign: "left" },
598
+ modelItemMeta: { flexShrink: 0, fontSize: 11, color: t.text3 },
599
+ modelSearch: { width: "100%", boxSizing: "border-box", padding: "6px 9px", borderRadius: t.radius, border: `1px solid ${t.border2}`, background: t.surface, color: t.text, outline: "none", fontSize: 12.5 },
600
+ modelEmpty: { fontSize: 12, color: t.text3, textAlign: "center", padding: "10px 0" }
591
601
  };
592
602
  var PANEL_CSS = `
593
603
  .mai-panel input:focus, .mai-panel textarea:focus, .mai-panel select:focus { border-color: #3568e8 !important; box-shadow: 0 0 0 2px rgba(53,104,232,0.14) !important; }
@@ -604,6 +614,10 @@ window.__ModuleLoader__.load({
604
614
  .mai-panel .mai-card-hover { transition: border-color .15s, background .15s; }
605
615
  .mai-panel .mai-card-hover:not(:disabled):hover { border-color: #b9cdf3 !important; background: #f7faff !important; }
606
616
  .mai-panel .mai-toolbar { position: relative; }
617
+ .mai-panel .mai-model-picker > summary::-webkit-details-marker { display: none; }
618
+ .mai-panel .mai-model-picker > summary::before { content: none; }
619
+ .mai-panel .mai-model-picker-menu { max-height: 52vh; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
620
+ .mai-panel .mai-model-picker[open] .mai-model-picker-caret { transform: rotate(180deg); }
607
621
  .mai-panel .mai-workspace { display: grid; grid-template-columns: minmax(248px, 292px) minmax(0, 1fr); gap: 12px; min-width: 0; align-items: stretch; }
608
622
  .mai-panel .mai-session-sidebar { min-height: min(70vh, 720px); }
609
623
  .mai-panel .mai-session-list { scrollbar-gutter: stable; }
@@ -625,6 +639,9 @@ window.__ModuleLoader__.load({
625
639
  .mai-panel .mai-page-title { font-size: 16px !important; }
626
640
  .mai-panel .mai-toolbar-controls { align-items: stretch; }
627
641
  .mai-panel .mai-toolbar-controls select { flex-basis: 100% !important; }
642
+ .mai-panel .mai-model-picker { flex: 1 1 100% !important; min-width: 0 !important; }
643
+ .mai-panel .mai-model-menu { position: fixed !important; inset: 50% 12px auto !important; transform: translateY(-50%); width: auto !important; max-width: none !important; max-height: 70vh !important; }
644
+ .mai-panel .mai-model-picker-menu { max-height: calc(70vh - 80px) !important; }
628
645
  .mai-panel .mai-session-list { grid-template-columns: 1fr !important; max-height: 210px; }
629
646
  .mai-panel .mai-thread { height: 68vh; min-height: 360px; padding: 10px; }
630
647
  .mai-panel .mai-thread-header { gap: 6px !important; }
@@ -658,7 +675,7 @@ window.__ModuleLoader__.load({
658
675
  });
659
676
  }
660
677
  function isRemoteStreamCarrierError(error) {
661
- return /api gateway: remote stream websocket (?:failed|closed)/i.test(errorText(error));
678
+ return /api gateway: remote stream websocket (?:failed|closed)/i.test(errorText(error)) || /remote stream socket closed/i.test(errorText(error));
662
679
  }
663
680
  function renderMarkdown(text) {
664
681
  if (typeof text !== "string" || text === "") return null;
@@ -877,6 +894,7 @@ window.__ModuleLoader__.load({
877
894
  const [sessionStatus, setSessionStatus] = useState("");
878
895
  const [signInEmail, setSignInEmail] = useState("");
879
896
  const [signInPassword, setSignInPassword] = useState("");
897
+ const [modelFilter, setModelFilter] = useState("");
880
898
  const streamRef = useRef(null);
881
899
  const historyLoadedRef = useRef(false);
882
900
  const lastDraftRef = useRef("");
@@ -884,6 +902,16 @@ window.__ModuleLoader__.load({
884
902
  const eventsRef = useRef(null);
885
903
  const prevScrollHeightRef = useRef(null);
886
904
  const streamGenerationRef = useRef(0);
905
+ const modelPickerRef = useRef(null);
906
+ useEffect(() => {
907
+ if (typeof document === "undefined" || typeof document.addEventListener !== "function") return void 0;
908
+ const onDocClick = (event) => {
909
+ const picker = modelPickerRef.current;
910
+ if (picker && picker.open && picker.contains && !picker.contains(event.target)) picker.removeAttribute("open");
911
+ };
912
+ document.addEventListener("click", onDocClick);
913
+ return () => document.removeEventListener("click", onDocClick);
914
+ }, []);
887
915
  const title = useMemo(() => {
888
916
  const runtime = runtimeSessions.find((item) => item.id === sessionId);
889
917
  const summary = summarySessions.find((item) => item.id === sessionId);
@@ -1130,6 +1158,7 @@ window.__ModuleLoader__.load({
1130
1158
  setError("");
1131
1159
  setAnswerText("");
1132
1160
  setCurrentModel("");
1161
+ setModelFilter("");
1133
1162
  setVisibleCount(RECENT_LIMIT);
1134
1163
  setHistoryLoading(true);
1135
1164
  setStatus("\u6B63\u5728\u8FDE\u63A5\u8FDC\u7A0B\u4F1A\u8BDD");
@@ -1153,7 +1182,7 @@ window.__ModuleLoader__.load({
1153
1182
  setStatus("\u5386\u53F2\u5DF2\u52A0\u8F7D");
1154
1183
  setHistoryLoading(false);
1155
1184
  setSessionStatus((current) => current || "idle");
1156
- if (frame.model) setCurrentModel(frame.model);
1185
+ if (frame.data?.model) setCurrentModel(frame.data.model);
1157
1186
  }
1158
1187
  if (frame.type === "history_incomplete") {
1159
1188
  setStatus("\u5386\u53F2\u4E0D\u5B8C\u6574\uFF1A" + (frame.message || "\u56DE\u653E\u88AB\u4E2D\u65AD"));
@@ -1177,6 +1206,9 @@ window.__ModuleLoader__.load({
1177
1206
  if (frame.type === "request") {
1178
1207
  setPendingRequests((current) => [...current.filter((item) => item.requestId !== frame.requestId), frame]);
1179
1208
  }
1209
+ if (frame.type === "notification" && frame.method === "_monkeyai/session/model") {
1210
+ if (typeof frame.data?.model === "string" && frame.data.model !== "") setCurrentModel(frame.data.model);
1211
+ }
1180
1212
  if (frame.type === "notification" && frame.method === "_monkeyai/session/state") {
1181
1213
  const status2 = frame.data?.state?.status;
1182
1214
  if (typeof status2 === "string" && SESSION_STATUS_LABEL[status2]) {
@@ -1741,7 +1773,7 @@ window.__ModuleLoader__.load({
1741
1773
  h("h2", { style: style.sectionTitle }, "\u4F1A\u8BDD\u5217\u8868"),
1742
1774
  h("span", { style: style.chip }, String(sessionOptions.length))
1743
1775
  ),
1744
- sessionOptions.length > 0 ? h("div", { style: style.sessions, className: "mai-session-list" }, sessionOptions.map((session) => {
1776
+ sessionOptions.length > 0 ? h("div", { style: style.sessionList, className: "mai-session-list" }, sessionOptions.map((session) => {
1745
1777
  const flag = session.stale ? "\u5DF2\u8FC7\u671F" : session.deviceStatus && session.deviceStatus !== "online" ? session.deviceStatus === "offline" ? "\u79BB\u7EBF" : session.deviceStatus : "";
1746
1778
  return h(
1747
1779
  "button",
@@ -1809,21 +1841,76 @@ window.__ModuleLoader__.load({
1809
1841
  sessionStatus === "running" ? h("span", { className: "mai-pulse", style: { ...style.dot, background: t.accent } }) : null,
1810
1842
  SESSION_STATUS_LABEL[sessionStatus]
1811
1843
  ) : null,
1812
- h(
1813
- "select",
1814
- {
1815
- style: { ...style.input, flex: "0 1 190px", minWidth: 130 },
1816
- value: currentModel,
1817
- disabled: !hasControl || switchingModel || models.length === 0,
1818
- title: "\u5207\u6362\u8BE5\u4F1A\u8BDD\u7684\u6A21\u578B",
1819
- onChange: (event) => {
1820
- const next = event.target.value;
1821
- if (next && next !== currentModel) switchSessionModel(next);
1822
- }
1823
- },
1824
- currentModel && !models.some((m) => m.id === currentModel) ? h("option", { value: currentModel }, currentModel.slice(0, 24)) : null,
1825
- models.map((m) => h("option", { key: m.id, value: m.id }, `${m.displayName} (${Math.round(m.contextWindowTokens / 1e3)}k)`))
1826
- )
1844
+ (() => {
1845
+ const modelPickerDisabled = !hasControl || switchingModel || models.length === 0;
1846
+ const currentModelInfo = models.find((m) => m.id === currentModel);
1847
+ const currentModelUnknown = Boolean(currentModel && !currentModelInfo);
1848
+ const needle = modelFilter.trim().toLowerCase();
1849
+ const filteredModels = needle === "" ? models : models.filter((m) => `${m.displayName} ${m.model ?? ""} ${m.id}`.toLowerCase().includes(needle));
1850
+ return h(
1851
+ "details",
1852
+ {
1853
+ className: "mai-model-picker",
1854
+ style: { ...style.modelPicker, ...modelPickerDisabled ? { opacity: 0.5, pointerEvents: "none" } : {} },
1855
+ ref: modelPickerRef
1856
+ },
1857
+ h(
1858
+ "summary",
1859
+ { style: style.modelPickerSummary, title: "\u5207\u6362\u8BE5\u4F1A\u8BDD\u7684\u6A21\u578B" },
1860
+ h("span", { className: "mai-model-picker-caret", style: { ...style.modelPickerCaret, flexShrink: 0 } }, "\u25BE"),
1861
+ h(
1862
+ "span",
1863
+ { style: { flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", textAlign: "left" } },
1864
+ currentModelInfo ? `${currentModelInfo.displayName} (${Math.round(currentModelInfo.contextWindowTokens / 1e3)}k)` : currentModel ? currentModel.slice(0, 24) : "\u4F1A\u8BDD\u6A21\u578B"
1865
+ )
1866
+ ),
1867
+ h(
1868
+ "div",
1869
+ { className: "mai-model-menu", style: style.modelMenu },
1870
+ models.length > 6 ? h("input", {
1871
+ type: "search",
1872
+ placeholder: "\u641C\u7D22\u6A21\u578B\u2026",
1873
+ "aria-label": "\u641C\u7D22\u6A21\u578B",
1874
+ value: modelFilter,
1875
+ style: style.modelSearch,
1876
+ onChange: (event) => setModelFilter(event.target.value)
1877
+ }) : null,
1878
+ h(
1879
+ "div",
1880
+ { className: "mai-model-picker-menu", style: style.modelMenuList },
1881
+ currentModelUnknown && (needle === "" || currentModel.toLowerCase().includes(needle)) ? h(
1882
+ "button",
1883
+ {
1884
+ type: "button",
1885
+ disabled: true,
1886
+ "aria-pressed": true,
1887
+ style: { ...style.modelItem, ...style.modelItemActive }
1888
+ },
1889
+ h("span", { style: style.modelItemName }, currentModel.slice(0, 24)),
1890
+ h("span", { style: style.modelItemMeta }, "\u5F53\u524D\u6A21\u578B")
1891
+ ) : null,
1892
+ filteredModels.length === 0 ? h("div", { style: style.modelEmpty }, "\u6CA1\u6709\u5339\u914D\u7684\u6A21\u578B") : null,
1893
+ filteredModels.map((m) => h(
1894
+ "button",
1895
+ {
1896
+ key: m.id,
1897
+ type: "button",
1898
+ "aria-pressed": m.id === currentModel,
1899
+ style: { ...style.modelItem, ...m.id === currentModel ? style.modelItemActive : {} },
1900
+ disabled: !hasControl || switchingModel,
1901
+ title: m.model && m.model !== m.displayName ? m.model : void 0,
1902
+ onClick: () => {
1903
+ if (m.id !== currentModel) switchSessionModel(m.id);
1904
+ modelPickerRef.current?.removeAttribute("open");
1905
+ }
1906
+ },
1907
+ h("span", { style: style.modelItemName }, m.displayName),
1908
+ h("span", { style: style.modelItemMeta }, `${Math.round(m.contextWindowTokens / 1e3)}k`)
1909
+ ))
1910
+ )
1911
+ )
1912
+ );
1913
+ })()
1827
1914
  ),
1828
1915
  h(
1829
1916
  "div",
@@ -548,7 +548,7 @@ var style = {
548
548
  toolbarCard: { display: "grid", gap: 10, padding: "13px 15px", border: `1px solid ${t.border}`, borderRadius: 12, background: t.surface, backgroundImage: "linear-gradient(90deg, #3568e8 0%, #8baaf4 46%, #e7edfc 100%)", backgroundSize: "100% 3px", backgroundPosition: "0 0", backgroundRepeat: "no-repeat", boxShadow: "0 2px 8px rgba(16,24,40,0.035)" },
549
549
  workspace: { display: "grid", gridTemplateColumns: "minmax(248px, 292px) minmax(0, 1fr)", gap: 12, minWidth: 0, alignItems: "stretch" },
550
550
  sessionSidebar: { display: "grid", gridTemplateRows: "auto minmax(0, 1fr) auto", gap: 10, minWidth: 0, padding: 12, border: `1px solid ${t.border}`, borderRadius: 12, background: t.surface, boxShadow: "0 2px 8px rgba(16,24,40,0.035)" },
551
- sessionList: { display: "grid", gridTemplateColumns: "1fr", alignContent: "start", gap: 6, minHeight: 0, overflowY: "auto", padding: "1px 3px 2px 1px" },
551
+ sessionList: { display: "grid", alignContent: "start", gap: 6, minHeight: 0, overflowY: "auto", padding: "1px 3px 2px 1px" },
552
552
  thread: { display: "grid", gridTemplateRows: "auto minmax(0, 1fr) auto", gap: 10, minWidth: 0, height: "min(70vh, 720px)", minHeight: 440, padding: 12 },
553
553
  emptyThread: { display: "grid", placeContent: "center", justifyItems: "center", gap: 9, minWidth: 0, padding: 28, border: `1px dashed ${t.border2}`, borderRadius: 12, background: "linear-gradient(145deg, #ffffff 0%, #f8faff 100%)", textAlign: "center" },
554
554
  row: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" },
@@ -563,7 +563,6 @@ var style = {
563
563
  statusBadge: { display: "inline-flex", alignItems: "center", gap: 6, padding: "3px 10px", borderRadius: 999, fontSize: 11.5, fontWeight: 600, background: t.surfaceAlt, border: `1px solid ${t.border}`, color: t.text2, whiteSpace: "nowrap", maxWidth: 340, overflow: "hidden", textOverflow: "ellipsis" },
564
564
  dot: { width: 7, height: 7, borderRadius: "50%", flexShrink: 0 },
565
565
  error: { display: "flex", gap: 8, alignItems: "flex-start", color: t.danger, margin: 0, fontSize: 12.5, background: t.dangerBg, border: `1px solid ${t.dangerBorder}`, borderRadius: t.radius, padding: "8px 10px", whiteSpace: "pre-wrap", overflowWrap: "anywhere" },
566
- sessions: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(230px, 1fr))", gap: 8 },
567
566
  session: { display: "grid", gap: 4, textAlign: "left", padding: "10px 12px", borderRadius: t.radius, border: `1px solid ${t.border}`, background: t.surface, color: t.text, cursor: "pointer", transition: "border-color .15s, background .15s" },
568
567
  selected: { border: `1px solid ${t.accent}`, background: t.accentSoft, boxShadow: `0 0 0 1px ${t.accent} inset` },
569
568
  sessionTitle: { fontSize: 13, fontWeight: 650, lineHeight: 1.35, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
@@ -582,7 +581,18 @@ var style = {
582
581
  composer: { display: "flex", gap: 8, alignItems: "flex-end" },
583
582
  textarea: { width: "100%", minHeight: 56, maxHeight: 200, boxSizing: "border-box", padding: "8px 11px", borderRadius: t.radius, border: `1px solid ${t.border2}`, background: t.surface, color: t.text, resize: "vertical", outline: "none", fontSize: 13, lineHeight: 1.5, transition: "border-color .15s, box-shadow .15s" },
584
583
  details: { fontSize: 12, color: t.text2 },
585
- summary: { cursor: "pointer", userSelect: "none", fontSize: 12, color: t.text2 }
584
+ summary: { cursor: "pointer", userSelect: "none", fontSize: 12, color: t.text2 },
585
+ modelPicker: { position: "relative", flex: "0 1 200px", minWidth: 132, maxWidth: "100%" },
586
+ modelPickerSummary: { display: "flex", alignItems: "center", gap: 6, width: "100%", boxSizing: "border-box", padding: "7px 10px", borderRadius: t.radius, border: `1px solid ${t.border2}`, background: t.surface, color: t.text, outline: "none", fontSize: 13, cursor: "pointer", listStyle: "none", userSelect: "none" },
587
+ modelPickerCaret: { flexShrink: 0, color: t.text3, fontSize: 9 },
588
+ modelMenu: { position: "absolute", right: 0, top: "calc(100% + 6px)", zIndex: 40, width: "max(100%, 264px)", maxWidth: "94vw", boxSizing: "border-box", display: "grid", gap: 6, padding: 8, borderRadius: 10, border: `1px solid ${t.border}`, background: t.surface, boxShadow: "0 10px 30px rgba(16,24,40,0.16)" },
589
+ modelMenuList: { display: "grid", gap: 2, maxHeight: "52vh", overflowY: "auto", overscrollBehavior: "contain" },
590
+ modelItem: { display: "flex", alignItems: "baseline", gap: 8, width: "100%", boxSizing: "border-box", padding: "7px 9px", borderRadius: 7, border: "1px solid transparent", background: "transparent", color: t.text, cursor: "pointer", fontSize: 12.5, textAlign: "left", transition: "background .12s" },
591
+ modelItemActive: { background: t.accentSoft, borderColor: t.border, fontWeight: 650 },
592
+ modelItemName: { flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", textAlign: "left" },
593
+ modelItemMeta: { flexShrink: 0, fontSize: 11, color: t.text3 },
594
+ modelSearch: { width: "100%", boxSizing: "border-box", padding: "6px 9px", borderRadius: t.radius, border: `1px solid ${t.border2}`, background: t.surface, color: t.text, outline: "none", fontSize: 12.5 },
595
+ modelEmpty: { fontSize: 12, color: t.text3, textAlign: "center", padding: "10px 0" }
586
596
  };
587
597
  var PANEL_CSS = `
588
598
  .mai-panel input:focus, .mai-panel textarea:focus, .mai-panel select:focus { border-color: #3568e8 !important; box-shadow: 0 0 0 2px rgba(53,104,232,0.14) !important; }
@@ -599,6 +609,10 @@ var PANEL_CSS = `
599
609
  .mai-panel .mai-card-hover { transition: border-color .15s, background .15s; }
600
610
  .mai-panel .mai-card-hover:not(:disabled):hover { border-color: #b9cdf3 !important; background: #f7faff !important; }
601
611
  .mai-panel .mai-toolbar { position: relative; }
612
+ .mai-panel .mai-model-picker > summary::-webkit-details-marker { display: none; }
613
+ .mai-panel .mai-model-picker > summary::before { content: none; }
614
+ .mai-panel .mai-model-picker-menu { max-height: 52vh; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
615
+ .mai-panel .mai-model-picker[open] .mai-model-picker-caret { transform: rotate(180deg); }
602
616
  .mai-panel .mai-workspace { display: grid; grid-template-columns: minmax(248px, 292px) minmax(0, 1fr); gap: 12px; min-width: 0; align-items: stretch; }
603
617
  .mai-panel .mai-session-sidebar { min-height: min(70vh, 720px); }
604
618
  .mai-panel .mai-session-list { scrollbar-gutter: stable; }
@@ -620,6 +634,9 @@ var PANEL_CSS = `
620
634
  .mai-panel .mai-page-title { font-size: 16px !important; }
621
635
  .mai-panel .mai-toolbar-controls { align-items: stretch; }
622
636
  .mai-panel .mai-toolbar-controls select { flex-basis: 100% !important; }
637
+ .mai-panel .mai-model-picker { flex: 1 1 100% !important; min-width: 0 !important; }
638
+ .mai-panel .mai-model-menu { position: fixed !important; inset: 50% 12px auto !important; transform: translateY(-50%); width: auto !important; max-width: none !important; max-height: 70vh !important; }
639
+ .mai-panel .mai-model-picker-menu { max-height: calc(70vh - 80px) !important; }
623
640
  .mai-panel .mai-session-list { grid-template-columns: 1fr !important; max-height: 210px; }
624
641
  .mai-panel .mai-thread { height: 68vh; min-height: 360px; padding: 10px; }
625
642
  .mai-panel .mai-thread-header { gap: 6px !important; }
@@ -653,7 +670,7 @@ function withTimeout(promise, timeoutMs, label) {
653
670
  });
654
671
  }
655
672
  function isRemoteStreamCarrierError(error) {
656
- return /api gateway: remote stream websocket (?:failed|closed)/i.test(errorText(error));
673
+ return /api gateway: remote stream websocket (?:failed|closed)/i.test(errorText(error)) || /remote stream socket closed/i.test(errorText(error));
657
674
  }
658
675
  function renderMarkdown(text) {
659
676
  if (typeof text !== "string" || text === "") return null;
@@ -872,6 +889,7 @@ function RemoteSessionPage({ api }) {
872
889
  const [sessionStatus, setSessionStatus] = useState("");
873
890
  const [signInEmail, setSignInEmail] = useState("");
874
891
  const [signInPassword, setSignInPassword] = useState("");
892
+ const [modelFilter, setModelFilter] = useState("");
875
893
  const streamRef = useRef(null);
876
894
  const historyLoadedRef = useRef(false);
877
895
  const lastDraftRef = useRef("");
@@ -879,6 +897,16 @@ function RemoteSessionPage({ api }) {
879
897
  const eventsRef = useRef(null);
880
898
  const prevScrollHeightRef = useRef(null);
881
899
  const streamGenerationRef = useRef(0);
900
+ const modelPickerRef = useRef(null);
901
+ useEffect(() => {
902
+ if (typeof document === "undefined" || typeof document.addEventListener !== "function") return void 0;
903
+ const onDocClick = (event) => {
904
+ const picker = modelPickerRef.current;
905
+ if (picker && picker.open && picker.contains && !picker.contains(event.target)) picker.removeAttribute("open");
906
+ };
907
+ document.addEventListener("click", onDocClick);
908
+ return () => document.removeEventListener("click", onDocClick);
909
+ }, []);
882
910
  const title = useMemo(() => {
883
911
  const runtime = runtimeSessions.find((item) => item.id === sessionId);
884
912
  const summary = summarySessions.find((item) => item.id === sessionId);
@@ -1125,6 +1153,7 @@ function RemoteSessionPage({ api }) {
1125
1153
  setError("");
1126
1154
  setAnswerText("");
1127
1155
  setCurrentModel("");
1156
+ setModelFilter("");
1128
1157
  setVisibleCount(RECENT_LIMIT);
1129
1158
  setHistoryLoading(true);
1130
1159
  setStatus("\u6B63\u5728\u8FDE\u63A5\u8FDC\u7A0B\u4F1A\u8BDD");
@@ -1148,7 +1177,7 @@ function RemoteSessionPage({ api }) {
1148
1177
  setStatus("\u5386\u53F2\u5DF2\u52A0\u8F7D");
1149
1178
  setHistoryLoading(false);
1150
1179
  setSessionStatus((current) => current || "idle");
1151
- if (frame.model) setCurrentModel(frame.model);
1180
+ if (frame.data?.model) setCurrentModel(frame.data.model);
1152
1181
  }
1153
1182
  if (frame.type === "history_incomplete") {
1154
1183
  setStatus("\u5386\u53F2\u4E0D\u5B8C\u6574\uFF1A" + (frame.message || "\u56DE\u653E\u88AB\u4E2D\u65AD"));
@@ -1172,6 +1201,9 @@ function RemoteSessionPage({ api }) {
1172
1201
  if (frame.type === "request") {
1173
1202
  setPendingRequests((current) => [...current.filter((item) => item.requestId !== frame.requestId), frame]);
1174
1203
  }
1204
+ if (frame.type === "notification" && frame.method === "_monkeyai/session/model") {
1205
+ if (typeof frame.data?.model === "string" && frame.data.model !== "") setCurrentModel(frame.data.model);
1206
+ }
1175
1207
  if (frame.type === "notification" && frame.method === "_monkeyai/session/state") {
1176
1208
  const status2 = frame.data?.state?.status;
1177
1209
  if (typeof status2 === "string" && SESSION_STATUS_LABEL[status2]) {
@@ -1736,7 +1768,7 @@ function RemoteSessionPage({ api }) {
1736
1768
  h("h2", { style: style.sectionTitle }, "\u4F1A\u8BDD\u5217\u8868"),
1737
1769
  h("span", { style: style.chip }, String(sessionOptions.length))
1738
1770
  ),
1739
- sessionOptions.length > 0 ? h("div", { style: style.sessions, className: "mai-session-list" }, sessionOptions.map((session) => {
1771
+ sessionOptions.length > 0 ? h("div", { style: style.sessionList, className: "mai-session-list" }, sessionOptions.map((session) => {
1740
1772
  const flag = session.stale ? "\u5DF2\u8FC7\u671F" : session.deviceStatus && session.deviceStatus !== "online" ? session.deviceStatus === "offline" ? "\u79BB\u7EBF" : session.deviceStatus : "";
1741
1773
  return h(
1742
1774
  "button",
@@ -1804,21 +1836,76 @@ function RemoteSessionPage({ api }) {
1804
1836
  sessionStatus === "running" ? h("span", { className: "mai-pulse", style: { ...style.dot, background: t.accent } }) : null,
1805
1837
  SESSION_STATUS_LABEL[sessionStatus]
1806
1838
  ) : null,
1807
- h(
1808
- "select",
1809
- {
1810
- style: { ...style.input, flex: "0 1 190px", minWidth: 130 },
1811
- value: currentModel,
1812
- disabled: !hasControl || switchingModel || models.length === 0,
1813
- title: "\u5207\u6362\u8BE5\u4F1A\u8BDD\u7684\u6A21\u578B",
1814
- onChange: (event) => {
1815
- const next = event.target.value;
1816
- if (next && next !== currentModel) switchSessionModel(next);
1817
- }
1818
- },
1819
- currentModel && !models.some((m) => m.id === currentModel) ? h("option", { value: currentModel }, currentModel.slice(0, 24)) : null,
1820
- models.map((m) => h("option", { key: m.id, value: m.id }, `${m.displayName} (${Math.round(m.contextWindowTokens / 1e3)}k)`))
1821
- )
1839
+ (() => {
1840
+ const modelPickerDisabled = !hasControl || switchingModel || models.length === 0;
1841
+ const currentModelInfo = models.find((m) => m.id === currentModel);
1842
+ const currentModelUnknown = Boolean(currentModel && !currentModelInfo);
1843
+ const needle = modelFilter.trim().toLowerCase();
1844
+ const filteredModels = needle === "" ? models : models.filter((m) => `${m.displayName} ${m.model ?? ""} ${m.id}`.toLowerCase().includes(needle));
1845
+ return h(
1846
+ "details",
1847
+ {
1848
+ className: "mai-model-picker",
1849
+ style: { ...style.modelPicker, ...modelPickerDisabled ? { opacity: 0.5, pointerEvents: "none" } : {} },
1850
+ ref: modelPickerRef
1851
+ },
1852
+ h(
1853
+ "summary",
1854
+ { style: style.modelPickerSummary, title: "\u5207\u6362\u8BE5\u4F1A\u8BDD\u7684\u6A21\u578B" },
1855
+ h("span", { className: "mai-model-picker-caret", style: { ...style.modelPickerCaret, flexShrink: 0 } }, "\u25BE"),
1856
+ h(
1857
+ "span",
1858
+ { style: { flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", textAlign: "left" } },
1859
+ currentModelInfo ? `${currentModelInfo.displayName} (${Math.round(currentModelInfo.contextWindowTokens / 1e3)}k)` : currentModel ? currentModel.slice(0, 24) : "\u4F1A\u8BDD\u6A21\u578B"
1860
+ )
1861
+ ),
1862
+ h(
1863
+ "div",
1864
+ { className: "mai-model-menu", style: style.modelMenu },
1865
+ models.length > 6 ? h("input", {
1866
+ type: "search",
1867
+ placeholder: "\u641C\u7D22\u6A21\u578B\u2026",
1868
+ "aria-label": "\u641C\u7D22\u6A21\u578B",
1869
+ value: modelFilter,
1870
+ style: style.modelSearch,
1871
+ onChange: (event) => setModelFilter(event.target.value)
1872
+ }) : null,
1873
+ h(
1874
+ "div",
1875
+ { className: "mai-model-picker-menu", style: style.modelMenuList },
1876
+ currentModelUnknown && (needle === "" || currentModel.toLowerCase().includes(needle)) ? h(
1877
+ "button",
1878
+ {
1879
+ type: "button",
1880
+ disabled: true,
1881
+ "aria-pressed": true,
1882
+ style: { ...style.modelItem, ...style.modelItemActive }
1883
+ },
1884
+ h("span", { style: style.modelItemName }, currentModel.slice(0, 24)),
1885
+ h("span", { style: style.modelItemMeta }, "\u5F53\u524D\u6A21\u578B")
1886
+ ) : null,
1887
+ filteredModels.length === 0 ? h("div", { style: style.modelEmpty }, "\u6CA1\u6709\u5339\u914D\u7684\u6A21\u578B") : null,
1888
+ filteredModels.map((m) => h(
1889
+ "button",
1890
+ {
1891
+ key: m.id,
1892
+ type: "button",
1893
+ "aria-pressed": m.id === currentModel,
1894
+ style: { ...style.modelItem, ...m.id === currentModel ? style.modelItemActive : {} },
1895
+ disabled: !hasControl || switchingModel,
1896
+ title: m.model && m.model !== m.displayName ? m.model : void 0,
1897
+ onClick: () => {
1898
+ if (m.id !== currentModel) switchSessionModel(m.id);
1899
+ modelPickerRef.current?.removeAttribute("open");
1900
+ }
1901
+ },
1902
+ h("span", { style: style.modelItemName }, m.displayName),
1903
+ h("span", { style: style.modelItemMeta }, `${Math.round(m.contextWindowTokens / 1e3)}k`)
1904
+ ))
1905
+ )
1906
+ )
1907
+ );
1908
+ })()
1822
1909
  ),
1823
1910
  h(
1824
1911
  "div",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-monkeyai-sessions",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "在 DeepSeek Harness 中读取和继续 MonkeyAI 会话:查看完整对话历史、下发新指令,无缝接续之前在 MonkeyAI 客户端上的工作",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -38,7 +38,7 @@ const style = {
38
38
  toolbarCard: { display: 'grid', gap: 10, padding: '13px 15px', border: `1px solid ${t.border}`, borderRadius: 12, background: t.surface, backgroundImage: 'linear-gradient(90deg, #3568e8 0%, #8baaf4 46%, #e7edfc 100%)', backgroundSize: '100% 3px', backgroundPosition: '0 0', backgroundRepeat: 'no-repeat', boxShadow: '0 2px 8px rgba(16,24,40,0.035)' },
39
39
  workspace: { display: 'grid', gridTemplateColumns: 'minmax(248px, 292px) minmax(0, 1fr)', gap: 12, minWidth: 0, alignItems: 'stretch' },
40
40
  sessionSidebar: { display: 'grid', gridTemplateRows: 'auto minmax(0, 1fr) auto', gap: 10, minWidth: 0, padding: 12, border: `1px solid ${t.border}`, borderRadius: 12, background: t.surface, boxShadow: '0 2px 8px rgba(16,24,40,0.035)' },
41
- sessionList: { display: 'grid', gridTemplateColumns: '1fr', alignContent: 'start', gap: 6, minHeight: 0, overflowY: 'auto', padding: '1px 3px 2px 1px' },
41
+ sessionList: { display: 'grid', alignContent: 'start', gap: 6, minHeight: 0, overflowY: 'auto', padding: '1px 3px 2px 1px' },
42
42
  thread: { display: 'grid', gridTemplateRows: 'auto minmax(0, 1fr) auto', gap: 10, minWidth: 0, height: 'min(70vh, 720px)', minHeight: 440, padding: 12 },
43
43
  emptyThread: { display: 'grid', placeContent: 'center', justifyItems: 'center', gap: 9, minWidth: 0, padding: 28, border: `1px dashed ${t.border2}`, borderRadius: 12, background: 'linear-gradient(145deg, #ffffff 0%, #f8faff 100%)', textAlign: 'center' },
44
44
  row: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' },
@@ -53,7 +53,6 @@ const style = {
53
53
  statusBadge: { display: 'inline-flex', alignItems: 'center', gap: 6, padding: '3px 10px', borderRadius: 999, fontSize: 11.5, fontWeight: 600, background: t.surfaceAlt, border: `1px solid ${t.border}`, color: t.text2, whiteSpace: 'nowrap', maxWidth: 340, overflow: 'hidden', textOverflow: 'ellipsis' },
54
54
  dot: { width: 7, height: 7, borderRadius: '50%', flexShrink: 0 },
55
55
  error: { display: 'flex', gap: 8, alignItems: 'flex-start', color: t.danger, margin: 0, fontSize: 12.5, background: t.dangerBg, border: `1px solid ${t.dangerBorder}`, borderRadius: t.radius, padding: '8px 10px', whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' },
56
- sessions: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(230px, 1fr))', gap: 8 },
57
56
  session: { display: 'grid', gap: 4, textAlign: 'left', padding: '10px 12px', borderRadius: t.radius, border: `1px solid ${t.border}`, background: t.surface, color: t.text, cursor: 'pointer', transition: 'border-color .15s, background .15s' },
58
57
  selected: { border: `1px solid ${t.accent}`, background: t.accentSoft, boxShadow: `0 0 0 1px ${t.accent} inset` },
59
58
  sessionTitle: { fontSize: 13, fontWeight: 650, lineHeight: 1.35, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
@@ -73,6 +72,17 @@ const style = {
73
72
  textarea: { width: '100%', minHeight: 56, maxHeight: 200, boxSizing: 'border-box', padding: '8px 11px', borderRadius: t.radius, border: `1px solid ${t.border2}`, background: t.surface, color: t.text, resize: 'vertical', outline: 'none', fontSize: 13, lineHeight: 1.5, transition: 'border-color .15s, box-shadow .15s' },
74
73
  details: { fontSize: 12, color: t.text2 },
75
74
  summary: { cursor: 'pointer', userSelect: 'none', fontSize: 12, color: t.text2 },
75
+ modelPicker: { position: 'relative', flex: '0 1 200px', minWidth: 132, maxWidth: '100%' },
76
+ modelPickerSummary: { display: 'flex', alignItems: 'center', gap: 6, width: '100%', boxSizing: 'border-box', padding: '7px 10px', borderRadius: t.radius, border: `1px solid ${t.border2}`, background: t.surface, color: t.text, outline: 'none', fontSize: 13, cursor: 'pointer', listStyle: 'none', userSelect: 'none' },
77
+ modelPickerCaret: { flexShrink: 0, color: t.text3, fontSize: 9 },
78
+ modelMenu: { position: 'absolute', right: 0, top: 'calc(100% + 6px)', zIndex: 40, width: 'max(100%, 264px)', maxWidth: '94vw', boxSizing: 'border-box', display: 'grid', gap: 6, padding: 8, borderRadius: 10, border: `1px solid ${t.border}`, background: t.surface, boxShadow: '0 10px 30px rgba(16,24,40,0.16)' },
79
+ modelMenuList: { display: 'grid', gap: 2, maxHeight: '52vh', overflowY: 'auto', overscrollBehavior: 'contain' },
80
+ modelItem: { display: 'flex', alignItems: 'baseline', gap: 8, width: '100%', boxSizing: 'border-box', padding: '7px 9px', borderRadius: 7, border: '1px solid transparent', background: 'transparent', color: t.text, cursor: 'pointer', fontSize: 12.5, textAlign: 'left', transition: 'background .12s' },
81
+ modelItemActive: { background: t.accentSoft, borderColor: t.border, fontWeight: 650 },
82
+ modelItemName: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'left' },
83
+ modelItemMeta: { flexShrink: 0, fontSize: 11, color: t.text3 },
84
+ modelSearch: { width: '100%', boxSizing: 'border-box', padding: '6px 9px', borderRadius: t.radius, border: `1px solid ${t.border2}`, background: t.surface, color: t.text, outline: 'none', fontSize: 12.5 },
85
+ modelEmpty: { fontSize: 12, color: t.text3, textAlign: 'center', padding: '10px 0' },
76
86
  }
77
87
 
78
88
  const PANEL_CSS = `
@@ -90,6 +100,10 @@ const PANEL_CSS = `
90
100
  .mai-panel .mai-card-hover { transition: border-color .15s, background .15s; }
91
101
  .mai-panel .mai-card-hover:not(:disabled):hover { border-color: #b9cdf3 !important; background: #f7faff !important; }
92
102
  .mai-panel .mai-toolbar { position: relative; }
103
+ .mai-panel .mai-model-picker > summary::-webkit-details-marker { display: none; }
104
+ .mai-panel .mai-model-picker > summary::before { content: none; }
105
+ .mai-panel .mai-model-picker-menu { max-height: 52vh; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
106
+ .mai-panel .mai-model-picker[open] .mai-model-picker-caret { transform: rotate(180deg); }
93
107
  .mai-panel .mai-workspace { display: grid; grid-template-columns: minmax(248px, 292px) minmax(0, 1fr); gap: 12px; min-width: 0; align-items: stretch; }
94
108
  .mai-panel .mai-session-sidebar { min-height: min(70vh, 720px); }
95
109
  .mai-panel .mai-session-list { scrollbar-gutter: stable; }
@@ -111,6 +125,9 @@ const PANEL_CSS = `
111
125
  .mai-panel .mai-page-title { font-size: 16px !important; }
112
126
  .mai-panel .mai-toolbar-controls { align-items: stretch; }
113
127
  .mai-panel .mai-toolbar-controls select { flex-basis: 100% !important; }
128
+ .mai-panel .mai-model-picker { flex: 1 1 100% !important; min-width: 0 !important; }
129
+ .mai-panel .mai-model-menu { position: fixed !important; inset: 50% 12px auto !important; transform: translateY(-50%); width: auto !important; max-width: none !important; max-height: 70vh !important; }
130
+ .mai-panel .mai-model-picker-menu { max-height: calc(70vh - 80px) !important; }
114
131
  .mai-panel .mai-session-list { grid-template-columns: 1fr !important; max-height: 210px; }
115
132
  .mai-panel .mai-thread { height: 68vh; min-height: 360px; padding: 10px; }
116
133
  .mai-panel .mai-thread-header { gap: 6px !important; }
@@ -142,6 +159,7 @@ function withTimeout(promise, timeoutMs, label) {
142
159
 
143
160
  function isRemoteStreamCarrierError(error) {
144
161
  return /api gateway: remote stream websocket (?:failed|closed)/i.test(errorText(error))
162
+ || /remote stream socket closed/i.test(errorText(error))
145
163
  }
146
164
 
147
165
  // Lightweight inline markdown: fenced code blocks, `code`, **bold**,
@@ -343,6 +361,7 @@ function RemoteSessionPage({ api }) {
343
361
  const [sessionStatus, setSessionStatus] = useState('')
344
362
  const [signInEmail, setSignInEmail] = useState('')
345
363
  const [signInPassword, setSignInPassword] = useState('')
364
+ const [modelFilter, setModelFilter] = useState('')
346
365
  const streamRef = useRef(null)
347
366
  const historyLoadedRef = useRef(false)
348
367
  const lastDraftRef = useRef('')
@@ -350,6 +369,17 @@ function RemoteSessionPage({ api }) {
350
369
  const eventsRef = useRef(null)
351
370
  const prevScrollHeightRef = useRef(null)
352
371
  const streamGenerationRef = useRef(0)
372
+ const modelPickerRef = useRef(null)
373
+
374
+ useEffect(() => {
375
+ if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') return undefined
376
+ const onDocClick = event => {
377
+ const picker = modelPickerRef.current
378
+ if (picker && picker.open && picker.contains && !picker.contains(event.target)) picker.removeAttribute('open')
379
+ }
380
+ document.addEventListener('click', onDocClick)
381
+ return () => document.removeEventListener('click', onDocClick)
382
+ }, [])
353
383
  const title = useMemo(() => {
354
384
  const runtime = runtimeSessions.find(item => item.id === sessionId)
355
385
  const summary = summarySessions.find(item => item.id === sessionId)
@@ -606,6 +636,7 @@ function RemoteSessionPage({ api }) {
606
636
  setError('')
607
637
  setAnswerText('')
608
638
  setCurrentModel('')
639
+ setModelFilter('')
609
640
  setVisibleCount(RECENT_LIMIT)
610
641
  setHistoryLoading(true)
611
642
  setStatus('正在连接远程会话')
@@ -629,7 +660,7 @@ function RemoteSessionPage({ api }) {
629
660
  setStatus('历史已加载')
630
661
  setHistoryLoading(false)
631
662
  setSessionStatus(current => current || 'idle')
632
- if (frame.model) setCurrentModel(frame.model)
663
+ if (frame.data?.model) setCurrentModel(frame.data.model)
633
664
  }
634
665
  if (frame.type === 'history_incomplete') {
635
666
  setStatus('历史不完整:' + (frame.message || '回放被中断'))
@@ -653,6 +684,9 @@ function RemoteSessionPage({ api }) {
653
684
  if (frame.type === 'request') {
654
685
  setPendingRequests(current => [...current.filter(item => item.requestId !== frame.requestId), frame])
655
686
  }
687
+ if (frame.type === 'notification' && frame.method === '_monkeyai/session/model') {
688
+ if (typeof frame.data?.model === 'string' && frame.data.model !== '') setCurrentModel(frame.data.model)
689
+ }
656
690
  if (frame.type === 'notification' && frame.method === '_monkeyai/session/state') {
657
691
  const status = frame.data?.state?.status
658
692
  if (typeof status === 'string' && SESSION_STATUS_LABEL[status]) {
@@ -1101,7 +1135,7 @@ function RemoteSessionPage({ api }) {
1101
1135
  h('h2', { style: style.sectionTitle }, '会话列表'),
1102
1136
  h('span', { style: style.chip }, String(sessionOptions.length))),
1103
1137
  sessionOptions.length > 0
1104
- ? h('div', { style: style.sessions, className: 'mai-session-list' }, sessionOptions.map(session => {
1138
+ ? h('div', { style: style.sessionList, className: 'mai-session-list' }, sessionOptions.map(session => {
1105
1139
  const flag = session.stale ? '已过期' : session.deviceStatus && session.deviceStatus !== 'online' ? (session.deviceStatus === 'offline' ? '离线' : session.deviceStatus) : ''
1106
1140
  return h('button', {
1107
1141
  style: { ...style.session, ...(session.id === sessionId ? style.selected : {}) },
@@ -1142,20 +1176,55 @@ function RemoteSessionPage({ api }) {
1142
1176
  },
1143
1177
  sessionStatus === 'running' ? h('span', { className: 'mai-pulse', style: { ...style.dot, background: t.accent } }) : null,
1144
1178
  SESSION_STATUS_LABEL[sessionStatus]) : null,
1145
- h('select', {
1146
- style: { ...style.input, flex: '0 1 190px', minWidth: 130 },
1147
- value: currentModel,
1148
- disabled: !hasControl || switchingModel || models.length === 0,
1149
- title: '切换该会话的模型',
1150
- onChange: event => {
1151
- const next = event.target.value
1152
- if (next && next !== currentModel) switchSessionModel(next)
1179
+ (() => {
1180
+ const modelPickerDisabled = !hasControl || switchingModel || models.length === 0
1181
+ const currentModelInfo = models.find(m => m.id === currentModel)
1182
+ const currentModelUnknown = Boolean(currentModel && !currentModelInfo)
1183
+ const needle = modelFilter.trim().toLowerCase()
1184
+ const filteredModels = needle === ''
1185
+ ? models
1186
+ : models.filter(m => `${m.displayName} ${m.model ?? ''} ${m.id}`.toLowerCase().includes(needle))
1187
+ return h('details', {
1188
+ className: 'mai-model-picker',
1189
+ style: { ...style.modelPicker, ...(modelPickerDisabled ? { opacity: 0.5, pointerEvents: 'none' } : {}) },
1190
+ ref: modelPickerRef,
1153
1191
  },
1154
- },
1155
- currentModel && !models.some(m => m.id === currentModel)
1156
- ? h('option', { value: currentModel }, currentModel.slice(0, 24))
1157
- : null,
1158
- models.map(m => h('option', { key: m.id, value: m.id }, `${m.displayName} (${Math.round(m.contextWindowTokens / 1000)}k)`)))),
1192
+ h('summary', { style: style.modelPickerSummary, title: '切换该会话的模型' },
1193
+ h('span', { className: 'mai-model-picker-caret', style: { ...style.modelPickerCaret, flexShrink: 0 } }, '▾'),
1194
+ h('span', { style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'left' } },
1195
+ currentModelInfo
1196
+ ? `${currentModelInfo.displayName} (${Math.round(currentModelInfo.contextWindowTokens / 1000)}k)`
1197
+ : currentModel ? currentModel.slice(0, 24) : '会话模型')),
1198
+ h('div', { className: 'mai-model-menu', style: style.modelMenu },
1199
+ models.length > 6 ? h('input', {
1200
+ type: 'search', placeholder: '搜索模型…', 'aria-label': '搜索模型', value: modelFilter,
1201
+ style: style.modelSearch,
1202
+ onChange: event => setModelFilter(event.target.value),
1203
+ }) : null,
1204
+ h('div', { className: 'mai-model-picker-menu', style: style.modelMenuList },
1205
+ currentModelUnknown && (needle === '' || currentModel.toLowerCase().includes(needle))
1206
+ ? h('button', {
1207
+ type: 'button', disabled: true, 'aria-pressed': true,
1208
+ style: { ...style.modelItem, ...style.modelItemActive },
1209
+ },
1210
+ h('span', { style: style.modelItemName }, currentModel.slice(0, 24)),
1211
+ h('span', { style: style.modelItemMeta }, '当前模型'))
1212
+ : null,
1213
+ filteredModels.length === 0 ? h('div', { style: style.modelEmpty }, '没有匹配的模型') : null,
1214
+ filteredModels.map(m => h('button', {
1215
+ key: m.id, type: 'button',
1216
+ 'aria-pressed': m.id === currentModel,
1217
+ style: { ...style.modelItem, ...(m.id === currentModel ? style.modelItemActive : {}) },
1218
+ disabled: !hasControl || switchingModel,
1219
+ title: m.model && m.model !== m.displayName ? m.model : undefined,
1220
+ onClick: () => {
1221
+ if (m.id !== currentModel) switchSessionModel(m.id)
1222
+ modelPickerRef.current?.removeAttribute('open')
1223
+ },
1224
+ },
1225
+ h('span', { style: style.modelItemName }, m.displayName),
1226
+ h('span', { style: style.modelItemMeta }, `${Math.round(m.contextWindowTokens / 1000)}k`))))))
1227
+ })()),
1159
1228
  h('div', { style: style.events, className: 'mai-thread-events mai-events', 'aria-live': 'polite', 'data-events': '', ref: eventsRef,
1160
1229
  onScroll: event => {
1161
1230
  const el = event.currentTarget
@@ -739,7 +739,9 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
739
739
  const emitHistoryLoaded = model => {
740
740
  if (historyLoadedEmitted) return
741
741
  historyLoadedEmitted = true
742
- emit({ type: 'history_loaded', sessionId: selectedSession, ...(model === undefined ? {} : { model }) })
742
+ // model travels inside data: the RemoteFrame schema only passes through
743
+ // declared top-level fields, so a bare `model` key would be stripped.
744
+ emit({ type: 'history_loaded', sessionId: selectedSession, ...(model === undefined ? {} : { data: { model } }) })
743
745
  }
744
746
  const refreshPersistedHistory = async changedMessageId => {
745
747
  if (!ticketHttpBase || !ticketValue) return
@@ -1055,6 +1057,21 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
1055
1057
  ticketHttpBase = ticket.httpBase
1056
1058
  if (signal.aborted) throw signal.reason ?? new Error('远程会话已取消')
1057
1059
 
1060
+ // Surface the session's current model as early as possible. Large
1061
+ // histories paginate slowly and the stream carrier can drop mid-walk;
1062
+ // a one-message read already carries session.model, so the model picker
1063
+ // syncs before (and independently of) the history walk.
1064
+ try {
1065
+ const probeTimeout = AbortSignal.any([AbortSignal.timeout(20_000), ...(signal ? [signal] : [])])
1066
+ const modelProbe = await readSessionHistoryPage({ httpBase: ticket.httpBase, ticket: ticketValue, sessionId: selectedSession, limit: 1, signal: probeTimeout })
1067
+ if (typeof modelProbe.session?.model === 'string' && modelProbe.session.model !== '') {
1068
+ yield { type: 'notification', sessionId: selectedSession, method: '_monkeyai/session/model', data: { model: modelProbe.session.model } }
1069
+ }
1070
+ } catch {
1071
+ // Best-effort: history_loaded still carries the model when its walk
1072
+ // completes, and the picker shows the placeholder meanwhile.
1073
+ }
1074
+
1058
1075
  // Load the full history over the runtime's HTTP read channel before the
1059
1076
  // ACP connection starts. The ACP session/load replay only streams a
1060
1077
  // small recent window and the socket is closed mid-replay; the HTTP
@@ -1071,7 +1088,10 @@ export class MonkeyAIRemoteService extends TypertRemoteService {
1071
1088
  if (history.count > 0) {
1072
1089
  httpHistoryComplete = true
1073
1090
  httpConfigurationRevision = history.configurationRevision
1074
- emitHistoryLoaded()
1091
+ // The runtime's session/load response omits the current model; the
1092
+ // history channel's session object carries it, so surface it with
1093
+ // history_loaded to preselect the right entry in the model picker.
1094
+ emitHistoryLoaded(history.sessionModel)
1075
1095
  }
1076
1096
  } catch (historyError) {
1077
1097
  if (signal?.aborted) throw historyError
@@ -1518,12 +1538,16 @@ async function fetchFullSessionHistory({ httpBase, ticket, sessionId, signal, on
1518
1538
  const toolCallsById = {}
1519
1539
  let cursor
1520
1540
  let configurationRevision
1541
+ let sessionModel
1521
1542
  for (let page = 0; page < maxPages; page++) {
1522
1543
  const timeout = AbortSignal.any([AbortSignal.timeout(30_000), ...(signal ? [signal] : [])])
1523
1544
  const result = await readSessionHistoryPage({ httpBase, ticket, sessionId, cursor, direction: 'older', signal: timeout })
1524
1545
  if (Number.isSafeInteger(result.session?.configurationRevision)) {
1525
1546
  configurationRevision = result.session.configurationRevision
1526
1547
  }
1548
+ if (typeof result.session?.model === 'string' && result.session.model !== '') {
1549
+ sessionModel = result.session.model
1550
+ }
1527
1551
  const pageMessages = []
1528
1552
  for (const message of Array.isArray(result.messages) ? result.messages : []) {
1529
1553
  if (typeof message?.id !== 'string' || message.id === '' || seenIds.has(message.id)) continue
@@ -1540,7 +1564,7 @@ async function fetchFullSessionHistory({ httpBase, ticket, sessionId, signal, on
1540
1564
  for (const frame of historyMessageToFrames(sessionId, message, toolCallsById)) await onFrame(frame)
1541
1565
  }
1542
1566
  }
1543
- return { messageIds: [...seenIds], count: seenIds.size, configurationRevision }
1567
+ return { messageIds: [...seenIds], count: seenIds.size, configurationRevision, sessionModel }
1544
1568
  }
1545
1569
 
1546
1570
  // Refresh only the newest pages after a _monkeyai/session/history_changed