dsh-composer-plus 1.22.0 → 1.24.0

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": "dsh-composer-plus",
3
- "version": "1.22.0",
3
+ "version": "1.24.0",
4
4
  "description": "DeepSeek Harness helpers: 11 extra UI languages (Türkçe, Español, Français, Deutsch, 日本語…), Changes panel, swipe-card feature ideas, sidebar search in message text, turn rollback with git file restore, delete chats to the Trash, input history, Shift+Tab /plan, one-click Continue, resume running chats after a restart, queue drag-and-drop, deeplinks, Auto Review allow-for-chat, notifications, iPhone access with the Tether app, automatic updates",
5
5
  "icon": "./assets/icon.svg",
6
6
  "license": "MIT",
@@ -76,10 +76,11 @@
76
76
  "/README.md",
77
77
  "LICENSE",
78
78
  "locales/LICENSE",
79
- "assets/features/*.webp"
79
+ "assets/features/*.webp",
80
+ "assets/skills/"
80
81
  ],
81
82
  "scripts": {
82
- "test": "node scripts/build-locales.mjs --check && node scripts/build-docs.mjs --check && node test/locale.test.mjs && node test/continue.test.mjs && node test/continue-host.test.mjs && node test/resume-restart.test.mjs && node test/content-search.test.mjs && node test/turn-rollback.test.mjs && node test/turn-rollback-client.test.mjs && node test/delete-chat.test.mjs && node test/export-chat.test.mjs && node test/project-name.test.mjs && node test/queue-drag.test.mjs && node test/queue-move.test.mjs && node test/queue-edit.test.mjs && node test/send-first.test.mjs && node test/message-link.test.mjs && node test/mark-read.test.mjs && node test/plan-toggle.test.mjs && node test/keep-focus.test.mjs && node test/question-fit.test.mjs && node test/header-fit.test.mjs && node test/mobile-connect.test.mjs && node test/unread.test.mjs && node test/unread-client.test.mjs && node test/tether-gateway.test.mjs && node test/tether-parity.test.mjs && node test/tether-notify-per-mac.test.mjs && node test/tether-push-words.test.mjs && node test/deeplink.test.mjs && node test/chat-grant.test.mjs && node test/reject-note.test.mjs && node test/grant-ttl.test.mjs && node test/notify.test.mjs && node test/idle-switch.test.mjs && node test/next-waiting.test.mjs && node test/option-picks.test.mjs && node test/option-picks-ui.test.mjs && node test/changes.test.mjs && node test/chat-changes.test.mjs && node test/changes-tree.test.mjs && node test/file-chats.test.mjs && node test/file-chats-client.test.mjs && node test/feature-ideas.test.mjs && node test/feature-ideas-client.test.mjs && node test/github-issue.test.mjs && node test/project-status.test.mjs && node test/session-branch.test.mjs && node test/recent-projects.test.mjs && node test/project-tools.test.mjs && node test/has-selectors.test.mjs && node test/project-picker.test.mjs && node test/worktree-chats.test.mjs && node test/worktree-chats-client.test.mjs && node test/branch-tree.test.mjs && node test/branch-tree-client.test.mjs && node test/default-workspace.test.mjs && node test/default-workspace-client.test.mjs && node test/find-in-chat.test.mjs && node test/predict.test.mjs && node test/predict-client.test.mjs && node test/complete.test.mjs && node test/complete-client.test.mjs && node test/prompt-improve.test.mjs && node test/prompt-improve-client.test.mjs && node test/goal-check.test.mjs && node test/goal-check-client.test.mjs && node test/backup.test.mjs && node test/backup-host.test.mjs && node test/backup-client.test.mjs && node test/project-memory.test.mjs && node test/memory-editor.test.mjs && node test/memory-base.test.mjs && node test/memory-history.test.mjs && node test/memory-export.test.mjs && node test/memory-source.test.mjs && node test/memory-copy.test.mjs && node test/memory-stale.test.mjs && node test/memory-per-chat.test.mjs && node test/memory-suggest.test.mjs && node test/status-order.test.mjs && node test/toggles.test.mjs && node test/update.test.mjs && node test/whats-new-current.test.mjs && node test/release-notes.test.mjs && node test/active-count.test.mjs && node test/meta.test.mjs && node test/shots.test.mjs && node test/readme.test.mjs && node test/readme-labels.test.mjs && node test/feature-docs.test.mjs && node test/routes.test.mjs && node test/protected-files.test.mjs && node test/blocked-commands.test.mjs && node test/blocked-commands-client.test.mjs && node test/handoff.test.mjs && node test/context-breakdown.test.mjs && node test/task-board.test.mjs && node test/task-board-client.test.mjs && node test/planned-steps.test.mjs && node --test --test-timeout=120000 test/computers.test.mjs test/computers-host.test.mjs && node test/computers-client.test.mjs && node test/secret-input.test.mjs && node test/secret-input-client.test.mjs && node test/quote-reply.test.mjs && node test/side-chat.test.mjs && node test/diff-comments.test.mjs",
83
+ "test": "node scripts/build-locales.mjs --check && node scripts/build-docs.mjs --check && node test/locale.test.mjs && node test/continue.test.mjs && node test/continue-host.test.mjs && node test/resume-restart.test.mjs && node test/content-search.test.mjs && node test/turn-rollback.test.mjs && node test/turn-rollback-client.test.mjs && node test/delete-chat.test.mjs && node test/export-chat.test.mjs && node test/project-name.test.mjs && node test/queue-drag.test.mjs && node test/queue-move.test.mjs && node test/queue-edit.test.mjs && node test/send-first.test.mjs && node test/message-link.test.mjs && node test/mark-read.test.mjs && node test/plan-toggle.test.mjs && node test/keep-focus.test.mjs && node test/question-fit.test.mjs && node test/header-fit.test.mjs && node test/window-size.test.mjs && node test/mobile-connect.test.mjs && node test/unread.test.mjs && node test/unread-client.test.mjs && node test/tether-gateway.test.mjs && node test/tether-parity.test.mjs && node test/tether-notify-per-mac.test.mjs && node test/tether-push-words.test.mjs && node test/deeplink.test.mjs && node test/chat-grant.test.mjs && node test/reject-note.test.mjs && node test/grant-ttl.test.mjs && node test/notify.test.mjs && node test/idle-switch.test.mjs && node test/next-waiting.test.mjs && node test/next-waiting-locate.test.mjs && node test/option-picks.test.mjs && node test/option-picks-ui.test.mjs && node test/changes.test.mjs && node test/chat-changes.test.mjs && node test/changes-tree.test.mjs && node test/file-chats.test.mjs && node test/file-chats-client.test.mjs && node test/feature-ideas.test.mjs && node test/feature-ideas-client.test.mjs && node test/github-issue.test.mjs && node test/issue-report.test.mjs && node test/issue-report-client.test.mjs && node test/project-status.test.mjs && node test/session-branch.test.mjs && node test/recent-projects.test.mjs && node test/project-tools.test.mjs && node test/has-selectors.test.mjs && node test/project-picker.test.mjs && node test/worktree-chats.test.mjs && node test/worktree-chats-client.test.mjs && node test/branch-tree.test.mjs && node test/branch-tree-client.test.mjs && node test/default-workspace.test.mjs && node test/default-workspace-client.test.mjs && node test/find-in-chat.test.mjs && node test/predict.test.mjs && node test/predict-client.test.mjs && node test/complete.test.mjs && node test/complete-client.test.mjs && node test/prompt-improve.test.mjs && node test/prompt-improve-client.test.mjs && node test/goal-check.test.mjs && node test/goal-check-client.test.mjs && node test/backup.test.mjs && node test/backup-host.test.mjs && node test/backup-client.test.mjs && node test/project-memory.test.mjs && node test/memory-editor.test.mjs && node test/memory-base.test.mjs && node test/memory-history.test.mjs && node test/memory-export.test.mjs && node test/memory-source.test.mjs && node test/memory-copy.test.mjs && node test/memory-stale.test.mjs && node test/memory-per-chat.test.mjs && node test/memory-suggest.test.mjs && node test/status-order.test.mjs && node test/toggles.test.mjs && node test/feature-filter.test.mjs && node test/update.test.mjs && node test/whats-new-current.test.mjs && node test/whats-new-star.test.mjs && node test/release-notes.test.mjs && node test/active-count.test.mjs && node test/meta.test.mjs && node test/shots.test.mjs && node test/readme.test.mjs && node test/readme-labels.test.mjs && node test/feature-docs.test.mjs && node test/routes.test.mjs && node test/protected-files.test.mjs && node test/blocked-commands.test.mjs && node test/blocked-commands-client.test.mjs && node test/handoff.test.mjs && node test/context-breakdown.test.mjs && node test/task-board.test.mjs && node test/task-board-client.test.mjs && node test/assistant.test.mjs && node test/assistant-client.test.mjs && node test/planned-steps.test.mjs && node test/untangle-codebase.test.mjs && node test/turkish-skill.test.mjs && node --test --test-timeout=120000 test/computers.test.mjs test/computers-host.test.mjs && node test/computers-client.test.mjs && node test/secret-input.test.mjs && node test/secret-input-client.test.mjs && node test/quote-reply.test.mjs && node test/preview-quote.test.mjs && node test/side-chat.test.mjs && node test/diff-comments.test.mjs",
83
84
  "build:locales": "node scripts/build-locales.mjs",
84
85
  "build:readme": "node scripts/build-docs.mjs",
85
86
  "prepublishOnly": "npm test"
@@ -1,18 +1,18 @@
1
1
  {
2
- "version": "1.22.0",
2
+ "version": "1.24.0",
3
3
  "notes": {
4
- "en": "## New\n\n- **Mobile connect (iPhone)**: Tether notifications can come also while Tether is closed. Put an APNs key in `apns.json` on the Mac (see `tether/README.md`). The Mac then sends the approval, question, error and Turn finished notifications through Apple’s push service. The Mac must be awake with dsh open. Each notification shows the Mac and the chat’s project (`Studio Mac · recipe-box`) and the chat’s title. Turn finished shows the agent’s last reply. This needs a Tether build from this version.\n\n## Fixes\n\n- **Secret input**: the card of a secret request no longer stays inside a closed step group. In the desktop view, the card opens its group, so you see it at once.\n- In a narrow chat, the approval card with **Allow for this chat** and **Reject with note…** puts its buttons on more lines. Each label stays on one line in its button.",
5
- "zh": "## 新功能\n\n- **手机连接(iPhone)**:Tether 关闭时也能收到 Tether 通知。在 Mac 上把 APNs 密钥放入 `apns.json`(见 `tether/README.md`)。之后 Mac 通过 Apple 推送服务发送审批、提问、错误和 Turn finished 通知。Mac 必须处于唤醒状态并打开 dsh。每条通知显示 Mac 和对话的项目(`Studio Mac · recipe-box`)以及对话标题。Turn finished 显示代理的最后一条回复。这需要此版本的 Tether 构建。\n\n## 修复\n\n- **密钥输入**:密钥请求的卡片不再停留在折叠的步骤组中。在桌面视图中,卡片会展开它所在的组,让你立即看到它。\n- 在较窄的对话中,带有 **本对话内允许** 和 **拒绝并附言…** 的审批卡片会把按钮分成多行。每个按钮的文字保持在一行内。",
6
- "tr": "## Yeni\n\n- **Mobil bağlantı (iPhone)**: Tether bildirimleri Tether kapalıyken de gelebilir. Mac’te `apns.json` dosyasına bir APNs anahtarı koyun (bkz. `tether/README.md`). Mac bundan sonra onay, soru, hata ve Turn finished bildirimlerini Apple’ın push hizmetiyle gönderir. Mac uyanık ve dsh açık olmalıdır. Her bildirim Mac’i ve sohbetin projesini (`Studio Mac · recipe-box`) ve sohbetin başlığını gösterir. Turn finished, ajanın son yanıtını gösterir. Bunun için bu sürümden bir Tether derlemesi gerekir.\n\n## Düzeltmeler\n\n- **Gizli değer girişi**: gizli değer isteğinin kartı artık kapalı bir adım grubunun içinde kalmaz. Masaüstü görünümünde kart kendi grubunu açar; böylece kartı hemen görürsünüz.\n- Dar bir sohbette, **Bu sohbet boyunca izin ver** ve **Not ile reddet…** düğmelerinin olduğu onay kartı düğmelerini birden çok satıra dizer. Her düğmenin yazısı tek satırda kalır.",
7
- "es": "## Novedades\n\n- **Conexión móvil (iPhone)**: las notificaciones de Tether pueden llegar también con Tether cerrado. Pon una clave de APNs en `apns.json` en el Mac (consulta `tether/README.md`). Entonces el Mac envía las notificaciones de aprobación, pregunta, error y Turn finished mediante el servicio de notificaciones push de Apple. El Mac debe estar activo y con dsh abierto. Cada notificación muestra el Mac y el proyecto del chat (`Studio Mac · recipe-box`) y el título del chat. Turn finished muestra la última respuesta del agente. Hace falta una compilación de Tether de esta versión.\n\n## Correcciones\n\n- **Entrada secreta**: la tarjeta de una solicitud de secreto ya no se queda dentro de un grupo de pasos cerrado. En la vista de escritorio, la tarjeta abre su grupo, así que la ves enseguida.\n- En un chat estrecho, la tarjeta de aprobación con **Permitir en este chat** y **Rechazar con nota…** reparte sus botones en varias líneas. Cada texto queda en una sola línea dentro de su botón.",
8
- "pt-BR": "## Novidades\n\n- **Conexão móvel (iPhone)**: as notificações do Tether podem chegar também com o Tether fechado. Coloque uma chave de APNs em `apns.json` no Mac (veja `tether/README.md`). Então o Mac envia as notificações de aprovação, pergunta, erro e Turn finished pelo serviço de notificações push da Apple. O Mac precisa estar ativo e com o dsh aberto. Cada notificação mostra o Mac e o projeto do chat (`Studio Mac · recipe-box`) e o título do chat. Turn finished mostra a última resposta do agente. É preciso uma build do Tether desta versão.\n\n## Correções\n\n- **Entrada secreta**: o cartão de um pedido de segredo não fica mais dentro de um grupo de etapas fechado. Na visualização do desktop, o cartão abre o grupo dele, então você o vê na hora.\n- Em um chat estreito, o cartão de aprovação com **Permitir neste chat** e **Rejeitar com nota…** distribui os botões em mais linhas. Cada texto fica em uma linha dentro do botão.",
9
- "fr": "## Nouveautés\n\n- **Connexion mobile (iPhone)** : les notifications de Tether peuvent arriver aussi quand Tether est fermé. Placez une clé APNs dans `apns.json` sur le Mac (voir `tether/README.md`). Le Mac envoie alors les notifications d’approbation, de question, d’erreur et Turn finished par le service de notifications push d’Apple. Le Mac doit être éveillé, avec dsh ouvert. Chaque notification indique le Mac et le projet de la discussion (`Studio Mac · recipe-box`) ainsi que le titre de la discussion. Turn finished affiche la dernière réponse de l’agent. Il faut une version de Tether compilée à partir de cette version.\n\n## Corrections\n\n- **Saisie secrète** : la carte d’une demande de secret ne reste plus dans un groupe d’étapes fermé. Dans la vue bureau, la carte ouvre son groupe, vous la voyez donc tout de suite.\n- Dans une discussion étroite, la carte d’approbation avec **Autoriser pour cette discussion** et **Refuser avec une note…** répartit ses boutons sur plusieurs lignes. Chaque libellé reste sur une ligne dans son bouton.",
10
- "de": "## Neu\n\n- **Mobile Verbindung (iPhone)**: Tether-Benachrichtigungen kommen jetzt auch, wenn Tether geschlossen ist. Lege auf dem Mac einen APNs-Schlüssel in `apns.json` ab (siehe `tether/README.md`). Der Mac sendet dann die Benachrichtigungen zu Freigabe, Frage, Fehler und Turn finished über Apples Push-Dienst. Der Mac muss wach sein und dsh offen haben. Jede Benachrichtigung zeigt den Mac und das Projekt des Chats (`Studio Mac · recipe-box`) sowie den Titel des Chats. Turn finished zeigt die letzte Antwort des Agenten. Dafür ist ein Tether-Build aus dieser Version nötig.\n\n## Korrekturen\n\n- **Geheime Eingabe**: Die Karte einer Geheimnis-Anfrage bleibt nicht mehr in einer geschlossenen Schrittgruppe. In der Desktop-Ansicht öffnet die Karte ihre Gruppe, sodass du sie sofort siehst.\n- In einem schmalen Chat verteilt die Freigabekarte mit **Für diesen Chat zulassen** und **Mit Notiz ablehnen…** ihre Schaltflächen auf mehrere Zeilen. Jede Beschriftung bleibt einzeilig in ihrer Schaltfläche.",
11
- "ru": "## Новое\n\n- **Мобильное подключение (iPhone)**: уведомления Tether приходят и тогда, когда Tether закрыт. Положите ключ APNs в `apns.json` на Mac (см. `tether/README.md`). После этого Mac отправляет уведомления об одобрении, вопросе, ошибке и Turn finished через push-сервис Apple. Mac должен быть активен, а dsh открыт. Каждое уведомление показывает Mac и проект чата (`Studio Mac · recipe-box`), а также название чата. Turn finished показывает последний ответ агента. Для этого нужна сборка Tether из этой версии.\n\n## Исправления\n\n- **Ввод секрета**: карточка запроса секрета больше не остаётся внутри свёрнутой группы шагов. В режиме настольного приложения карточка раскрывает свою группу, и вы сразу её видите.\n- В узком чате карточка одобрения с кнопками **Разрешить для этого чата** и **Отклонить с примечанием…** располагает кнопки в несколько строк. Надпись каждой кнопки остаётся в одной строке внутри кнопки.",
12
- "ja": "## 新機能\n\n- **モバイル接続(iPhone)**:Tether を閉じていても Tether の通知が届くようになりました。Mac の `apns.json` に APNs キーを置いてください(`tether/README.md` を参照)。すると Mac は、承認、質問、エラー、Turn finished の通知を Apple のプッシュサービスで送ります。Mac はスリープしておらず、dsh が開いている必要があります。各通知には Mac とチャットのプロジェクト(`Studio Mac · recipe-box`)、チャットのタイトルが表示されます。Turn finished にはエージェントの最後の返信が表示されます。このバージョンからビルドした Tether が必要です。\n\n## 修正\n\n- **シークレット入力**:シークレット要求のカードが、閉じたステップグループの中に隠れたままになることはなくなりました。デスクトップ表示では、カードが自分のグループを開くので、すぐに見えます。\n- 狭いチャットでは、**このチャットで許可** と **メモを付けて拒否…** がある承認カードのボタンが複数行に並びます。各ボタンの文字はボタンの中で 1 行に収まります。",
13
- "ko": "## 새 기능\n\n- **모바일 연결(iPhone)**: Tether를 닫아 두어도 Tether 알림을 받을 수 있습니다. Mac의 `apns.json`에 APNs 키를 넣으세요(`tether/README.md` 참고). 그러면 Mac이 승인, 질문, 오류, Turn finished 알림을 Apple 푸시 서비스로 보냅니다. Mac은 깨어 있고 dsh가 열려 있어야 합니다. 각 알림에는 Mac과 채팅의 프로젝트(`Studio Mac · recipe-box`), 채팅 제목이 표시됩니다. Turn finished에는 에이전트의 마지막 답변이 표시됩니다. 이 버전으로 빌드한 Tether가 필요합니다.\n\n## 수정 사항\n\n- **비밀 값 입력**: 비밀 값 요청 카드가 더 이상 닫힌 단계 그룹 안에 남아 있지 않습니다. 데스크톱 보기에서 카드가 자기 그룹을 열어 바로 보입니다.\n- 좁은 채팅에서 **이 채팅에서 허용**과 **메모와 함께 거부…**가 있는 승인 카드는 버튼을 여러 줄에 배치합니다. 각 버튼의 글자는 버튼 안에서 한 줄로 유지됩니다.",
14
- "id": "## Baru\n\n- **Koneksi seluler (iPhone)**: notifikasi Tether kini bisa datang juga saat Tether tertutup. Taruh kunci APNs di `apns.json` pada Mac (lihat `tether/README.md`). Lalu Mac mengirim notifikasi persetujuan, pertanyaan, galat, dan Turn finished lewat layanan push Apple. Mac harus menyala (tidak tidur) dengan dsh terbuka. Setiap notifikasi menampilkan Mac dan proyek obrolan (`Studio Mac · recipe-box`) serta judul obrolan. Turn finished menampilkan balasan terakhir agen. Ini memerlukan build Tether dari versi ini.\n\n## Perbaikan\n\n- **Input rahasia**: kartu permintaan rahasia tidak lagi tetap berada di dalam grup langkah yang tertutup. Di tampilan desktop, kartu membuka grupnya sendiri sehingga Anda langsung melihatnya.\n- Di obrolan yang sempit, kartu persetujuan dengan **Izinkan untuk obrolan ini** dan **Tolak dengan catatan…** menyusun tombolnya dalam beberapa baris. Teks setiap tombol tetap satu baris di dalam tombolnya.",
15
- "vi": "## Mới\n\n- **Kết nối di động (iPhone)**: thông báo của Tether giờ có thể đến cả khi Tether đã đóng. Đặt một khóa APNs vào `apns.json` trên Mac (xem `tether/README.md`). Khi đó Mac gửi các thông báo phê duyệt, câu hỏi, lỗi và Turn finished qua dịch vụ push của Apple. Mac phải đang thức và dsh phải đang mở. Mỗi thông báo hiển thị Mac và dự án của cuộc trò chuyện (`Studio Mac · recipe-box`) cùng tiêu đề cuộc trò chuyện. Turn finished hiển thị câu trả lời cuối cùng của agent. Cần một bản build Tether từ phiên bản này.\n\n## Sửa lỗi\n\n- **Nhập giá trị bí mật**: thẻ của một yêu cầu bí mật không còn nằm trong một nhóm bước đã thu gọn. Ở chế độ xem desktop, thẻ tự mở nhóm của nó nên bạn thấy thẻ ngay.\n- Trong một cuộc trò chuyện hẹp, thẻ phê duyệt có **Cho phép trong cuộc trò chuyện này** và **Từ chối kèm ghi chú…** xếp các nút thành nhiều dòng. Chữ của mỗi nút nằm trên một dòng trong nút của nó.",
16
- "hi": "## नया\n\n- **मोबाइल कनेक्ट (iPhone)**: Tether बंद होने पर भी अब Tether सूचनाएँ आ सकती हैं। Mac पर `apns.json` में एक APNs कुंजी रखें (`tether/README.md` देखें)। इसके बाद Mac अनुमति, प्रश्न, त्रुटि और Turn finished सूचनाएँ Apple की पुश सेवा से भेजता है। Mac जागा हुआ होना चाहिए और dsh खुला होना चाहिए। हर सूचना Mac और चैट का प्रोजेक्ट (`Studio Mac · recipe-box`) और चैट का शीर्षक दिखाती है। Turn finished एजेंट का आखिरी जवाब दिखाता है। इसके लिए इस वर्शन से बना Tether बिल्ड चाहिए।\n\n## सुधार\n\n- **सीक्रेट इनपुट**: सीक्रेट अनुरोध का कार्ड अब किसी बंद स्टेप समूह के अंदर नहीं रहता। डेस्कटॉप व्यू में कार्ड अपना समूह खोल देता है, इसलिए आप उसे तुरंत देखते हैं।\n- संकरी चैट में, **इस चैट के लिए अनुमति दें** और **नोट के साथ अस्वीकार करें…** वाला अनुमति कार्ड अपने बटन कई पंक्तियों में रखता है। हर बटन का लेबल बटन के अंदर एक ही पंक्ति में रहता है।"
4
+ "en": "## New\n\n- **Assistant** (off by default): switch it on in Settings → Composer Plus, then click **Assistant** in the left sidebar, right under **Task board**. It opens one lasting chat in `~/Assistant` that you use as your personal assistant. It sets reminders and automations with dsh's scheduled tasks, searches the web, and lists and reads your other chats. It sends a message to a chat, or starts, stops or archives a chat, only after you click **Allow once**. It cannot delete a chat. Keep your profile in this project's memory, topic `profil`. In Tether, the \"Assistant\" button in the Sessions toolbar opens the same chat.\n\n## Changes\n\n- **Remote computers**: the bar at the top of the window is gone. While the dsh of another Mac fills the window, the computer picker in the left sidebar says **Back to this Mac**. With one other computer, a click comes back at once. With more, the click opens the menu.\n- Tether (a new build): a question that dsh asks while Tether is closed now waits for your phone, and you can answer it when you open Tether. The Mac shows it too. \"New branch\" in the `+` menu of the message field shows in every chat. When the Mac cannot make a branch, it is off and shows the reason.\n\n## Fixes\n\n- Tether: the text that you send while a turn runs no longer stays in the message field.",
5
+ "zh": "## 新功能\n\n- **助理**(默认关闭):在 设置 → Composer Plus 中打开,然后点击左侧边栏中 **任务看板** 正下方的 **助理**。它会在 `~/Assistant` 中打开一个长期保留的对话,你可以把它当作个人助理。它用 dsh 的定时任务设置提醒和自动化,搜索网页,并列出和阅读你的其他对话。只有在你点击 **允许一次** 之后,它才会向某个对话发送消息,或新建、停止、归档对话。它不能删除对话。请把你的个人资料保存在该项目的记忆中,主题为 `profil`。在 Tether 中,Sessions 工具栏里的“Assistant”按钮会打开同一个对话。\n\n## 变更\n\n- **远程电脑**:窗口顶部的横条已移除。当另一台 Mac 的 dsh 占满窗口时,左侧边栏中的电脑选择器显示 **返回本机**。只有一台其他电脑时,点击一次即可立即返回。有多台时,点击会打开菜单。\n- Tether(新版本):Tether 关闭时 dsh 提出的问题现在会等待你的手机,你打开 Tether 后即可回答。Mac 上也会显示这个问题。消息框 `+` 菜单中的“New branch”现在会在每个对话中显示。Mac 无法创建分支时,它会被停用并显示原因。\n\n## 修复\n\n- Tether:轮次运行时发送的文本不再留在消息框中。",
6
+ "tr": "## Yeni\n\n- **Asistan** (varsayılan olarak kapalı): Ayarlar → Composer Plus içinden açın, sonra sol kenar çubuğunda **Görev panosu**'nun hemen altındaki **Asistan**'a tıklayın. `~/Assistant` klasöründe, kişisel asistanınız olarak kullandığınız tek ve kalıcı bir sohbet açılır. dsh'nin zamanlanmış görevleriyle hatırlatma ve otomasyon kurar, web'de arar, öteki sohbetlerinizi listeler ve okur. Bir sohbete mesaj göndermeden, sohbet açmadan, durdurmadan ya da arşivlemeden önce sizin **Bir kez izin ver**'e tıklamanızı bekler. Sohbet silemez. Profilinizi bu projenin hafızasında, `profil` konusunda tutun. Tether'da Sessions araç çubuğundaki \"Assistant\" düğmesi aynı sohbeti açar.\n\n## Değişiklikler\n\n- **Uzak bilgisayarlar**: pencerenin üstündeki şerit kaldırıldı. Başka bir Mac'in dsh'si pencereyi kaplarken sol kenar çubuğundaki bilgisayar seçici **Bu Mac’e dön** der. Tek bir başka bilgisayar varsa bir tıklama hemen geri döner. Daha fazlası varsa tıklama menüyü açar.\n- Tether (yeni derleme): Tether kapalıyken dsh'nin sorduğu soru artık telefonunuzu bekler ve Tether'ı açınca yanıtlayabilirsiniz. Soru Mac'te de görünür. Mesaj alanının `+` menüsündeki \"New branch\" her sohbette görünür. Mac dal oluşturamıyorsa seçenek kapalıdır ve nedenini gösterir.\n\n## Düzeltmeler\n\n- Tether: bir tur çalışırken gönderdiğiniz metin artık mesaj alanında kalmaz.",
7
+ "es": "## Novedades\n\n- **Asistente** (desactivado de forma predeterminada): actívalo en Configuración → Composer Plus y haz clic en **Asistente** en la barra lateral izquierda, justo debajo de **Tablero de tareas**. Abre un chat permanente en `~/Assistant` que usas como tu asistente personal. Crea recordatorios y automatizaciones con las tareas programadas de dsh, busca en la web y lista y lee tus otros chats. Solo envía un mensaje a un chat, o inicia, detiene o archiva un chat, después de que hagas clic en **Permitir una vez**. No puede eliminar chats. Guarda tu perfil en la memoria de este proyecto, tema `profil`. En Tether, el botón \"Assistant\" de la barra de herramientas de Sessions abre el mismo chat.\n\n## Cambios\n\n- **Equipos remotos**: la barra de la parte superior de la ventana ya no está. Mientras el dsh de otro Mac ocupa la ventana, el selector de equipos de la barra lateral izquierda dice **Volver a este Mac**. Con un solo equipo más, un clic vuelve al instante. Con más, el clic abre el menú.\n- Tether (nueva compilación): una pregunta que dsh hace mientras Tether está cerrado ahora espera a tu teléfono, y puedes responderla al abrir Tether. El Mac también la muestra. \"New branch\" en el menú `+` del campo de mensaje aparece en todos los chats. Cuando el Mac no puede crear una rama, está desactivado y muestra el motivo.\n\n## Correcciones\n\n- Tether: el texto que envías mientras se ejecuta un turno ya no se queda en el campo de mensaje.",
8
+ "pt-BR": "## Novidades\n\n- **Assistente** (desativado por padrão): ative em Configurações → Composer Plus e clique em **Assistente** na barra lateral esquerda, logo abaixo de **Quadro de tarefas**. Ele abre um chat permanente em `~/Assistant` que você usa como seu assistente pessoal. Ele cria lembretes e automações com as tarefas agendadas do dsh, pesquisa na web e lista e lê seus outros chats. Ele só envia uma mensagem a um chat, ou inicia, para ou arquiva um chat, depois que você clica em **Permitir uma vez**. Ele não pode excluir chats. Guarde seu perfil na memória deste projeto, tema `profil`. No Tether, o botão \"Assistant\" da barra de ferramentas de Sessions abre o mesmo chat.\n\n## Alterações\n\n- **Computadores remotos**: a barra no topo da janela foi removida. Enquanto o dsh de outro Mac ocupa a janela, o seletor de computadores na barra lateral esquerda mostra **Voltar para este Mac**. Com só mais um computador, um clique volta na hora. Com mais, o clique abre o menu.\n- Tether (nova versão): uma pergunta que o dsh faz enquanto o Tether está fechado agora espera pelo seu celular, e você pode respondê-la ao abrir o Tether. O Mac também a mostra. \"New branch\" no menu `+` do campo de mensagem aparece em todos os chats. Quando o Mac não pode criar um branch, a opção fica desativada e mostra o motivo.\n\n## Correções\n\n- Tether: o texto que você envia enquanto um turno está em execução não fica mais no campo de mensagem.",
9
+ "fr": "## Nouveautés\n\n- **Assistant** (désactivé par défaut) : activez-le dans Réglages → Composer Plus, puis cliquez sur **Assistant** dans la barre latérale gauche, juste sous **Tableau des tâches**. Il ouvre une discussion durable dans `~/Assistant` que vous utilisez comme assistant personnel. Il crée des rappels et des automatisations avec les tâches planifiées de dsh, cherche sur le web, et liste et lit vos autres discussions. Il n’envoie un message à une discussion, ou ne lance, n’arrête ou n’archive une discussion, qu’après un clic sur **Autoriser une fois**. Il ne peut pas supprimer de discussion. Gardez votre profil dans la mémoire de ce projet, sujet `profil`. Dans Tether, le bouton « Assistant » de la barre d’outils de Sessions ouvre la même discussion.\n\n## Modifications\n\n- **Ordinateurs distants** : la barre en haut de la fenêtre a disparu. Pendant que le dsh d’un autre Mac remplit la fenêtre, le sélecteur d’ordinateurs de la barre latérale gauche affiche **Revenir à ce Mac**. Avec un seul autre ordinateur, un clic revient tout de suite. Avec plusieurs, le clic ouvre le menu.\n- Tether (nouvelle version) : une question que dsh pose pendant que Tether est fermé attend maintenant votre téléphone, et vous pouvez y répondre quand vous ouvrez Tether. Le Mac l’affiche aussi. « New branch » dans le menu `+` du champ de message apparaît dans chaque discussion. Quand le Mac ne peut pas créer de branche, l’option est désactivée et indique la raison.\n\n## Corrections\n\n- Tether : le texte que vous envoyez pendant qu’un tour s’exécute ne reste plus dans le champ de message.",
10
+ "de": "## Neu\n\n- **Assistent** (standardmäßig aus): Schalte ihn unter Einstellungen → Composer Plus ein und klicke dann in der linken Seitenleiste direkt unter **Aufgabentafel** auf **Assistent**. Er öffnet einen dauerhaften Chat in `~/Assistant`, den du als persönlichen Assistenten nutzt. Er legt mit den geplanten Aufgaben von dsh Erinnerungen und Automatisierungen an, sucht im Web und listet und liest deine anderen Chats. Er sendet eine Nachricht an einen Chat oder startet, stoppt oder archiviert einen Chat erst, nachdem du auf **Einmal zulassen** geklickt hast. Er kann keinen Chat löschen. Speichere dein Profil im Gedächtnis dieses Projekts, Thema `profil`. In Tether öffnet die Schaltfläche „Assistant“ in der Symbolleiste von Sessions denselben Chat.\n\n## Änderungen\n\n- **Remote-Computer**: Die Leiste oben im Fenster ist weg. Während das dsh eines anderen Mac das Fenster füllt, zeigt die Computerauswahl in der linken Seitenleiste **Zurück zu diesem Mac**. Bei nur einem anderen Computer führt ein Klick sofort zurück. Bei mehreren öffnet der Klick das Menü.\n- Tether (neuer Build): Eine Frage, die dsh stellt, während Tether geschlossen ist, wartet jetzt auf dein iPhone, und du kannst sie beantworten, wenn du Tether öffnest. Der Mac zeigt sie ebenfalls. „New branch“ im Menü `+` des Nachrichtenfelds erscheint in jedem Chat. Wenn der Mac keinen Branch anlegen kann, ist die Option aus und nennt den Grund.\n\n## Korrekturen\n\n- Tether: Text, den du sendest, während ein Durchlauf läuft, bleibt nicht mehr im Nachrichtenfeld.",
11
+ "ru": "## Новое\n\n- **Ассистент** (по умолчанию выключено): включите его в Настройки → Composer Plus, затем нажмите **Ассистент** на левой боковой панели, сразу под **Доска задач**. Откроется один постоянный чат в `~/Assistant`, которым вы пользуетесь как личным ассистентом. Он создаёт напоминания и автоматизации с помощью запланированных задач dsh, ищет в интернете, показывает список ваших других чатов и читает их. Отправить сообщение в чат, начать, остановить или архивировать чат он может только после того, как вы нажмёте **Разрешить один раз**. Удалять чаты он не может. Храните свой профиль в памяти этого проекта, тема `profil`. В Tether кнопка «Assistant» на панели инструментов Sessions открывает тот же чат.\n\n## Изменения\n\n- **Удалённые компьютеры**: полоса вверху окна убрана. Пока dsh другого Mac занимает окно, переключатель компьютеров на левой боковой панели показывает **Вернуться к этому Mac**. Если другой компьютер один, нажатие сразу возвращает назад. Если их больше, нажатие открывает меню.\n- Tether (новая сборка): вопрос, который dsh задаёт, пока Tether закрыт, теперь ждёт ваш телефон, и вы можете ответить на него, когда откроете Tether. Mac тоже показывает его. «New branch» в меню `+` поля сообщения есть в каждом чате. Если Mac не может создать ветку, пункт выключен и показывает причину.\n\n## Исправления\n\n- Tether: текст, который вы отправляете во время выполнения хода, больше не остаётся в поле сообщения.",
12
+ "ja": "## 新機能\n\n- **アシスタント**(既定ではオフ):設定 → Composer Plus でオンにして、左サイドバーの **タスクボード** のすぐ下にある **アシスタント** をクリックします。`~/Assistant` に、パーソナルアシスタントとして使う長く続く 1 つのチャットが開きます。dsh のスケジュールされたタスクでリマインダーと自動化を設定し、Web を検索し、ほかのチャットを一覧表示して読みます。チャットへのメッセージ送信と、チャットの開始、停止、アーカイブは、あなたが **1回だけ許可** をクリックした後にだけ行います。チャットは削除できません。プロフィールは、このプロジェクトのメモリーのトピック `profil` に保存してください。Tether では、Sessions のツールバーにある「Assistant」ボタンが同じチャットを開きます。\n\n## 変更点\n\n- **リモートコンピュータ**:ウィンドウ上部のバーはなくなりました。別の Mac の dsh がウィンドウ全体に表示されている間、左サイドバーのコンピュータ選択は **この Mac に戻る** と表示します。ほかのコンピュータが 1 台だけなら、1 回のクリックですぐに戻ります。複数あるときは、クリックでメニューが開きます。\n- Tether(新しいビルド):Tether が閉じている間に dsh が出した質問は、iPhone を待つようになりました。Tether を開いたときに回答できます。Mac にも表示されます。メッセージ欄の `+` メニューにある「New branch」は、すべてのチャットに表示されます。Mac がブランチを作れないときはオフになり、理由を表示します。\n\n## 修正\n\n- Tether:ターンの実行中に送信したテキストが、メッセージ欄に残らなくなりました。",
13
+ "ko": "## 새 기능\n\n- **어시스턴트**(기본적으로 꺼짐): 설정 → Composer Plus에서 켠 다음, 왼쪽 사이드바에서 **작업 보드** 바로 아래의 **어시스턴트**를 클릭하세요. `~/Assistant`에 개인 어시스턴트로 쓰는, 오래 유지되는 채팅 하나가 열립니다. dsh의 예약 작업으로 미리 알림과 자동화를 설정하고, 웹을 검색하고, 다른 채팅을 나열하고 읽습니다. 채팅에 메시지를 보내거나 채팅을 시작, 중지, 보관하는 일은 **한 번 허용**을 클릭한 뒤에만 합니다. 채팅을 삭제할 수는 없습니다. 프로필은 이 프로젝트의 메모리에 `profil` 주제로 저장해 두세요. Tether에서는 Sessions 도구 막대의 \"Assistant\" 버튼이 같은 채팅을 엽니다.\n\n## 변경 사항\n\n- **원격 컴퓨터**: 창 위쪽의 막대가 없어졌습니다. 다른 Mac의 dsh가 창을 채우는 동안, 왼쪽 사이드바의 컴퓨터 선택기에 **이 Mac으로 돌아가기**가 표시됩니다. 다른 컴퓨터가 하나뿐이면 한 번의 클릭으로 바로 돌아옵니다. 더 많으면 클릭할 때 메뉴가 열립니다.\n- Tether(새 빌드): Tether가 닫혀 있는 동안 dsh가 묻는 질문은 이제 휴대폰을 기다리며, Tether를 열 때 답할 수 있습니다. Mac에도 표시됩니다. 메시지 입력란의 `+` 메뉴에 있는 \"New branch\"가 모든 채팅에 표시됩니다. Mac이 브랜치를 만들 수 없으면 꺼지고 이유를 보여 줍니다.\n\n## 수정 사항\n\n- Tether: 턴이 실행되는 동안 보낸 텍스트가 더 이상 메시지 입력란에 남지 않습니다.",
14
+ "id": "## Baru\n\n- **Asisten** (nonaktif secara default): aktifkan di Pengaturan → Composer Plus, lalu klik **Asisten** di bilah samping kiri, tepat di bawah **Papan tugas**. Fitur ini membuka satu obrolan tetap di `~/Assistant` yang Anda pakai sebagai asisten pribadi. Asisten membuat pengingat dan otomatisasi dengan tugas terjadwal dsh, mencari di web, serta menampilkan dan membaca obrolan Anda yang lain. Asisten hanya mengirim pesan ke obrolan, atau memulai, menghentikan, atau mengarsipkan obrolan, setelah Anda mengklik **Izinkan sekali**. Asisten tidak dapat menghapus obrolan. Simpan profil Anda di memori proyek ini, topik `profil`. Di Tether, tombol \"Assistant\" di bilah alat Sessions membuka obrolan yang sama.\n\n## Perubahan\n\n- **Komputer jarak jauh**: bilah di bagian atas jendela sudah dihapus. Saat dsh dari Mac lain memenuhi jendela, pemilih komputer di bilah samping kiri menampilkan **Kembali ke Mac ini**. Jika hanya ada satu komputer lain, satu klik langsung kembali. Jika lebih, klik membuka menu.\n- Tether (build baru): pertanyaan yang diajukan dsh saat Tether tertutup kini menunggu ponsel Anda, dan Anda dapat menjawabnya saat membuka Tether. Mac juga menampilkannya. \"New branch\" di menu `+` kolom pesan muncul di setiap obrolan. Jika Mac tidak dapat membuat branch, opsi ini nonaktif dan menampilkan alasannya.\n\n## Perbaikan\n\n- Tether: teks yang Anda kirim saat giliran berjalan tidak lagi tertinggal di kolom pesan.",
15
+ "vi": "## Mới\n\n- **Trợ lý** (mặc định tắt): bật trong Cài đặt → Composer Plus, rồi nhấp **Trợ lý** ở thanh bên trái, ngay bên dưới **Bảng tác vụ**. Tính năng này mở một cuộc trò chuyện lâu dài trong `~/Assistant` mà bạn dùng làm trợ lý cá nhân. Trợ lý đặt lời nhắc và tự động hóa bằng các tác vụ theo lịch của dsh, tìm kiếm trên web, liệt kê và đọc các cuộc trò chuyện khác của bạn. Trợ lý chỉ gửi tin nhắn đến một cuộc trò chuyện, hoặc bắt đầu, dừng hay lưu trữ một cuộc trò chuyện, sau khi bạn nhấp **Cho phép một lần**. Trợ lý không thể xóa cuộc trò chuyện. Hãy lưu hồ sơ của bạn trong bộ nhớ của dự án này, chủ đề `profil`. Trong Tether, nút \"Assistant\" trên thanh công cụ Sessions mở cùng cuộc trò chuyện đó.\n\n## Thay đổi\n\n- **Máy tính từ xa**: thanh ở đầu cửa sổ đã được bỏ. Khi dsh của một máy Mac khác chiếm toàn bộ cửa sổ, bộ chọn máy tính ở thanh bên trái hiện **Quay lại máy Mac này**. Nếu chỉ có một máy tính khác, một lần nhấp sẽ quay lại ngay. Nếu có nhiều hơn, lần nhấp sẽ mở menu.\n- Tether (bản dựng mới): câu hỏi mà dsh đặt ra khi Tether đang đóng giờ sẽ chờ điện thoại của bạn, và bạn có thể trả lời khi mở Tether. Mac cũng hiện câu hỏi đó. \"New branch\" trong menu `+` của ô tin nhắn hiện ở mọi cuộc trò chuyện. Khi Mac không thể tạo branch, tùy chọn này bị tắt và cho biết lý do.\n\n## Sửa lỗi\n\n- Tether: văn bản bạn gửi trong khi một lượt đang chạy không còn nằm lại trong ô tin nhắn.",
16
+ "hi": "## नया\n\n- **असिस्टेंट** (डिफ़ॉल्ट रूप से बंद): इसे सेटिंग → Composer Plus में चालू करें, फिर बाएं साइडबार में **टास्क बोर्ड** के ठीक नीचे **असिस्टेंट** पर क्लिक करें। यह `~/Assistant` में एक स्थायी चैट खोलता है, जिसे आप अपने निजी असिस्टेंट की तरह इस्तेमाल करते हैं। यह dsh के शेड्यूल किए गए टास्क से रिमाइंडर और ऑटोमेशन बनाता है, वेब पर खोजता है, और आपकी दूसरी चैट की सूची दिखाता और उन्हें पढ़ता है। यह किसी चैट को संदेश भेजने, या चैट शुरू करने, रोकने या आर्काइव करने का काम तभी करता है जब आप **एक बार अनुमति दें** पर क्लिक करते हैं। यह चैट डिलीट नहीं कर सकता। अपनी प्रोफ़ाइल इस प्रोजेक्ट की मेमोरी में, विषय `profil` में रखें। Tether में, Sessions टूलबार का \"Assistant\" बटन वही चैट खोलता है।\n\n## बदलाव\n\n- **रिमोट कंप्यूटर**: विंडो के ऊपर वाली पट्टी हटा दी गई है। जब किसी दूसरे Mac का dsh विंडो भरता है, तब बाएं साइडबार का कंप्यूटर पिकर **इस Mac पर वापस जाएं** दिखाता है। सिर्फ़ एक दूसरा कंप्यूटर होने पर एक क्लिक तुरंत वापस ले आता है। ज़्यादा होने पर क्लिक मेन्यू खोलता है।\n- Tether (नया बिल्ड): Tether बंद होने पर dsh जो सवाल पूछता है, वह अब आपके फ़ोन का इंतज़ार करता है, और Tether खोलने पर आप उसका जवाब दे सकते हैं। Mac भी उसे दिखाता है। मैसेज फ़ील्ड के `+` मेन्यू में \"New branch\" हर चैट में दिखता है। जब Mac ब्रांच नहीं बना सकता, तब यह बंद रहता है और कारण बताता है।\n\n## सुधार\n\n- Tether: टर्न चलते समय भेजा गया टेक्स्ट अब मैसेज फ़ील्ड में नहीं रहता।"
17
17
  }
18
18
  }
@@ -51,6 +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']], ['assistant', ['GET']],
54
55
  ])
