@linxin666/dsh-remote-web-ui 0.2.0 → 0.2.1

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.
Files changed (68) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +15 -7
  3. package/README.zh.md +4 -4
  4. package/lib/client.js +235 -37
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +26 -7
  7. package/lib/mobile.js +209 -39
  8. package/lib/mobile.js.map +1 -1
  9. package/lib/types/client/PluginSettingsCard.d.ts +22 -0
  10. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  11. package/lib/types/client/PluginSettingsCard.js +142 -3
  12. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  13. package/lib/types/client/RemoteEntry.js +11 -0
  14. package/lib/types/client/RemoteSettingsCard.d.ts +2 -2
  15. package/lib/types/client/UpdatePanel.d.ts.map +1 -1
  16. package/lib/types/client/UpdatePanel.js +2 -2
  17. package/lib/types/client/locales.d.ts +2 -0
  18. package/lib/types/client/locales.d.ts.map +1 -1
  19. package/lib/types/client/locales.js +2 -0
  20. package/lib/types/gate.d.ts.map +1 -1
  21. package/lib/types/gate.js +2 -0
  22. package/lib/types/mobile/api.d.ts +10 -0
  23. package/lib/types/mobile/api.d.ts.map +1 -1
  24. package/lib/types/mobile/api.js +4 -0
  25. package/lib/types/mobile/messages.d.ts +24 -0
  26. package/lib/types/mobile/messages.d.ts.map +1 -1
  27. package/lib/types/mobile/messages.js +60 -23
  28. package/lib/types/mobile/messages.test.js +49 -1
  29. package/lib/types/mobile/mobile-styles.d.ts +1 -1
  30. package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
  31. package/lib/types/mobile/mobile-styles.js +38 -0
  32. package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
  33. package/lib/types/mobile/views/ChatView.js +34 -11
  34. package/lib/types/mobile/views/SessionListView.d.ts.map +1 -1
  35. package/lib/types/mobile/views/SessionListView.js +30 -4
  36. package/lib/types/mobile/views/SessionListView.test.js +58 -1
  37. package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
  38. package/lib/types/mobile/views/WorkspaceView.js +4 -2
  39. package/lib/types/mobile-api.d.ts.map +1 -1
  40. package/lib/types/mobile-api.js +10 -1
  41. package/lib/types/routes.d.ts.map +1 -1
  42. package/lib/types/routes.js +22 -2
  43. package/lib/types/tunnel.d.ts +1 -0
  44. package/lib/types/tunnel.d.ts.map +1 -1
  45. package/lib/types/tunnel.js +7 -2
  46. package/lib/types/update.d.ts +1 -1
  47. package/lib/types/update.d.ts.map +1 -1
  48. package/lib/types/update.js +1 -1
  49. package/package.json +1 -1
  50. package/src/client/PluginSettingsCard.tsx +219 -18
  51. package/src/client/RemoteEntry.tsx +10 -0
  52. package/src/client/RemoteSettingsCard.tsx +2 -2
  53. package/src/client/UpdatePanel.tsx +2 -0
  54. package/src/client/locales.ts +2 -0
  55. package/src/client/settings-card.module.css +90 -1
  56. package/src/gate.ts +2 -0
  57. package/src/mobile/api.ts +14 -1
  58. package/src/mobile/messages.test.ts +52 -1
  59. package/src/mobile/messages.ts +65 -22
  60. package/src/mobile/mobile-styles.ts +38 -0
  61. package/src/mobile/views/ChatView.tsx +33 -10
  62. package/src/mobile/views/SessionListView.test.tsx +69 -1
  63. package/src/mobile/views/SessionListView.tsx +58 -5
  64. package/src/mobile/views/WorkspaceView.tsx +4 -2
  65. package/src/mobile-api.ts +9 -1
  66. package/src/routes.ts +22 -2
  67. package/src/tunnel.ts +7 -2
  68. package/src/update.ts +2 -2
@@ -88,6 +88,27 @@ export declare function ValueField(props: FieldProps & {
88
88
  /** Placeholder shown while the draft is empty. */
89
89
  placeholder?: string;
90
90
  }): import("react").JSX.Element;
91
+ interface SelectOption {
92
+ value: string;
93
+ label: string;
94
+ }
95
+ /**
96
+ * The shared dual-mode select control. While an appearance skin is active it
97
+ * renders the legacy native `<select>` untouched, so element-level skin
98
+ * selectors keep working; under the default appearance it renders a
99
+ * self-drawn `role="listbox"` popup whose open/close is transition-animated.
100
+ * Staged cards reach it through BooleanField/ChoiceField; immediate-apply
101
+ * editors (the side-card prefs) bind it directly through onEdit.
102
+ * 双模式下拉框:皮肤激活时用原生 select,默认外观用自绘动画弹层。
103
+ */
104
+ export declare function SelectField(props: {
105
+ id: string;
106
+ options: ReadonlyArray<SelectOption>;
107
+ value: string;
108
+ disabled: boolean;
109
+ invalid: boolean;
110
+ onEdit: (text: string) => void;
111
+ }): import("react").JSX.Element;
91
112
  /** A staged boolean field: 继承 / 开 / 关. */
