@xmanrui/dsh-im 4.19.0 → 4.19.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "4.19.0",
3
+ "version": "4.19.1",
4
4
  "description": "把十一种 IM 渠道和公网 AI Office 接入本机 DeepSeek Harness。 Connect eleven IM channels and a public AI Office to a local DeepSeek Harness.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -72,10 +72,87 @@ function AliasDialog({ bot, onSave, onClose }) {
72
72
  return globalThis.document?.body ? createPortal(content, document.body) : content;
73
73
  }
74
74
 
75
+ function BotNameTooltip({ anchorRef, id, name, onDismiss }) {
76
+ const tooltipRef = React.useRef(null);
77
+ const [position, setPosition] = React.useState(null);
78
+ React.useLayoutEffect(() => {
79
+ const anchor = anchorRef.current;
80
+ const tooltip = tooltipRef.current;
81
+ if (!anchor || !tooltip) return undefined;
82
+ const document = anchor.ownerDocument;
83
+ const view = document.defaultView;
84
+ const place = () => {
85
+ const rect = anchor.getBoundingClientRect();
86
+ const { width, height } = tooltip.getBoundingClientRect();
87
+ const viewport = document.documentElement;
88
+ const margin = 8;
89
+ const below = rect.bottom + 6;
90
+ setPosition({
91
+ left: Math.max(margin, Math.min(rect.left, viewport.clientWidth - width - margin)),
92
+ top: Math.max(margin, Math.min(
93
+ below + height <= viewport.clientHeight - margin ? below : rect.top - height - 6,
94
+ viewport.clientHeight - height - margin,
95
+ )),
96
+ });
97
+ };
98
+ const dismiss = (event) => {
99
+ if (event.key === 'Escape') { event.stopPropagation(); onDismiss(); }
100
+ };
101
+ place();
102
+ view.addEventListener('resize', place);
103
+ // Capture nested scrolling containers as well as page scrolling.
104
+ document.addEventListener('scroll', onDismiss, true);
105
+ document.addEventListener('keydown', dismiss, true);
106
+ return () => {
107
+ view.removeEventListener('resize', place);
108
+ document.removeEventListener('scroll', onDismiss, true);
109
+ document.removeEventListener('keydown', dismiss, true);
110
+ };
111
+ }, [anchorRef, name, onDismiss]);
112
+ const body = anchorRef.current?.ownerDocument.body;
113
+ return body ? createPortal(h('span', {
114
+ ref: tooltipRef, id, role: 'tooltip', className: 'dim-botNameTooltip',
115
+ style: position ?? { visibility: 'hidden' },
116
+ }, name), body) : null;
117
+ }
118
+
75
119
  export function BotName({ bot, id, disabled = false, onSave }) {
76
120
  const [open, setOpen] = React.useState(false);
121
+ const nameRef = React.useRef(null);
122
+ const tooltipId = React.useId();
123
+ const [truncated, setTruncated] = React.useState(false);
124
+ const [hovered, setHovered] = React.useState(false);
125
+ const [focused, setFocused] = React.useState(false);
126
+ const [dismissed, setDismissed] = React.useState(false);
127
+ const dismissTooltip = React.useCallback(() => setDismissed(true), []);
128
+ const measure = React.useCallback(() => {
129
+ const node = nameRef.current;
130
+ setTruncated(Boolean(node && node.scrollWidth > node.clientWidth));
131
+ }, []);
132
+ React.useEffect(() => {
133
+ const node = nameRef.current;
134
+ if (!node) return undefined;
135
+ measure();
136
+ const view = node.ownerDocument.defaultView;
137
+ const observer = view.ResizeObserver ? new view.ResizeObserver(measure) : null;
138
+ observer?.observe(node);
139
+ view.addEventListener('resize', measure);
140
+ return () => { observer?.disconnect(); view.removeEventListener('resize', measure); };
141
+ }, [bot.name, measure]);
142
+ const showTooltip = truncated && !dismissed && !open && (hovered || focused);
77
143
  return h('div', { className: 'dim-aliasName' },
78
- h('h3', { id, title: bot.name }, bot.name),
144
+ h('h3', {
145
+ id, ref: nameRef, tabIndex: truncated ? 0 : undefined,
146
+ 'aria-describedby': showTooltip ? tooltipId : undefined,
147
+ onMouseEnter: () => { measure(); setHovered(true); setDismissed(false); },
148
+ onMouseLeave: () => setHovered(false),
149
+ onFocus: () => { measure(); setFocused(true); setDismissed(false); },
150
+ onBlur: () => setFocused(false),
151
+ onClick: dismissTooltip,
152
+ }, bot.name),
153
+ showTooltip ? h(BotNameTooltip, {
154
+ anchorRef: nameRef, id: tooltipId, name: bot.name, onDismiss: dismissTooltip,
155
+ }) : null,
79
156
  h('span', {
80
157
  className: 'dim-aliasEntry',
81
158
  onClick: (event) => event.stopPropagation(),
@@ -3,9 +3,15 @@ export const IM_STYLE_ID = 'xmanrui-dsh-im-settings';
3
3
  const CSS = String.raw`
4
4
  .dim-aliasName { display: flex; align-items: center; gap: 4px; min-width: 0; }
5
5
  .dim-aliasName h3 { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
6
+ .dim-aliasName h3:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #3370ff); outline-offset: 2px; border-radius: 3px; }
7
+ .dim-botNameTooltip { position: fixed; z-index: 1000; box-sizing: border-box; width: max-content; max-width: min(320px, calc(100vw - 16px)); padding: 6px 9px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 8px 24px rgb(31 35 41 / 14%); font: 500 12px/18px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; white-space: normal; overflow-wrap: anywhere; pointer-events: none; animation: dim-botNameTooltip-in .15s ease; }
8
+ @keyframes dim-botNameTooltip-in { from { opacity: 0; } to { opacity: 1; } }
9
+ @media (prefers-reduced-motion: reduce) { .dim-botNameTooltip { animation: none; } }
6
10
  .dim-aliasEntry { display: inline-flex; flex: none; }
7
- .dim-aliasEdit { display: grid; place-items: center; width: 28px; height: 28px; padding: 4px; border: 0; border-radius: 5px; color: var(--dsw-alias-label-secondary, #646a73); background: transparent; cursor: pointer; }
8
- .dim-aliasEdit:hover:not(:disabled) { color: var(--dsw-alias-state-business-primary, #3370ff); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
11
+ .dim-aliasEdit { display: grid; place-items: center; width: 28px; height: 28px; padding: 4px; border: 0; border-radius: 5px; color: var(--dsw-alias-label-tertiary, #8f959e); background: transparent; cursor: pointer; }
12
+ .dim-aliasEdit svg { opacity: .55; transition: opacity .15s ease; }
13
+ .dim-aliasName:hover .dim-aliasEdit:not(:disabled) svg, .dim-aliasEdit:focus-visible svg { opacity: 1; }
14
+ .dim-aliasEdit:hover:not(:disabled), .dim-aliasEdit:focus-visible { color: var(--dsw-alias-state-business-primary, #3370ff); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
9
15
  .dim-aliasDialog { box-sizing: border-box; width: min(380px, calc(100% - 32px)); max-height: calc(100dvh - 32px); overflow-y: auto; padding: 22px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 12px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 12px 36px rgb(0 0 0 / 18%); font: 13px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
10
16
  .dim-aliasDialog * { box-sizing: border-box; }
11
17
  .dim-aliasDialog::backdrop { background: rgb(15 17 21 / 30%); }
@@ -401,9 +407,9 @@ const CSS = String.raw`
401
407
  /* Header tooltips may extend beyond the card; the collapsible body clips its own content. */
402
408
  .dim-panel .dim-botCard { position: relative; min-width: 0; width: 100%; max-width: 100%; overflow: visible; border: 1px solid var(--dsw-alias-border-l2, #e5e6eb); border-radius: 14px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 3%); }
403
409
  .dim-panel .dim-botCard::before { display: none; }
404
- .dim-panel .dim-botCardBody { position: relative; min-width: 0; width: 100%; max-width: 100%; padding: 12px; }
410
+ .dim-panel .dim-botCardBody { position: relative; min-width: 0; width: 100%; max-width: 100%; padding: 12px 8px; }
405
411
  .dim-collapsibleAccount { min-width: 0; display: flex; flex-direction: column; }
406
- .dim-collapsibleHead { min-width: 0; display: flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; -webkit-user-select: none; }
412
+ .dim-collapsibleHead { min-width: 0; display: flex; align-items: center; gap: 4px; cursor: pointer; user-select: none; -webkit-user-select: none; }
407
413
  .dim-collapsibleHead:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #3370ff); outline-offset: 2px; border-radius: 8px; }
408
414
  .dim-collapsibleHeaderContent { min-width: 0; flex: 1 1 auto; display: flex; align-items: center; }
409
415
  .dim-collapsibleChevron { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 9px; height: 9px; border-right: 1.6px solid var(--dsw-alias-label-tertiary, #8f959e); border-bottom: 1.6px solid var(--dsw-alias-label-tertiary, #8f959e); transform: rotate(-45deg); transition: transform .22s cubic-bezier(.4, 0, .2, 1); transform-origin: 50% 50%; }
@@ -413,14 +419,17 @@ const CSS = String.raw`
413
419
  .dim-collapsibleAccount.is-open > .dim-collapsibleBody { grid-template-rows: 1fr; }
414
420
  .dim-collapsibleBodyInner { min-height: 0; overflow: hidden; }
415
421
  .dim-collapsibleAccount:not(.is-open) .dim-collapsibleBodyInner { visibility: hidden; }
416
- .dim-panel .dim-botCardTop { min-width: 0; max-width: 100%; display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
417
- .dim-panel .dim-botIdentity { min-width: 0; flex: 1 1 0; display: flex; align-items: center; gap: 10px; }
422
+ /* Reclaim horizontal spacing for names while keeping status on the same row,
423
+ including when a channel's mobile stylesheet requests a column layout. */
424
+ .dim-panel .dim-botCardTop { min-width: 0; width: 100%; max-width: 100%; display: flex; flex-direction: row; flex-wrap: nowrap; align-items: flex-start; justify-content: space-between; gap: 6px; }
425
+ .dim-panel .dim-botIdentity { min-width: 0; flex: 1 1 0; display: flex; align-items: center; gap: 6px; }
418
426
  .dim-panel .dim-botAvatar { flex: none; width: 38px; height: 38px; display: grid; place-items: center; overflow: hidden; border-radius: 11px; box-shadow: none; }
419
427
  .dim-panel .dim-botAvatar svg { width: 27px; height: 27px; }
420
- .dim-panel .dim-botName { min-width: 0; }
428
+ .dim-panel .dim-botName { min-width: 0; flex: 1; }
429
+ .dim-panel .dim-aliasName { gap: 2px; }
421
430
  .dim-panel .dim-botName h3 { overflow: hidden; margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 15px; font-weight: 650; line-height: normal; text-overflow: ellipsis; white-space: nowrap; }
422
431
  .dim-panel .dim-botName p { overflow: hidden; margin: 4px 0 0; color: var(--dsw-alias-label-secondary, #646a73); font: 12px ui-monospace, SFMono-Regular, monospace; line-height: normal; text-overflow: ellipsis; white-space: nowrap; }
423
- .dim-panel .dim-botCardTools { flex: none; display: flex; align-items: flex-start; gap: 8px; }
432
+ .dim-panel .dim-botCardTools { flex: none; display: flex; align-items: flex-start; gap: 4px; }
424
433
  .dim-panel .dim-botHealthGroup { min-width: 0; max-width: 100%; flex: none; display: grid; justify-items: end; gap: 5px; }
425
434
  .dim-panel .dim-botCard .dim-botHealth { flex: none; min-height: 0; display: inline-flex; align-items: center; gap: 7px; padding: 0; border: 0; border-radius: 0; color: var(--dsw-alias-label-secondary, #646a73); background: transparent; font: inherit; font-size: 12px; font-weight: 400; line-height: normal; white-space: nowrap; }
426
435
  .dim-panel .dim-lastChecked { display: inline-flex; align-items: baseline; gap: 4px; color: var(--dsw-alias-label-tertiary, #8f959e); font: inherit; font-size: 11px; font-weight: 400; line-height: normal; white-space: nowrap; }
@@ -3,10 +3,7 @@ import {
3
3
  consumeDshImInputOrigin,
4
4
  textFromHarnessContent,
5
5
  } from '../../src/channels/shared/harness-client.mjs';
6
- import {
7
- isSessionSyncMirrored,
8
- releaseSessionSyncMirror,
9
- } from '../../src/channels/shared/session-sync-registry.mjs';
6
+ import { deliverSessionSyncMirror } from '../../src/channels/shared/session-sync-registry.mjs';
10
7
 
11
8
  const DSH_USER_PREFIX = '[来自 DSH]\n';
12
9
  const DSH_ASSISTANT_PREFIX = '[DSH 助手]\n';
@@ -159,15 +156,19 @@ export function createSessionSyncCoordinator({ deliveryService, logger = console
159
156
  turns.delete(sessionId);
160
157
  if (state.origin !== 'dsh' || !completedTurn(event.data?.reason)
161
158
  || !state.recipients?.size || !state.assistant.text) return;
162
- // Per-target suppression: targets whose process-card mirror delivered
163
- // the answer are skipped; every other synced target still gets the text.
164
- const mirrored = [];
165
- const plain = [];
166
- for (const target of state.recipients.values()) {
167
- if (isSessionSyncMirrored(sessionId, target.targetId, state.turn)) mirrored.push(target);
168
- else plain.push(target);
169
- }
170
- for (const target of mirrored) releaseSessionSyncMirror(sessionId, target.targetId);
159
+ // A pending card is not proof of delivery. Only suppress this target's
160
+ // text after its renderer confirms the complete final answer is visible.
161
+ // Targets without a renderer retain the original text delivery path.
162
+ const recipients = [...state.recipients.values()];
163
+ const rendered = await Promise.all(recipients.map(async (target) => {
164
+ try {
165
+ return await deliverSessionSyncMirror(target, sessionId, state.turn, state.assistant.text);
166
+ } catch (error) {
167
+ logFailure('card delivery', target, error);
168
+ return false;
169
+ }
170
+ }));
171
+ const plain = recipients.filter((_target, index) => !rendered[index]);
171
172
  if (plain.length === 0) return;
172
173
  await deliver(
173
174
  sessionId,