55
56
  /** Allow for this chat: an Auto Review ask (`approval/request` with this reason) for a tool that the chat granted. */
56
57
  export const AUTO_REVIEW_ASK = 'Auto review denied tool '
@@ -291,10 +292,14 @@ function pipeUpgrade(req, socket, head, opts) {
291
292
  up.end()
292
293
  }
293
294
 
294
- /** Remote computers, the Mac that opens: one loopback port per paired computer. The browser (dsh's Browser tab or a
295
- * browser window) gets a one-time ticket URL; the ticket becomes this port's own cookie. Each request then goes to the
296
- * computer over TLS that must show the pinned certificate (checked before one byte goes out), with the computer's token. */
297
- export function createRelay({ host, port, fp, token, ticketTtlMs = 60e3, onUnauthorized, connectTimeoutMs = 5000, frameAncestors = [] }) {
295
+ /** The paths that the relay answers itself (never sent to the computer): the other Mac's page asks which computers this
296
+ * Mac has (`opener`) and switches this Mac's window to one of them (`switch`). */
297
+ export const RELAY_OWN = '/__composer-plus/'
298
+ /** Remote computers, the Mac that opens: one loopback port per paired computer. dsh's window (a `<webview>` on desktop, an
299
+ * iframe on dsh web) gets a one-time ticket URL; the ticket becomes this port's own cookie. Each request then goes to the
300
+ * computer over TLS that must show the pinned certificate (checked before one byte goes out), with the computer's token.
301
+ * `peer`: `{ opener(), signal(event) }` of this Mac, for the paths under `RELAY_OWN`. */
302
+ export function createRelay({ host, port, fp, token, ticketTtlMs = 60e3, onUnauthorized, connectTimeoutMs = 5000, frameAncestors = [], peer }) {
298
303
  const pin = normFp(fp)
299
304
  const tickets = new Map() // one-time ticket → expiry
300
305
  const cookieValue = randomBytes(32).toString('base64url')
@@ -349,14 +354,28 @@ export function createRelay({ host, port, fp, token, ticketTtlMs = 60e3, onUnaut
349
354
  return res.end()
350
355
  }
351
356
  if (!hasCookie(req)) return text(res, 401, 'Pick the computer again in dsh.')
352
- // Each answer may show only in this port's own frames and in dsh's page (its Browser tab).
357
+ // Each answer may show only in this port's own frames and in dsh's page (its window).
353
358
  const writeHead = res.writeHead.bind(res)
354
359
  res.writeHead = (code, headers = {}) => writeHead(code, { ...headers, 'content-security-policy': [headers['content-security-policy'] ?? [], `frame-ancestors ${ancestors}`].flat() })
360
+ if (url.pathname.startsWith(RELAY_OWN)) return ownPath(req, res, url.pathname.slice(RELAY_OWN.length))
355
361
  let unpaired = false
356
362
  await pipeHttp(req, res, next(req), { onStatus: (code) => { const r = status(code); if (r === 'skip') unpaired = true; return r },
357
363
  onError: (r) => text(r, 502, fail === 'pin' ? 'This computer shows another certificate. Pair it again in dsh.' : 'This computer does not answer over Tailscale. Make sure that it is awake and that Tailscale is on.') })
358
364
  if (unpaired) text(res, 401, UNPAIRED)
359
365
  }
366
+ /** The other Mac's page (in this Mac's window) reads this Mac's computers and switches the window: nothing else. */
367
+ async function ownPath(req, res, name) {
368
+ if (peer === undefined) return reply(res, 404, { error: 'not found' })
369
+ if (name === 'opener' && req.method === 'GET') { peer.signal({ hello: true }); return reply(res, 200, peer.opener()) }
370
+ if (name === 'switch' && req.method === 'POST') {
371
+ let to
372
+ try { to = JSON.parse(await readBody(req, 1024))?.to } catch { return reply(res, 400, { error: 'bad request' }) }
373
+ if (to !== 'self' && to !== 'add' && !(typeof to === 'string' && /^[0-9a-f]{64}$/.test(to))) return reply(res, 400, { error: 'bad request' })
374
+ peer.signal({ switch: to })
375
+ return reply(res, 200, { ok: true })
376
+ }
377
+ return reply(res, 404, { error: 'not found' })
378
+ }
360
379
  function onUpgrade(req, socket, head) {
361
380
  if (!trusted(req) || !hasCookie(req)) return socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n')
362
381
  pipeUpgrade(req, socket, head, next(req))
@@ -649,6 +668,8 @@ export function createGateway(opts = {}) {
649
668
  if (devices.length !== before) saveDevices()
650
669
  for (const s of devSockets.get(id) ?? []) s.destroy()
651
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' })
652
673
  return devices.length !== before
653
674
  }
654
675
  function notify(msg) {
@@ -736,11 +757,13 @@ export function createGateway(opts = {}) {
736
757
  if (v.type === 'cancel' && dropPending(v.eventId)) return toPhones({ type: 'cancel', eventId: v.eventId })
737
758
  if (v.type !== 'waterfall') return
738
759
  if (v.event === 'approval/request') return grantedOrAsk(v)
739
- 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(() => {})
740
761
  askPhones(v)
741
762
  }
742
- /** The gateway keeps an ask for the phones while one is connected, or while a phone gets pushes: the push opens Tether,
743
- * 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. */
744
767
  const holds = () => evSubs.size > 0 || pushTo().length > 0
745
768
  /** Allow for this chat, also with no dsh window open: the gateway allows a granted tool itself (the grants that the
746
769
  * desktop and the phone save in dsh-composer-plus). Any other approval goes to the phones. */
@@ -757,8 +780,10 @@ export function createGateway(opts = {}) {
757
780
  const frame = { type: 'waterfall', event: v.event, eventId: v.eventId, agentId: v.agentId, request: v.request }
758
781
  if (v.event === 'approval/request') pushAll('Approval needed', v.request?.toolName, v.agentId)
759
782
  else pushAll('DSH has a question', v.request?.questions?.[0]?.question, v.agentId)
760
- // 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.
761
- 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
762
787
  pendingEv.set(v.eventId, { frame, timer })
763
788
  toPhones(frame)
764
789
  }
@@ -892,7 +917,11 @@ export function createGateway(opts = {}) {
892
917
  // A failed connect (DSH restarting) is forgotten so the next stream open retries instead of failing forever.
893
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 })
894
919
  const err = (streamId, message) => ws.send(JSON.stringify({ type: 'error', streamId, error: { code: 'mobile/forbidden', message } }))
895
- 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
+ }
896
925
  ws.onText = async text => {
897
926
  let f
898
927
  try { f = JSON.parse(text) } catch { return ws.close(1007) }