92
113
  export declare function BooleanField(props: FieldProps & {
93
114
  /** Copy for the inherit option. */
@@ -107,4 +128,5 @@ export declare function ChoiceField(props: FieldProps & {
107
128
  label: string;
108
129
  }>;
109
130
  }): import("react").JSX.Element;
131
+ export {};
110
132
  //# sourceMappingURL=PluginSettingsCard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PluginSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PluginSettingsCard.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PluginSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PluginSettingsCard.tsx"],"names":[],"mappings":"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"}
@@ -8,7 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  * it. Inlined into each consumer's client bundle; mirrors the official
9
9
  * ui-plugin-config PluginCard in a self-contained slice.
10
10
  */
11
- import { useState } from 'react';
11
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
12
12
  import css from './settings-card.module.css';
13
13
  /** Copy keys the card chrome itself reads; every consumer locale carries this shared vocabulary. */
14
14
  export const CARD_COPY_KEYS = [
@@ -65,15 +65,154 @@ export function ValueField(props) {
65
65
  ? (_jsxs("span", { className: css.badges, children: [_jsx("span", { className: css.badge, children: props.overriddenLabel }), _jsx("button", { type: "button", className: css.reset, disabled: props.disabled, onClick: props.onReset, children: props.resetLabel })] }))
66
66
  : null] }), _jsx("input", { id: props.id, className: props.invalid ? css.inputInvalid : css.input, type: "text", ...props.numeric === true ? { inputMode: 'numeric' } : {}, ...props.invalid ? { 'aria-invalid': true } : {}, value: props.text, placeholder: props.placeholder ?? '', disabled: props.disabled, onChange: (event) => { props.onEdit(event.target.value); } }), _jsx("p", { className: props.invalid ? css.invalid : css.hint, children: props.invalid ? props.invalidLabel : props.hint })] }));
67
67
  }
68
+ const NON_SKIN_BODY_MARKERS = new Set(['dshSkinCenter', 'dshSidebarCollapsed']);
69
+ // 检测使用使用皮肤,用了皮肤退回原生select样式,防止样式冲突。默认外观下使用优化后的select样式。
70
+ function isSkinActive() {
71
+ const datasetList = Object.keys(document.body.dataset);
72
+ const isActive = datasetList.some(key => key.startsWith('dsh') && !NON_SKIN_BODY_MARKERS.has(key));
73
+ return isActive;
74
+ }
75
+ const SELECT_CLOSE_MS = 100;
76
+ /**
77
+ * The shared dual-mode select control. While an appearance skin is active it
78
+ * renders the legacy native `<select>` untouched, so element-level skin
79
+ * selectors keep working; under the default appearance it renders a
80
+ * self-drawn `role="listbox"` popup whose open/close is transition-animated.
81
+ * Staged cards reach it through BooleanField/ChoiceField; immediate-apply
82
+ * editors (the side-card prefs) bind it directly through onEdit.
83
+ * 双模式下拉框:皮肤激活时用原生 select,默认外观用自绘动画弹层。
84
+ */
85
+ export function SelectField(props) {
86
+ const { id, options, value } = props;
87
+ const [open, setOpen] = useState(false);
88
+ const [closing, setClosing] = useState(false);
89
+ const [phase, setPhase] = useState('initial');
90
+ const [activeIndex, setActiveIndex] = useState(0);
91
+ const closeTimer = useRef(undefined);
92
+ const wrapRef = useRef(null);
93
+ const popupRef = useRef(null);
94
+ const currentIndex = () => {
95
+ const index = options.findIndex(option => option.value === value);
96
+ return index >= 0 ? index : 0;
97
+ };
98
+ const close = useCallback(() => {
99
+ if (closeTimer.current !== undefined)
100
+ clearTimeout(closeTimer.current);
101
+ setClosing(true);
102
+ closeTimer.current = setTimeout(() => {
103
+ setClosing(false);
104
+ setOpen(false);
105
+ }, SELECT_CLOSE_MS);
106
+ }, []);
107
+ const openPopup = () => {
108
+ if (closeTimer.current !== undefined)
109
+ clearTimeout(closeTimer.current);
110
+ setActiveIndex(currentIndex());
111
+ setPhase('initial');
112
+ setClosing(false);
113
+ setOpen(true);
114
+ };
115
+ const commit = (index) => {
116
+ const option = options[index];
117
+ if (option)
118
+ props.onEdit(option.value);
119
+ close();
120
+ };
121
+ const onTriggerClick = () => {
122
+ if (props.disabled)
123
+ return;
124
+ if (open && !closing)
125
+ close();
126
+ else
127
+ openPopup();
128
+ };
129
+ const onKeyDown = (event) => {
130
+ if (props.disabled)
131
+ return;
132
+ const count = options.length;
133
+ switch (event.key) {
134
+ case 'ArrowDown':
135
+ case 'ArrowUp':
136
+ case 'Enter':
137
+ case ' ':
138
+ event.preventDefault();
139
+ if (!open) {
140
+ openPopup();
141
+ }
142
+ else if (!closing) {
143
+ if (event.key === 'ArrowDown')
144
+ setActiveIndex(index => (index + 1) % count);
145
+ else if (event.key === 'ArrowUp')
146
+ setActiveIndex(index => (index - 1 + count) % count);
147
+ else
148
+ commit(activeIndex);
149
+ }
150
+ break;
151
+ case 'Escape':
152
+ if (open) {
153
+ event.preventDefault();
154
+ event.stopPropagation();
155
+ close();
156
+ }
157
+ break;
158
+ case 'Tab':
159
+ if (open)
160
+ close();
161
+ break;
162
+ }
163
+ };
164
+ useEffect(() => () => {
165
+ if (closeTimer.current !== undefined)
166
+ clearTimeout(closeTimer.current);
167
+ }, []);
168
+ useLayoutEffect(() => {
169
+ if (open && !closing && phase === 'initial') {
170
+ void popupRef.current?.offsetHeight;
171
+ setPhase('open');
172
+ }
173
+ }, [open, closing, phase]);
174
+ useEffect(() => {
175
+ if (!open)
176
+ return;
177
+ const onPointerDown = (event) => {
178
+ const target = event.target;
179
+ if (target instanceof Node && !wrapRef.current?.contains(target))
180
+ close();
181
+ };
182
+ document.addEventListener('pointerdown', onPointerDown);
183
+ return () => document.removeEventListener('pointerdown', onPointerDown);
184
+ }, [open, close]);
185
+ useEffect(() => {
186
+ if (props.disabled && open)
187
+ close();
188
+ }, [props.disabled, open, close]);
189
+ // 使用了皮肤则退回原生select样式防止样式冲突。
190
+ if (isSkinActive()) {
191
+ return (_jsx("select", { id: id, className: css.select, value: value, disabled: props.disabled, onChange: (event) => { props.onEdit(event.target.value); }, children: options.map(option => (_jsx("option", { value: option.value, children: option.label }, option.value))) }));
192
+ }
193
+ const label = options.find(option => option.value === value)?.label ?? '';
194
+ const popupClass = closing
195
+ ? `${css.selectPopup} ${css.selectPopupClose}`
196
+ : phase === 'open'
197
+ ? `${css.selectPopup} ${css.selectPopupOpen}`
198
+ : css.selectPopup;
199
+ return (_jsxs("div", { className: css.selectWrap, ref: wrapRef, children: [_jsxs("button", { type: "button", id: id, className: `${css.select} ${css.selectButton}`, disabled: props.disabled, "aria-haspopup": "listbox", "aria-expanded": open, "aria-activedescendant": open ? `${id}-o${activeIndex}` : undefined, "aria-invalid": props.invalid || undefined, onClick: onTriggerClick, onKeyDown: onKeyDown, children: [_jsx("span", { className: css.selectLabel, children: label }), _jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: open ? `${css.selectChevron} ${css.selectChevronOpen}` : css.selectChevron, "aria-hidden": "true", children: _jsx("path", { d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z", fill: "currentColor" }) })] }), open
200
+ ? (_jsx("div", { className: popupClass, role: "listbox", ref: popupRef, children: options.map((option, index) => (_jsx("div", { id: `${id}-o${index}`, role: "option", "aria-selected": option.value === value, className: `${css.selectOption}${option.value === value ? ` ${css.selectOptionSelected}` : ''}${index === activeIndex && !closing ? ` ${css.selectOptionActive}` : ''}`, onClick: () => { commit(index); }, children: option.label }, option.value))) }))
201
+ : null] }));
202
+ }
68
203
  /** A staged boolean field: 继承 / 开 / 关. */
69
204
  export function BooleanField(props) {
70
205
  return (_jsxs("div", { className: css.field, children: [_jsxs("div", { className: css.head, children: [_jsx("label", { className: css.label, htmlFor: props.id, children: props.label }), props.overridden
71
206
  ? (_jsxs("span", { className: css.badges, children: [_jsx("span", { className: css.badge, children: props.overriddenLabel }), _jsx("button", { type: "button", className: css.reset, disabled: props.disabled, onClick: props.onReset, children: props.resetLabel })] }))
72
- : null] }), _jsxs("select", { id: props.id, className: css.select, value: props.text, disabled: props.disabled, onChange: (event) => { props.onEdit(event.target.value); }, children: [_jsx("option", { value: "", children: props.inheritLabel }), _jsx("option", { value: "true", children: props.onLabel }), _jsx("option", { value: "false", children: props.offLabel })] }), _jsx("p", { className: css.hint, children: props.hint })] }));
207
+ : null] }), _jsx(SelectField, { id: props.id, options: [
208
+ { value: '', label: props.inheritLabel },
209
+ { value: 'true', label: props.onLabel },
210
+ { value: 'false', label: props.offLabel },
211
+ ], value: props.text, disabled: props.disabled, invalid: props.invalid, onEdit: props.onEdit }), _jsx("p", { className: css.hint, children: props.hint })] }));
73
212
  }
