dsh-composer-plus 1.23.0 → 1.24.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.
@@ -51,7 +51,7 @@ export const PLUS = new Map([
51
51
  ['queue-move', ['POST']], ['queue-edit', ['POST']], ['branches', ['GET']], ['predict', ['POST']], ['complete', ['POST']], ['improve', ['POST']], ['unread', ['GET', 'POST']],
52
52
  ['worktrees', ['GET', 'POST']], ['new-session-workspace', ['POST']], ['memory-edit', ['GET', 'POST']], ['memory-history', ['GET', 'POST']], ['memory-export', ['POST']], ['memory-copy', ['GET', 'POST']], ['memory-stale', ['GET']], ['memory-chat', ['GET', 'POST']], ['memory-suggest', ['POST']], ['handoff', ['POST']], ['context', ['GET']],
53
53
  ['board-chats', ['GET', 'POST']], ['board-progress', ['GET']], ['secret-answer', ['GET', 'POST']], ['side-chat', ['GET', 'POST']],
54
- ['issue-report', ['POST']],
54
+ ['issue-report', ['POST']], ['assistant', ['GET']],
55
55
  ])
56
56
  /** Allow for this chat: an Auto Review ask (`approval/request` with this reason) for a tool that the chat granted. */
57
57
  export const AUTO_REVIEW_ASK = 'Auto review denied tool '
@@ -668,6 +668,8 @@ export function createGateway(opts = {}) {
668
668
  if (devices.length !== before) saveDevices()
669
669
  for (const s of devSockets.get(id) ?? []) s.destroy()
670
670
  devSockets.delete(id)
671
+ // No paired phone is left to answer the questions that wait for one: they go back to dsh's desktop.
672
+ if (devices.length === 0) for (const e of [...pendingEv.keys()]) dropPending(e, { kind: 'next' })
671
673
  return devices.length !== before
672
674
  }
673
675
  function notify(msg) {
@@ -755,11 +757,13 @@ export function createGateway(opts = {}) {
755
757
  if (v.type === 'cancel' && dropPending(v.eventId)) return toPhones({ type: 'cancel', eventId: v.eventId })
756
758
  if (v.type !== 'waterfall') return
757
759
  if (v.event === 'approval/request') return grantedOrAsk(v)
758
- if (!EVENTS.has(v.event) || !holds()) return answer(v.eventId, { kind: 'next' }).catch(() => {})
760
+ if (!EVENTS.has(v.event) || devices.length === 0) return answer(v.eventId, { kind: 'next' }).catch(() => {})
759
761
  askPhones(v)
760
762
  }
761
- /** The gateway keeps an ask for the phones while one is connected, or while a phone gets pushes: the push opens Tether,
762
- * and Tether gets the ask when it connects. dsh's desktop can answer it at the same time. */
763
+ /** The gateway keeps an approval for the phones while one is connected, or while a phone gets pushes: the push opens
764
+ * Tether, and Tether gets the ask when it connects. dsh's desktop can answer it at the same time. A question
765
+ * (`ask_user_question`) waits for a paired phone also while Tether is closed: dsh's "next" would leave it on the desktop
766
+ * only, and the phone that opens later could not answer it. The question still shows on the desktop meanwhile. */
763
767
  const holds = () => evSubs.size > 0 || pushTo().length > 0
764
768
  /** Allow for this chat, also with no dsh window open: the gateway allows a granted tool itself (the grants that the
765
769
  * desktop and the phone save in dsh-composer-plus). Any other approval goes to the phones. */
@@ -776,8 +780,10 @@ export function createGateway(opts = {}) {
776
780
  const frame = { type: 'waterfall', event: v.event, eventId: v.eventId, agentId: v.agentId, request: v.request }
777
781
  if (v.event === 'approval/request') pushAll('Approval needed', v.request?.toolName, v.agentId)
778
782
  else pushAll('DSH has a question', v.request?.questions?.[0]?.question, v.agentId)
779
- // shortcut: unanswered phone prompts fall back to 'next' (DSH desktop still decides) after a fixed timeout; make it configurable per event if users want longer.
780
- const timer = setTimeout(() => { if (dropPending(v.eventId, { kind: 'next' })) toPhones({ type: 'cancel', eventId: v.eventId }) }, waterfallTtl)
783
+ // shortcut: an unanswered approval falls back to 'next' (DSH desktop still decides) after a fixed timeout; make it configurable if users want longer.
784
+ // A question has no timeout: dsh waits for its answer, and the phone that opens hours later must still get it.
785
+ const timer = v.event === 'approval/request'
786
+ ? setTimeout(() => { if (dropPending(v.eventId, { kind: 'next' })) toPhones({ type: 'cancel', eventId: v.eventId }) }, waterfallTtl) : undefined
781
787
  pendingEv.set(v.eventId, { frame, timer })
782
788
  toPhones(frame)
783
789
  }
@@ -911,7 +917,11 @@ export function createGateway(opts = {}) {
911
917
  // A failed connect (DSH restarting) is forgotten so the next stream open retries instead of failing forever.
912
918
  const getUp = () => upP ??= upstream.mux().then(u => { up = u; u.onText = t => ws.send(t); u.onClose = () => ws.close(1011); return u }, e => { upP = null; throw e })
913
919
  const err = (streamId, message) => ws.send(JSON.stringify({ type: 'error', streamId, error: { code: 'mobile/forbidden', message } }))
914
- const unsub = id => { const s = subs.get(id); if (!s) return; subs.delete(id); evSubs.delete(s); if (!holds()) for (const e of [...pendingEv.keys()]) dropPending(e, { kind: 'next' }) }
920
+ // The last phone left: its approvals go back to dsh's desktop; its questions keep waiting for the phone (askPhones).
921
+ const unsub = id => {
922
+ const s = subs.get(id); if (!s) return; subs.delete(id); evSubs.delete(s)
923
+ if (!holds()) for (const [e, p] of [...pendingEv]) if (p.frame.event === 'approval/request') dropPending(e, { kind: 'next' })
924
+ }
915
925
  ws.onText = async text => {
916
926
  let f
917
927
  try { f = JSON.parse(text) } catch { return ws.close(1007) }