74
213
  /** A staged enumerated field rendered as a select. */
75
214
  export function ChoiceField(props) {
76
215
  return (_jsxs("div", { className: css.field, children: [_jsxs("div", { className: css.head, children: [_jsx("label", { className: css.label, htmlFor: props.id, children: props.label }), props.overridden
77
216
  ? (_jsxs("span", { className: css.badges, children: [_jsx("span", { className: css.badge, children: props.overriddenLabel }), _jsx("button", { type: "button", className: css.reset, disabled: props.disabled, onClick: props.onReset, children: props.resetLabel })] }))
78
- : null] }), _jsxs("select", { id: props.id, className: css.select, value: props.text, disabled: props.disabled, onChange: (event) => { props.onEdit(event.target.value); }, children: [_jsx("option", { value: "", children: props.inheritLabel }), props.choices.map(choice => (_jsx("option", { value: choice.value, children: choice.label }, choice.value)))] }), _jsx("p", { className: props.invalid ? css.invalid : css.hint, children: props.invalid ? props.invalidLabel : props.hint })] }));
217
+ : null] }), _jsx(SelectField, { id: props.id, options: [{ value: '', label: props.inheritLabel }, ...props.choices], value: props.text, disabled: props.disabled, invalid: props.invalid, onEdit: props.onEdit }), _jsx("p", { className: props.invalid ? css.invalid : css.hint, children: props.invalid ? props.invalidLabel : props.hint })] }));
79
218
  }
@@ -1 +1 @@
1
- {"version":3,"file":"RemoteEntry.d.ts","sourceRoot":"","sources":["../../../src/client/RemoteEntry.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAQjF,wEAAwE;AACxE,MAAM,MAAM,gBAAgB,GAAG,YAAY,CAAC,gBAAgB,CAAC,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAA;AAwBrF;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,EAAE,EAAE,gBAAgB,+BA6KvE"}
1
+ {"version":3,"file":"RemoteEntry.d.ts","sourceRoot":"","sources":["../../../src/client/RemoteEntry.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAQjF,wEAAwE;AACxE,MAAM,MAAM,gBAAgB,GAAG,YAAY,CAAC,gBAAgB,CAAC,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAA;AAwBrF;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,EAAE,EAAE,gBAAgB,+BAuLvE"}
@@ -52,6 +52,10 @@ export function RemoteEntry({ wide, useWorkspaces, t }) {
52
52
  useEffect(() => { stateRef.current = state; }, [state]);
53
53
  const [copied, setCopied] = useState(false);
54
54
  const eventSource = useRef(undefined);
55
+ // Generation counter for the open flow: closing (or re-opening) the panel
56
+ // bumps it, so an in-flight issue() that resolves after a close does not
57
+ // spawn a stray EventSource.
58
+ const openSeq = useRef(0);
55
59
  // The current workspace (the recent-workspace projection the shell's New
56
60
  // Session flow targets) — the deep-link target for the phone.
57
61
  const workspaceId = useWorkspaces(s => s.recentWorkspaceId);
@@ -98,8 +102,14 @@ export function RemoteEntry({ wide, useWorkspaces, t }) {
98
102
  };
99
103
  }, [workspaceId]);
100
104
  const openPanel = useCallback(async () => {
105
+ const seq = ++openSeq.current;
101
106
  setOpen(true);
102
107
  const next = await mint();
108
+ // A close (or re-open) during the await invalidates this issue: skip the
109
+ // state write and the stream so a panel closed mid-mint neither leaks an
110
+ // EventSource nor resurrects a stale QR.
111
+ if (seq !== openSeq.current)
112
+ return;
103
113
  setState(next);
104
114
  // Live status: the desktop panel mirrors the pairing service state. The
105
115
  // stream makes sense in the ready state and on the lan-required banner —
@@ -137,6 +147,7 @@ export function RemoteEntry({ wide, useWorkspaces, t }) {
137
147
  };
138
148
  }, [mint]);
139
149
  const closePanel = useCallback(() => {
150
+ openSeq.current += 1;
140
151
  closeEventSource();
141
152
  setOpen(false);
142
153
  }, [closeEventSource]);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The remote-control settings card: pairing security and device limits.
3
- * Registers into the `settings.plugin.item` slot the plugin-configuration
4
- * section renders, bound to the `remote-web-ui` settings namespace.
3
+ * Registers into the `web-ui.plugin.item` child slot the Web UI plugin group
4
+ * renders, bound to the `remote-web-ui` settings namespace.
5
5
  */
6
6
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
7
7
  import type { SettingsScope, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
@@ -1 +1 @@
1
- {"version":3,"file":"UpdatePanel.d.ts","sourceRoot":"","sources":["../../../src/client/UpdatePanel.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAEnE,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAGjE,0DAA0D;AAC1D,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvD,qDAAqD;AACrD,MAAM,WAAW,gBAAgB;IAC/B,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACxB,IAAI,EAAE,UAAU,CAAA;IAChB,OAAO,IAAI,IAAI,CAAA;IACf,8CAA8C;IAC9C,SAAS,IAAI,IAAI,CAAA;CAClB;AAYD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,gBAAgB,+BAyC5E"}
1
+ {"version":3,"file":"UpdatePanel.d.ts","sourceRoot":"","sources":["../../../src/client/UpdatePanel.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAEnE,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAGjE,0DAA0D;AAC1D,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvD,qDAAqD;AACrD,MAAM,WAAW,gBAAgB;IAC/B,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACxB,IAAI,EAAE,UAAU,CAAA;IAChB,OAAO,IAAI,IAAI,CAAA;IACf,8CAA8C;IAC9C,SAAS,IAAI,IAAI,CAAA;CAClB;AAYD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,gBAAgB,+BA0C5E"}
@@ -18,7 +18,7 @@ export function UpdatePanel({ t, view, onClose, onRecheck }) {
18
18
  const status = view.kind === "result" || view.kind === "updating" ? view.status : undefined;
19
19
  const title = view.kind === "done" && view.result.ok ? t("update.done") : t("update.title");
20
20
  const subtitle = subtitleOf(t, view);
21
- return (_jsxs("div", { className: css.panel, role: "dialog", "aria-modal": "true", "aria-label": title, children: [_jsxs("div", { className: css.header, children: [_jsxs("div", { className: css.heading, children: [_jsx("h2", { className: css.title, children: title }), subtitle !== undefined && _jsx("p", { className: css.subtitle, children: subtitle })] }), _jsx("button", { type: "button", className: css.close, "aria-label": t("update.close"), onClick: onClose, children: _jsx(IconCloseOutline16, {}) })] }), view.kind === "checking" && _jsx("p", { className: css.updateStatus, children: t("update.checking") }), view.kind === "result" && status !== undefined && _jsx(ResultBody, { t: t, status: status }), view.kind === "updating" && status !== undefined && (_jsxs("div", { children: [_jsx("p", { className: css.updateStatus, children: t("update.updating", { name: anchorName(status) ?? "", version: anchorLatest(status) ?? "" }) }), _jsx(PackageList, { status: status })] })), view.kind === "done" && _jsx(DoneBody, { t: t, result: view.result }), view.kind === "error" && (_jsxs("div", { children: [_jsx("p", { className: css.updateError, children: view.message }), view.detail !== undefined && _jsx("pre", { className: css.updateOutput, children: view.detail })] })), (view.kind === "done" || view.kind === "error") && (_jsx("div", { className: css.updateActions, children: _jsxs("button", { type: "button", className: css.updateRetry, onClick: onRecheck, children: [_jsx(IconRefreshOutline16, {}), " ", t("update.retry")] }) }))] }));
21
+ return (_jsxs("div", { className: css.panel, role: "dialog", "aria-modal": "true", "aria-label": title, children: [_jsxs("div", { className: css.header, children: [_jsxs("div", { className: css.heading, children: [_jsx("h2", { className: css.title, children: title }), subtitle !== undefined && _jsx("p", { className: css.subtitle, children: subtitle })] }), _jsx("button", { type: "button", className: css.close, "aria-label": t("update.close"), onClick: onClose, children: _jsx(IconCloseOutline16, {}) })] }), view.kind === "checking" && _jsx("p", { className: css.updateStatus, children: t("update.checking") }), view.kind === "result" && status !== undefined && _jsx(ResultBody, { t: t, status: status }), view.kind === "updating" && status !== undefined && (_jsxs("div", { children: [_jsx("p", { className: css.updateStatus, children: t("update.updating", { name: anchorName(status) ?? "", version: anchorLatest(status) ?? "" }) }), _jsx("p", { className: css.updateDetail, children: t("update.cooldownNotice") }), _jsx(PackageList, { status: status })] })), view.kind === "done" && _jsx(DoneBody, { t: t, result: view.result }), view.kind === "error" && (_jsxs("div", { children: [_jsx("p", { className: css.updateError, children: view.message }), view.detail !== undefined && _jsx("pre", { className: css.updateOutput, children: view.detail })] })), (view.kind === "done" || view.kind === "error") && (_jsx("div", { className: css.updateActions, children: _jsxs("button", { type: "button", className: css.updateRetry, onClick: onRecheck, children: [_jsx(IconRefreshOutline16, {}), " ", t("update.retry")] }) }))] }));
22
22
  }
23
23
  /** The subtitle copy per view state (absent on plain results). */
24
24
  function subtitleOf(t, view) {
@@ -50,7 +50,7 @@ function ResultBody({ t, status }) {
50
50
  return (_jsxs("div", { children: [_jsx("p", { className: css.updateStatus, children: t("update.offline") }), _jsx("p", { className: css.updateDetail, children: t("update.offlineDetail") })] }));
51
51
  }
52
52
  if (status.outdated) {
53
- return (_jsxs("div", { children: [_jsx("p", { className: css.updateStatus, children: t("update.found") }), _jsx("p", { className: css.updateDetail, children: anchor !== undefined ? t("update.foundDetail", { name: anchor, version: latest ?? "" }) : "" })] }));
53
+ return (_jsxs("div", { children: [_jsx("p", { className: css.updateStatus, children: t("update.found") }), _jsx("p", { className: css.updateDetail, children: anchor !== undefined ? t("update.foundDetail", { name: anchor, version: latest ?? "" }) : "" }), _jsx("p", { className: css.updateDetail, children: t("update.cooldownNotice") })] }));
54
54
  }
55
55
  return (_jsxs("div", { children: [_jsx("p", { className: css.updateStatus, children: t("update.upToDate") }), anchor !== undefined && latest !== undefined && (_jsx("p", { className: css.updateDetail, children: t("update.upToDateDetail", { name: anchor, version: latest }) })), _jsx(PackageList, { status: status })] }));
56
56
  }
@@ -79,6 +79,7 @@ export declare const zh: {
79
79
  'update.found': string;
80
80
  'update.foundDetail': string;
81
81
  'update.updating': string;
82
+ 'update.cooldownNotice': string;
82
83
  'update.updatingTitle': string;
83
84
  'update.done': string;
84
85
  'update.doneDetail': string;
@@ -188,6 +189,7 @@ export declare const en: {
188
189
  'update.found': string;
189
190
  'update.foundDetail': string;
190
191
  'update.updating': string;
192
+ 'update.cooldownNotice': string;
191
193
  'update.updatingTitle': string;
192
194
  'update.done': string;
193
195
  'update.doneDetail': string;
@@ -1 +1 @@
1
- {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyGmB,CAAA;AAElC,sCAAsC;AACtC,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,EAAE,CAAA;AAEvC,wDAAwD;AACxD,MAAM,MAAM,eAAe,GAAG,SAAS,CAAA;AAEvC,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyGsB,CAAA"}
1
+ {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0GmB,CAAA;AAElC,sCAAsC;AACtC,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,EAAE,CAAA;AAEvC,wDAAwD;AACxD,MAAM,MAAM,eAAe,GAAG,SAAS,CAAA;AAEvC,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0GsB,CAAA"}
@@ -81,6 +81,7 @@ export const zh = {
81
81
  'update.found': '发现新版本',
82
82
  'update.foundDetail': '{name} 有新版本 v{version},正在自动更新…',
83
83
  'update.updating': '正在更新 {name} 至 v{version},可能需要 1-2 分钟…',
84
+ 'update.cooldownNotice': '本次更新由你主动触发,将跳过 pnpm 的 24 小时发布冷却期(供应链保护)直接安装所选版本。',
84
85
  'update.updatingTitle': '正在自动更新…',
85
86
  'update.done': '更新完成',
86
87
  'update.doneDetail': '所有组件已更新到最新版本。',
@@ -188,6 +189,7 @@ export const en = {
188
189
  'update.found': 'A new version is available',
189
190
  'update.foundDetail': '{name} has a newer release v{version}; updating automatically…',
190
191
  'update.updating': 'Updating {name} to v{version}; this may take 1-2 minutes…',
192
+ 'update.cooldownNotice': 'This user-initiated update bypasses pnpm\'s 24-hour release cooldown (supply-chain protection) and installs the selected version immediately.',
191
193
  'update.updatingTitle': 'Updating automatically…',
192
194
  'update.done': 'Update complete',
193
195
  'update.doneDetail': 'All components are up to date.',
@@ -1 +1 @@
1
- {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAGlD;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASvF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAQvE;AAED,oGAAoG;AACpG,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAKlE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,cAAc,EACvB,oBAAoB,GAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAQ,EACtD,OAAO,GAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAQ,GACxC,CAAC,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAS9H;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAIhG"}
1
+ {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAGlD;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASvF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAQvE;AAED,oGAAoG;AACpG,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAKlE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,cAAc,EACvB,oBAAoB,GAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAQ,EACtD,OAAO,GAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAQ,GACxC,CAAC,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAW9H;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAIhG"}
package/lib/types/gate.js CHANGED
@@ -86,6 +86,8 @@ export function makeGateListener(service, requirePairingForLan = true, enabled =
86
86
  const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan;
87
87
  if (!require)
88
88
  return next();
89
+ // The paired-device cookie is a full host-API credential: it passes this
90
+ // gate for the whole ApiProxy surface, not just the /m/api allowlist.
89
91
  return isPairedDeviceRequest(service, request) ? next() : false;
90
92
  };
91
93
  }
@@ -5,6 +5,7 @@
5
5
  * rpc/mux layers).
6
6
  */
7
7
  import type { WorkspaceView } from '@deepseek-ai/dsh-host-apiproxy/api/workspace';
8
+ import type { AgentPresetEntry } from '@deepseek-ai/dsh-host-apiproxy/api/agent-presets';
8
9
  import type { SessionSummary, SessionModels, SessionProjectionsBlock } from '@deepseek-ai/dsh-host-apiproxy/api/sessions';
9
10
  /** One session.list page. */
10
11
  export interface SessionPage {
@@ -19,6 +20,12 @@ export interface CreatedSession {
19
20
  /** The composition the new session runs (echoed so the caller can label it). */
20
21
  agentPreset?: string;
21
22
  }
23
+ /** Agent presets available when composing a new session. */
24
+ export interface AgentPresetRoster {
25
+ presets: readonly AgentPresetEntry[];
26
+ authorable: boolean;
27
+ hasDocument: boolean;
28
+ }
22
29
  /** One history page (already bounded to whole messages by the host). */
23
30
  export interface HistoryPage {
24
31
  events: import('@deepseek-ai/dsh-host-apiproxy/api/sessions').HistoryEntry[];
@@ -40,6 +47,8 @@ export declare function listWorkspaces(): Promise<WorkspaceView[]>;
40
47
  export declare function fetchMobilePreferences(): Promise<MobilePreferences>;
41
48
  /** One session.list page; omit the cursor for the first page. */
42
49
  export declare function listSessions(cursor?: string): Promise<SessionPage>;
50
+ /** Read the available agent compositions for a new session. */
51
+ export declare function listAgentPresets(): Promise<AgentPresetRoster>;
43
52
  /**
44
53
  * Create a blank session (entity birth precedes the first message). Name a
45
54
  * workspace to attach it there, or a cwd; omitting both uses the host cwd.
@@ -47,6 +56,7 @@ export declare function listSessions(cursor?: string): Promise<SessionPage>;
47
56
  export declare function createSession(options?: {
48
57
  workspaceId?: string;
49
58
  cwd?: string;
59
+ agentPreset?: string;
50
60
  }): Promise<CreatedSession>;
51
61
  /** One history window; omit beforeSeq for the tail page, pass a signal to abort. */
52
62
  export declare function history(sessionId: string, beforeSeq?: number, maxMessages?: number, signal?: AbortSignal): Promise<HistoryPage>;
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../../src/mobile/api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8CAA8C,CAAA;AACjF,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,6CAA6C,CAAA;AAGzH,6BAA6B;AAC7B,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,cAAc,EAAE,CAAA;IACvB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wEAAwE;AACxE,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,OAAO,6CAA6C,EAAE,YAAY,EAAE,CAAA;IAC5E,OAAO,EAAE,OAAO,CAAA;IAChB;;;OAGG;IACH,WAAW,CAAC,EAAE,uBAAuB,CAAA;CACtC;AAED,4EAA4E;AAC5E,MAAM,WAAW,iBAAiB;IAChC,6EAA6E;IAC7E,iBAAiB,EAAE,OAAO,CAAA;CAC3B;AAED,kEAAkE;AAClE,wBAAsB,cAAc,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAG/D;AAED,yFAAyF;AACzF,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAEzE;AAED,iEAAiE;AACjE,wBAAsB,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAExE;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GACnD,OAAO,CAAC,cAAc,CAAC,CAEzB;AAED,oFAAoF;AACpF,wBAAsB,OAAO,CAC3B,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,EAClB,WAAW,SAAK,EAChB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,WAAW,CAAC,CAMtB;AAED,qEAAqE;AACrE,wBAAsB,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM3E;AAED,wEAAwE;AACxE,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAMnF;AAED,oFAAoF;AACpF,wBAAsB,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAEtE;AAED,2FAA2F;AAC3F,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,GACvE,OAAO,CAAC;IAAE,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC,CAOtF"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../../src/mobile/api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8CAA8C,CAAA;AACjF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kDAAkD,CAAA;AACxF,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,6CAA6C,CAAA;AAGzH,6BAA6B;AAC7B,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,cAAc,EAAE,CAAA;IACvB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,4DAA4D;AAC5D,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAA;IACpC,UAAU,EAAE,OAAO,CAAA;IACnB,WAAW,EAAE,OAAO,CAAA;CACrB;AAED,wEAAwE;AACxE,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,OAAO,6CAA6C,EAAE,YAAY,EAAE,CAAA;IAC5E,OAAO,EAAE,OAAO,CAAA;IAChB;;;OAGG;IACH,WAAW,CAAC,EAAE,uBAAuB,CAAA;CACtC;AAED,4EAA4E;AAC5E,MAAM,WAAW,iBAAiB;IAChC,6EAA6E;IAC7E,iBAAiB,EAAE,OAAO,CAAA;CAC3B;AAED,kEAAkE;AAClE,wBAAsB,cAAc,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAG/D;AAED,yFAAyF;AACzF,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAEzE;AAED,iEAAiE;AACjE,wBAAsB,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAExE;AAED,+DAA+D;AAC/D,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAEnE;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,GACzE,OAAO,CAAC,cAAc,CAAC,CAEzB;AAED,oFAAoF;AACpF,wBAAsB,OAAO,CAC3B,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,EAClB,WAAW,SAAK,EAChB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,WAAW,CAAC,CAMtB;AAED,qEAAqE;AACrE,wBAAsB,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM3E;AAED,wEAAwE;AACxE,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAMnF;AAED,oFAAoF;AACpF,wBAAsB,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAEtE;AAED,2FAA2F;AAC3F,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,GACvE,OAAO,CAAC;IAAE,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC,CAOtF"}
@@ -18,6 +18,10 @@ export async function fetchMobilePreferences() {
18
18
  export async function listSessions(cursor) {
19
19
  return await callUnary('session.list', cursor === undefined ? {} : { cursor });
20
20
  }
21
+ /** Read the available agent compositions for a new session. */
22
+ export async function listAgentPresets() {
23
+ return await callUnary('agentPreset.list', {});
24
+ }
21
25
  /**
22
26
  * Create a blank session (entity birth precedes the first message). Name a
23
27
  * workspace to attach it there, or a cwd; omitting both uses the host cwd.
@@ -114,4 +114,28 @@ export interface WireEvent {
114
114
  * @returns messages sorted by seq.
115
115
  */
116
116
  export declare function foldEvents(events: readonly WireEvent[], existing?: readonly RenderMessage[]): RenderMessage[];
117
+ /**
118
+ * Incremental folder for one message stream. Live chat folds one event at a
119
+ * time; rebuilding the five index maps by scanning every message per event
120
+ * made that path O(n) per event (O(n * events) per turn). A folder keeps the
121
+ * indexes alive across folds, applies each event in O(1) map operations, and
122
+ * returns the previous snapshot identity unchanged when nothing applied, so
123
+ * React skips the re-render entirely. Replayed events are no-ops: the maxSeq
124
+ * watermark advanced by the first application skips them, which also makes a
125
+ * double-invoked React state updater harmless.
126
+ */
127
+ export declare class EventFolder {
128
+ private state;
129
+ private snapshotList;
130
+ /** @param initial - seed rows (history tail load); omit for an empty stream. */
131
+ constructor(initial?: readonly RenderMessage[]);
132
+ /** Fold one batch incrementally; returns the current snapshot list. */
133
+ fold(events: readonly WireEvent[]): RenderMessage[];
134
+ /** Replace the whole stream (history reload / session switch). */
135
+ seed(messages: readonly RenderMessage[]): void;
136
+ /** Prepend an older history page (exact seam; no overlapping seqs). */
137
+ prepend(older: readonly RenderMessage[]): void;
138
+ /** Current snapshot list; a fresh copy whenever the folder changed. */
139
+ snapshot(): RenderMessage[];
140
+ }
117
141
  //# sourceMappingURL=messages.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../../src/mobile/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,MAAM,WAAW,aAAa;IAC5B,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;IACnC,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,EAAE,CAAA;IAC/B,8EAA8E;IAC9E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,kFAAkF;IAClF,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,sFAAsF;IACtF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,kDAAkD;IAClD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE;QACf,WAAW,EAAE,MAAM,CAAA;QACnB,YAAY,EAAE,MAAM,CAAA;QACpB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAA;IACD,sFAAsF;IACtF,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,gEAAgE;IAChE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,8BAA8B;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,qDAAqD;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,IAAI,CAAA;CAC1B;AA6eD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,EAAE,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE,GAAG,aAAa,EAAE,CAsB7G"}
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../../src/mobile/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,MAAM,WAAW,aAAa;IAC5B,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;IACnC,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,EAAE,CAAA;IAC/B,8EAA8E;IAC9E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,kFAAkF;IAClF,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,sFAAsF;IACtF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,kDAAkD;IAClD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE;QACf,WAAW,EAAE,MAAM,CAAA;QACnB,YAAY,EAAE,MAAM,CAAA;QACpB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAA;IACD,sFAAsF;IACtF,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,gEAAgE;IAChE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,8BAA8B;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,qDAAqD;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,IAAI,CAAA;CAC1B;AAweD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,EAAE,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE,GAAG,aAAa,EAAE,CAE7G;AAED;;;;;;;;;GASG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,YAAY,CAA6B;IAEjD,gFAAgF;IAChF,YAAY,OAAO,CAAC,EAAE,SAAS,aAAa,EAAE,EAE7C;IAED,uEAAuE;IACvE,IAAI,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,GAAG,aAAa,EAAE,CAWlD;IAED,kEAAkE;IAClE,IAAI,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI,CAG7C;IAED,uEAAuE;IACvE,OAAO,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI,CAG7C;IAED,uEAAuE;IACvE,QAAQ,IAAI,aAAa,EAAE,CAI1B;CACF"}
@@ -83,17 +83,6 @@ function chunkTarget(data) {
83
83
  result.step = step;
84
84
  return result;
85
85
  }
86
- /** Max message seq across an existing list, or -1 for an empty list. */
87
- function watermarkOf(existing) {
88
- if (existing === undefined)
89
- return -1;
90
- let maxSeq = -1;
91
- for (const message of existing) {
92
- if (message.seq > maxSeq)
93
- maxSeq = message.seq;
94
- }
95
- return maxSeq;
96
- }
97
86
  function createState(existing) {
98
87
  const messages = existing === undefined ? [] : [...existing];
99
88
  const state = {
@@ -103,8 +92,11 @@ function createState(existing) {
103
92
  turnStepMessage: new Map(),
104
93
  messageTurn: new Map(),
105
94
  toolNames: new Map(),
95
+ maxSeq: -1,
106
96
  };
107
97
  for (const message of messages) {
98
+ if (message.seq > state.maxSeq)
99
+ state.maxSeq = message.seq;
108
100
  state.byId.set(message.id, message);
109
101
  if (message.kind !== 'assistant')
110
102
  continue;
@@ -167,6 +159,8 @@ function retargetTurnStep(state, key, oldMessage, next) {
167
159
  }
168
160
  /** Fold one event into the working state. Assumes the event passes the watermark. */
169
161
  function applyEvent(state, event) {
162
+ if (event.seq > state.maxSeq)
163
+ state.maxSeq = event.seq;
170
164
  switch (event.type) {
171
165
  case 'user/message':
172
166
  applyUserMessage(state, event);
@@ -489,18 +483,61 @@ function applyTurnEnd(state, event) {
489
483
  * @returns messages sorted by seq.
490
484
  */
491
485
  export function foldEvents(events, existing) {
492
- const sorted = [...events].sort((a, b) => a.seq - b.seq);
493
- const watermark = watermarkOf(existing);
494
- const state = createState(existing);
495
- for (const event of sorted) {
496
- if (event.seq <= watermark)
497
- continue;
498
- applyEvent(state, event);
486
+ return new EventFolder(existing).fold(events);
487
+ }
488
+ /**
489
+ * Incremental folder for one message stream. Live chat folds one event at a
490
+ * time; rebuilding the five index maps by scanning every message per event
491
+ * made that path O(n) per event (O(n * events) per turn). A folder keeps the
492
+ * indexes alive across folds, applies each event in O(1) map operations, and
493
+ * returns the previous snapshot identity unchanged when nothing applied, so
494
+ * React skips the re-render entirely. Replayed events are no-ops: the maxSeq
495
+ * watermark advanced by the first application skips them, which also makes a
496
+ * double-invoked React state updater harmless.
497
+ */
498
+ export class EventFolder {
499
+ state;
500
+ snapshotList;
501
+ /** @param initial - seed rows (history tail load); omit for an empty stream. */
502
+ constructor(initial) {
503
+ this.state = createState(initial);
499
504
  }
500
- // Incremental tails (the live-stream hot path) are appended in seq order
501
- // or updated in place, so the list is usually ordered already — verify in
502
- // O(n) and skip the O(n log n) re-sort when it is.
503
- const out = state.messages;
505
+ /** Fold one batch incrementally; returns the current snapshot list. */
506
+ fold(events) {
507
+ const sorted = [...events].sort((a, b) => a.seq - b.seq);
508
+ let applied = false;
509
+ for (const event of sorted) {
510
+ if (event.seq <= this.state.maxSeq)
511
+ continue;
512
+ applyEvent(this.state, event);
513
+ applied = true;
514
+ }
515
+ if (!applied && this.snapshotList !== undefined)
516
+ return this.snapshotList;
517
+ this.snapshotList = snapshotOf(this.state);
518
+ return this.snapshotList;
519
+ }
520
+ /** Replace the whole stream (history reload / session switch). */
521
+ seed(messages) {
522
+ this.state = createState(messages);
523
+ this.snapshotList = undefined;
524
+ }
525
+ /** Prepend an older history page (exact seam; no overlapping seqs). */
526
+ prepend(older) {
527
+ this.state = createState([...older, ...this.state.messages]);
528
+ this.snapshotList = undefined;
529
+ }
530
+ /** Current snapshot list; a fresh copy whenever the folder changed. */
531
+ snapshot() {
532
+ if (this.snapshotList !== undefined)
533
+ return this.snapshotList;
534
+ this.snapshotList = snapshotOf(this.state);
535
+ return this.snapshotList;
536
+ }
537
+ }
538
+ /** Copy the folder's rows and keep them seq-ordered (skips re-sorting the common ordered case). */
539
+ function snapshotOf(state) {
540
+ const out = [...state.messages];
504
541
  let ordered = true;
505
542
  for (let index = 1; index < out.length; index += 1) {
506
543
  const prev = out[index - 1];
@@ -510,5 +547,5 @@ export function foldEvents(events, existing) {
510
547
  break;
511
548
  }
512
549
  }
513
- return ordered ? out : [...out].sort((a, b) => a.seq - b.seq || (a.id < b.id ? -1 : 1));
550
+ return ordered ? out : out.sort((a, b) => a.seq - b.seq || (a.id < b.id ? -1 : 1));
514
551
  }