@vellumai/assistant 0.3.19 → 0.3.20
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/ARCHITECTURE.md +151 -15
- package/Dockerfile +1 -0
- package/README.md +40 -4
- package/docs/architecture/integrations.md +7 -11
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -0
- package/src/__tests__/approval-primitive.test.ts +540 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
- package/src/__tests__/call-controller.test.ts +439 -108
- package/src/__tests__/channel-invite-transport.test.ts +264 -0
- package/src/__tests__/cli.test.ts +42 -1
- package/src/__tests__/config-schema.test.ts +11 -127
- package/src/__tests__/config-watcher.test.ts +0 -8
- package/src/__tests__/daemon-lifecycle.test.ts +1 -0
- package/src/__tests__/daemon-server-session-init.test.ts +8 -2
- package/src/__tests__/diff.test.ts +22 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +300 -32
- package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
- package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
- package/src/__tests__/guardian-dispatch.test.ts +124 -0
- package/src/__tests__/guardian-grant-minting.test.ts +6 -17
- package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
- package/src/__tests__/invite-redemption-service.test.ts +306 -0
- package/src/__tests__/ipc-snapshot.test.ts +57 -0
- package/src/__tests__/notification-decision-fallback.test.ts +88 -0
- package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
- package/src/__tests__/sandbox-host-parity.test.ts +6 -13
- package/src/__tests__/scoped-approval-grants.test.ts +6 -6
- package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
- package/src/__tests__/session-load-history-repair.test.ts +169 -2
- package/src/__tests__/session-runtime-assembly.test.ts +33 -5
- package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
- package/src/__tests__/skill-feature-flags.test.ts +188 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
- package/src/__tests__/skill-mirror-parity.test.ts +1 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
- package/src/__tests__/system-prompt.test.ts +1 -1
- package/src/__tests__/terminal-sandbox.test.ts +142 -9
- package/src/__tests__/terminal-tools.test.ts +2 -93
- package/src/__tests__/thread-seed-composer.test.ts +18 -0
- package/src/__tests__/tool-approval-handler.test.ts +350 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
- package/src/agent/loop.ts +36 -1
- package/src/approvals/approval-primitive.ts +381 -0
- package/src/approvals/guardian-decision-primitive.ts +191 -0
- package/src/calls/call-controller.ts +252 -209
- package/src/calls/call-domain.ts +44 -6
- package/src/calls/guardian-dispatch.ts +48 -0
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +46 -30
- package/src/cli/core-commands.ts +0 -4
- package/src/cli.ts +76 -34
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
- package/src/config/assistant-feature-flags.ts +162 -0
- package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
- package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
- package/src/config/bundled-skills/notifications/SKILL.md +1 -1
- package/src/config/bundled-skills/reminder/SKILL.md +49 -2
- package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
- package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
- package/src/config/core-schema.ts +1 -1
- package/src/config/env-registry.ts +10 -0
- package/src/config/feature-flag-registry.json +61 -0
- package/src/config/loader.ts +22 -1
- package/src/config/sandbox-schema.ts +0 -39
- package/src/config/schema.ts +6 -2
- package/src/config/skill-state.ts +34 -0
- package/src/config/skills-schema.ts +0 -1
- package/src/config/skills.ts +9 -0
- package/src/config/system-prompt.ts +110 -46
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +19 -1
- package/src/config/vellum-skills/catalog.json +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/telegram-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +104 -3
- package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
- package/src/daemon/config-watcher.ts +0 -1
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/guardian-invite-intent.ts +124 -0
- package/src/daemon/handlers/avatar.ts +68 -0
- package/src/daemon/handlers/browser.ts +2 -2
- package/src/daemon/handlers/guardian-actions.ts +120 -0
- package/src/daemon/handlers/index.ts +4 -0
- package/src/daemon/handlers/sessions.ts +19 -0
- package/src/daemon/handlers/shared.ts +3 -1
- package/src/daemon/install-cli-launchers.ts +58 -13
- package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -2
- package/src/daemon/ipc-contract/settings.ts +25 -2
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +4 -0
- package/src/daemon/lifecycle.ts +6 -2
- package/src/daemon/main.ts +1 -0
- package/src/daemon/server.ts +1 -0
- package/src/daemon/session-lifecycle.ts +52 -7
- package/src/daemon/session-memory.ts +45 -0
- package/src/daemon/session-process.ts +258 -432
- package/src/daemon/session-runtime-assembly.ts +12 -0
- package/src/daemon/session-skill-tools.ts +14 -1
- package/src/daemon/session-tool-setup.ts +5 -0
- package/src/daemon/session.ts +11 -0
- package/src/daemon/tool-side-effects.ts +35 -9
- package/src/index.ts +0 -2
- package/src/memory/conversation-display-order-migration.ts +44 -0
- package/src/memory/conversation-queries.ts +2 -0
- package/src/memory/conversation-store.ts +91 -0
- package/src/memory/db-init.ts +5 -1
- package/src/memory/embedding-local.ts +13 -8
- package/src/memory/guardian-action-store.ts +125 -2
- package/src/memory/ingress-invite-store.ts +95 -1
- package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
- package/src/memory/migrations/index.ts +2 -1
- package/src/memory/schema.ts +5 -1
- package/src/memory/scoped-approval-grants.ts +14 -5
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/notifications/decision-engine.ts +49 -12
- package/src/notifications/emit-signal.ts +7 -0
- package/src/notifications/signal.ts +7 -0
- package/src/notifications/thread-seed-composer.ts +2 -1
- package/src/runtime/channel-approval-types.ts +16 -6
- package/src/runtime/channel-approvals.ts +19 -15
- package/src/runtime/channel-invite-transport.ts +85 -0
- package/src/runtime/channel-invite-transports/telegram.ts +105 -0
- package/src/runtime/guardian-action-grant-minter.ts +92 -35
- package/src/runtime/guardian-action-message-composer.ts +30 -0
- package/src/runtime/guardian-decision-types.ts +91 -0
- package/src/runtime/http-server.ts +23 -1
- package/src/runtime/ingress-service.ts +22 -0
- package/src/runtime/invite-redemption-service.ts +181 -0
- package/src/runtime/invite-redemption-templates.ts +39 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/guardian-action-routes.ts +206 -0
- package/src/runtime/routes/guardian-approval-interception.ts +66 -190
- package/src/runtime/routes/inbound-message-handler.ts +486 -394
- package/src/runtime/routes/pairing-routes.ts +4 -0
- package/src/security/encrypted-store.ts +31 -17
- package/src/security/keychain.ts +176 -2
- package/src/security/secure-keys.ts +97 -0
- package/src/security/tool-approval-digest.ts +1 -1
- package/src/tools/browser/browser-execution.ts +2 -2
- package/src/tools/browser/browser-manager.ts +46 -32
- package/src/tools/browser/browser-screencast.ts +2 -2
- package/src/tools/calls/call-start.ts +1 -1
- package/src/tools/executor.ts +22 -17
- package/src/tools/network/script-proxy/session-manager.ts +1 -5
- package/src/tools/skills/load.ts +22 -8
- package/src/tools/system/avatar-generator.ts +119 -0
- package/src/tools/system/navigate-settings.ts +65 -0
- package/src/tools/system/open-system-settings.ts +75 -0
- package/src/tools/system/voice-config.ts +121 -32
- package/src/tools/terminal/backends/native.ts +40 -19
- package/src/tools/terminal/backends/types.ts +3 -3
- package/src/tools/terminal/parser.ts +1 -1
- package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
- package/src/tools/terminal/sandbox.ts +1 -12
- package/src/tools/terminal/shell.ts +3 -31
- package/src/tools/tool-approval-handler.ts +141 -3
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +6 -0
- package/src/util/diff.ts +36 -13
- package/Dockerfile.sandbox +0 -5
- package/src/__tests__/doordash-client.test.ts +0 -187
- package/src/__tests__/doordash-session.test.ts +0 -154
- package/src/__tests__/signup-e2e.test.ts +0 -354
- package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
- package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
- package/src/cli/doordash.ts +0 -1057
- package/src/config/bundled-skills/doordash/SKILL.md +0 -163
- package/src/config/templates/LOOKS.md +0 -25
- package/src/doordash/cart-queries.ts +0 -787
- package/src/doordash/client.ts +0 -1016
- package/src/doordash/order-queries.ts +0 -85
- package/src/doordash/queries.ts +0 -13
- package/src/doordash/query-extractor.ts +0 -94
- package/src/doordash/search-queries.ts +0 -203
- package/src/doordash/session.ts +0 -84
- package/src/doordash/store-queries.ts +0 -246
- package/src/doordash/types.ts +0 -367
- package/src/tools/terminal/backends/docker.ts +0 -379
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "Trusted Contacts"
|
|
3
|
-
description: "Manage trusted contacts — list, allow, revoke,
|
|
3
|
+
description: "Manage trusted contacts and Telegram invite links — list, allow, revoke, block users, and create/list/revoke invite links for external channels"
|
|
4
4
|
user-invocable: true
|
|
5
5
|
metadata: {"vellum": {"emoji": "\ud83d\udc65"}}
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
You are helping your user manage trusted contacts for the Vellum Assistant. Trusted contacts control who is allowed to send messages to the assistant through external channels like Telegram and SMS. All operations go through the gateway HTTP API using `curl` with bearer auth.
|
|
8
|
+
You are helping your user manage trusted contacts and invite links for the Vellum Assistant. Trusted contacts control who is allowed to send messages to the assistant through external channels like Telegram and SMS. Invite links let the guardian share a Telegram deep link that automatically grants access when opened. All operations go through the gateway HTTP API using `curl` with bearer auth.
|
|
9
9
|
|
|
10
10
|
## Prerequisites
|
|
11
11
|
|
|
@@ -18,6 +18,7 @@ You are helping your user manage trusted contacts for the Vellum Assistant. Trus
|
|
|
18
18
|
- **Policy**: Controls what the member can do — `allow` (can message freely) or `deny` (blocked from messaging).
|
|
19
19
|
- **Status**: The member's lifecycle state — `active` (currently effective), `revoked` (access removed), or `blocked` (explicitly denied).
|
|
20
20
|
- **Source channel**: The messaging platform the contact uses (e.g., `telegram`, `sms`).
|
|
21
|
+
- **Invite link**: A shareable Telegram deep link that, when opened by someone, automatically grants them trusted-contact access. Each invite has a token, usage limits, and optional expiration.
|
|
21
22
|
|
|
22
23
|
## Available Actions
|
|
23
24
|
|
|
@@ -117,9 +118,101 @@ curl -s -X POST "http://localhost:7830/v1/ingress/members/<member_id>/block" \
|
|
|
117
118
|
-d '{"reason": "<optional reason>"}'
|
|
118
119
|
```
|
|
119
120
|
|
|
121
|
+
### 5. Create a Telegram invite link
|
|
122
|
+
|
|
123
|
+
Use this when the guardian wants to invite someone to message the assistant on Telegram without needing their user ID upfront. The invite link is a shareable Telegram deep link — when someone opens it, they automatically get trusted-contact access.
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
TOKEN=$(cat ~/.vellum/http-token)
|
|
127
|
+
curl -s -X POST http://localhost:7830/v1/ingress/invites \
|
|
128
|
+
-H "Content-Type: application/json" \
|
|
129
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
130
|
+
-d '{
|
|
131
|
+
"sourceChannel": "telegram",
|
|
132
|
+
"maxUses": 1,
|
|
133
|
+
"note": "<optional note, e.g. the person it is for>"
|
|
134
|
+
}'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Optional fields:
|
|
138
|
+
- `maxUses` — how many times the link can be used (default: 1). Use a higher number for group invites.
|
|
139
|
+
- `expiresInMs` — expiration time in milliseconds from now (e.g., `86400000` for 24 hours). Defaults to 7 days (`604800000`) if omitted.
|
|
140
|
+
- `note` — a human-readable label for the invite (e.g., "For Mom", "Family group").
|
|
141
|
+
|
|
142
|
+
The response contains `{ ok: true, invite: { id, token, ... } }`. The `token` field is the raw invite token — it is only returned at creation time and cannot be retrieved later.
|
|
143
|
+
|
|
144
|
+
**Building the shareable link**: After creating the invite, look up the Telegram bot username so you can build the deep link. Query the Telegram integration config:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
TOKEN=$(cat ~/.vellum/http-token)
|
|
148
|
+
curl -s http://localhost:7830/v1/integrations/telegram/config \
|
|
149
|
+
-H "Authorization: Bearer $TOKEN"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The response includes `botUsername`. Use it to construct the deep link:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
https://t.me/<botUsername>?start=iv_<token>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Presenting to the guardian**: Give the guardian the link with clear copy-paste instructions:
|
|
159
|
+
|
|
160
|
+
> Here's your Telegram invite link:
|
|
161
|
+
>
|
|
162
|
+
> `https://t.me/<botUsername>?start=iv_<token>`
|
|
163
|
+
>
|
|
164
|
+
> Share this link with the person you want to invite. When they open it in Telegram and press "Start", they'll automatically be added as a trusted contact and can message the assistant directly.
|
|
165
|
+
>
|
|
166
|
+
> This link can be used <maxUses> time(s)<and expires in X hours/days if applicable>.
|
|
167
|
+
|
|
168
|
+
If the Telegram bot username is not available (integration not set up), tell the guardian they need to set up the Telegram integration first using the Telegram Setup skill.
|
|
169
|
+
|
|
170
|
+
### 6. List invite links
|
|
171
|
+
|
|
172
|
+
Use this to show the guardian their active (and optionally all) invite links.
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
TOKEN=$(cat ~/.vellum/http-token)
|
|
176
|
+
curl -s "http://localhost:7830/v1/ingress/invites?sourceChannel=telegram" \
|
|
177
|
+
-H "Authorization: Bearer $TOKEN"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Optional query parameters:
|
|
181
|
+
- `sourceChannel` — filter by channel (e.g., `telegram`)
|
|
182
|
+
- `status` — filter by status (`active`, `revoked`, `redeemed`, `expired`)
|
|
183
|
+
|
|
184
|
+
The response contains `{ ok: true, invites: [...] }` where each invite has:
|
|
185
|
+
- `id` — unique invite ID (needed for revoke)
|
|
186
|
+
- `sourceChannel` — the channel
|
|
187
|
+
- `tokenHash` — hashed token (the raw token is only available at creation time)
|
|
188
|
+
- `maxUses` — total allowed uses
|
|
189
|
+
- `useCount` — how many times it has been redeemed
|
|
190
|
+
- `expiresAt` — expiration timestamp (null if no expiration)
|
|
191
|
+
- `status` — current status (`active`, `revoked`, `redeemed`, `expired`)
|
|
192
|
+
- `note` — the label set at creation
|
|
193
|
+
- `createdAt` — when the invite was created
|
|
194
|
+
|
|
195
|
+
**Presenting results**: Format as a readable list. Show the note (or "unnamed" as fallback), status, uses remaining (`maxUses - useCount`), and expiration. Highlight active invites and note which ones have been fully used or expired.
|
|
196
|
+
|
|
197
|
+
### 7. Revoke an invite link
|
|
198
|
+
|
|
199
|
+
Use this when the guardian wants to cancel an active invite link. **Always confirm before revoking.**
|
|
200
|
+
|
|
201
|
+
Ask the user: *"I'll revoke the invite link [note or ID]. It will no longer be usable. Should I proceed?"*
|
|
202
|
+
|
|
203
|
+
First, list invites to find the invite's `id`, then revoke:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
TOKEN=$(cat ~/.vellum/http-token)
|
|
207
|
+
curl -s -X DELETE "http://localhost:7830/v1/ingress/invites/<invite_id>" \
|
|
208
|
+
-H "Authorization: Bearer $TOKEN"
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Replace `<invite_id>` with the invite's `id` from the list response.
|
|
212
|
+
|
|
120
213
|
## Confirmation Requirements
|
|
121
214
|
|
|
122
|
-
**All mutating actions (allow, revoke, block) require explicit user confirmation before execution.** This is a safety measure — modifying who can access the assistant should always be a deliberate choice.
|
|
215
|
+
**All mutating actions (allow, revoke, block, revoke invite) require explicit user confirmation before execution.** This is a safety measure — modifying who can access the assistant should always be a deliberate choice. Creating an invite link does not require confirmation since it does not grant access until someone opens it.
|
|
123
216
|
|
|
124
217
|
- Clearly state what action you are about to take and who it affects.
|
|
125
218
|
- Wait for the user to confirm before running the curl command.
|
|
@@ -133,6 +226,8 @@ curl -s -X POST "http://localhost:7830/v1/ingress/members/<member_id>/block" \
|
|
|
133
226
|
- `At least one of externalUserId or externalChatId is required` — ask the user for the contact's channel-specific identifier.
|
|
134
227
|
- `Member not found or cannot be revoked` — the member ID may be invalid or the member is already revoked.
|
|
135
228
|
- `Member not found or already blocked` — the member ID may be invalid or the member is already blocked.
|
|
229
|
+
- `sourceChannel is required for create` — when creating an invite, always pass `"sourceChannel": "telegram"`.
|
|
230
|
+
- `Invite not found or already revoked` — the invite ID may be invalid or the invite is already revoked.
|
|
136
231
|
|
|
137
232
|
## Typical Workflows
|
|
138
233
|
|
|
@@ -145,3 +240,9 @@ curl -s -X POST "http://localhost:7830/v1/ingress/members/<member_id>/block" \
|
|
|
145
240
|
**"Block [name]"** — List members to find them, confirm the block, then execute.
|
|
146
241
|
|
|
147
242
|
**"Show me blocked contacts"** — List with `status=blocked` filter.
|
|
243
|
+
|
|
244
|
+
**"Create a Telegram invite link"** / **"Invite someone on Telegram"** — Create an invite with `sourceChannel: "telegram"`, look up the bot username, build the deep link, and present it with sharing instructions.
|
|
245
|
+
|
|
246
|
+
**"Show my invites"** / **"List active invite links"** — List invites filtered by `sourceChannel=telegram`, present active invites with uses remaining and expiration info.
|
|
247
|
+
|
|
248
|
+
**"Revoke invite"** / **"Cancel invite link"** — List invites to identify the target, confirm, then revoke by ID.
|
|
@@ -210,7 +210,7 @@ Install and load the **guardian-verify-setup** skill to handle the verification
|
|
|
210
210
|
|
|
211
211
|
The guardian-verify-setup skill manages the full outbound verification flow for **one channel at a time** (sms, voice, or telegram). Each invocation handles:
|
|
212
212
|
- Collecting the user's phone number as the destination (accepts any common format -- the API normalizes to E.164)
|
|
213
|
-
- Starting the outbound verification session via `POST /v1/integrations/guardian/outbound/start`
|
|
213
|
+
- Starting the outbound verification session via the gateway endpoint `POST /v1/integrations/guardian/outbound/start`
|
|
214
214
|
- For **SMS**: sending a 6-digit code to the phone number that the user must reply with from the SMS channel
|
|
215
215
|
- For **voice**: calling the phone number and providing a code for the user to enter via their phone's keypad
|
|
216
216
|
- Checking guardian status to confirm the binding was created
|
|
@@ -109,7 +109,6 @@ export class ConfigWatcher {
|
|
|
109
109
|
'SOUL.md': () => onSessionEvict(),
|
|
110
110
|
'IDENTITY.md': () => { onSessionEvict(); onIdentityChanged?.(); },
|
|
111
111
|
'USER.md': () => onSessionEvict(),
|
|
112
|
-
'LOOKS.md': () => onSessionEvict(),
|
|
113
112
|
'UPDATES.md': () => onSessionEvict(),
|
|
114
113
|
};
|
|
115
114
|
|
|
@@ -106,7 +106,7 @@ function isProcessRunning(pid: number): boolean {
|
|
|
106
106
|
*/
|
|
107
107
|
function isVellumDaemonProcess(pid: number): boolean {
|
|
108
108
|
try {
|
|
109
|
-
const cmd = execSync(`ps -p ${pid} -o command=`, {
|
|
109
|
+
const cmd = execSync(`ps -ww -p ${pid} -o command=`, {
|
|
110
110
|
encoding: 'utf-8',
|
|
111
111
|
timeout: 3000,
|
|
112
112
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Guardian invite intent resolution for deterministic first-turn routing.
|
|
2
|
+
// Exports `resolveGuardianInviteIntent` as the single public entry point.
|
|
3
|
+
// When a guardian invite management request is detected, the session pipeline
|
|
4
|
+
// rewrites the message to force immediate entry into the trusted-contacts
|
|
5
|
+
// skill flow, bypassing the normal agent loop's tendency to produce conceptual
|
|
6
|
+
// preambles before loading the skill.
|
|
7
|
+
|
|
8
|
+
export type GuardianInviteIntentResult =
|
|
9
|
+
| { kind: 'none' }
|
|
10
|
+
| { kind: 'invite_management'; rewrittenContent: string; action?: 'create' | 'list' | 'revoke' };
|
|
11
|
+
|
|
12
|
+
// ── Direct invite patterns ────────────────────────────────────────────────
|
|
13
|
+
// These capture imperative requests to manage Telegram invite links.
|
|
14
|
+
|
|
15
|
+
const CREATE_INVITE_PATTERNS: RegExp[] = [
|
|
16
|
+
/\bcreate\s+(?:an?\s+)?(?:telegram\s+)?invite\s*(?:link)?\b/i,
|
|
17
|
+
/\binvite\s+(?:someone|somebody|a\s+friend|a\s+person)\s+(?:on|to|via|through)\s+telegram\b/i,
|
|
18
|
+
/\b(?:make|generate|get)\s+(?:a\s+|an\s+)?(?:telegram\s+)?invite\s*(?:link)?\b/i,
|
|
19
|
+
/\btelegram\s+invite\s*(?:link)?\b/i,
|
|
20
|
+
/\bsend\s+(?:a\s+|an\s+)?invite\s+(?:link\s+)?(?:on|for|via|through)\s+telegram\b/i,
|
|
21
|
+
/\bshare\s+(?:a\s+|an\s+)?(?:telegram\s+)?invite\s*(?:link)?\b/i,
|
|
22
|
+
/\binvite\s+(?:link\s+)?for\s+telegram\b/i,
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const LIST_INVITE_PATTERNS: RegExp[] = [
|
|
26
|
+
/\b(?:show|list|view|see|display)\s+(?:my\s+)?(?:active\s+)?invite(?:s|\s*links?)\b/i,
|
|
27
|
+
/\b(?:show|list|view|see|display)\s+(?:my\s+)?(?:telegram\s+)?invite(?:s|\s*links?)\b/i,
|
|
28
|
+
/\bwhat\s+invite(?:s|\s*links?)\s+(?:do\s+I\s+have|are\s+active|exist)\b/i,
|
|
29
|
+
/\bhow\s+many\s+invite(?:s|\s*links?)\b/i,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const REVOKE_INVITE_PATTERNS: RegExp[] = [
|
|
33
|
+
/\b(?:revoke|cancel|disable|invalidate|delete|remove)\s+(?:the\s+|my\s+|an?\s+)?invite\s*(?:link)?\b/i,
|
|
34
|
+
/\b(?:revoke|cancel|disable|invalidate|delete|remove)\s+(?:the\s+|my\s+|an?\s+)?(?:telegram\s+)?invite\s*(?:link)?\b/i,
|
|
35
|
+
/\binvite\s*(?:link)?\s+(?:revoke|cancel|disable|invalidate|delete|remove)\b/i,
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// ── Conceptual / question patterns ──────────────────────────────────────
|
|
39
|
+
// These indicate the user is asking *about* invites rather than requesting
|
|
40
|
+
// to manage them. Return passthrough for these.
|
|
41
|
+
|
|
42
|
+
const CONCEPTUAL_PATTERNS: RegExp[] = [
|
|
43
|
+
/^\s*(?:how|what|why|when|where|who|which)\b.*\binvite/i,
|
|
44
|
+
/\bwhat\s+(?:is|are)\s+(?:an?\s+)?invite\s*(?:link)?\b/i,
|
|
45
|
+
/\bhow\s+(?:do|does|can)\s+(?:invite|invitation)s?\s+work\b/i,
|
|
46
|
+
/\bexplain\s+(?:the\s+)?invite\b/i,
|
|
47
|
+
/\btell\s+me\s+about\s+invite\b/i,
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/** Common polite/filler words stripped before checking intent-only status. */
|
|
51
|
+
const FILLER_PATTERN =
|
|
52
|
+
/\b(please|pls|plz|can\s+you|could\s+you|would\s+you|now|right\s+now|thanks|thank\s+you|thx|ty|for\s+me|ok(ay)?|hey|hi|hello|just|i\s+want\s+to|i'd\s+like\s+to|i\s+need\s+to|let's|let\s+me)\b/gi;
|
|
53
|
+
|
|
54
|
+
// ── Internal helpers ─────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
function isConceptualQuestion(text: string): boolean {
|
|
57
|
+
const cleaned = text.replace(/^\s*(hey|hi|hello|please|pls|plz)[,\s]+/i, '');
|
|
58
|
+
// Allow actionable requests through even though they start with
|
|
59
|
+
// question-like words — these are imperative invite management requests.
|
|
60
|
+
if (LIST_INVITE_PATTERNS.some((p) => p.test(cleaned))) return false;
|
|
61
|
+
if (CREATE_INVITE_PATTERNS.some((p) => p.test(cleaned))) return false;
|
|
62
|
+
if (REVOKE_INVITE_PATTERNS.some((p) => p.test(cleaned))) return false;
|
|
63
|
+
return CONCEPTUAL_PATTERNS.some((p) => p.test(cleaned));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function detectAction(text: string): 'create' | 'list' | 'revoke' | undefined {
|
|
67
|
+
// Check revoke and list before create — create patterns include the broad
|
|
68
|
+
// `telegram invite link` matcher that would otherwise swallow revoke/list inputs.
|
|
69
|
+
if (REVOKE_INVITE_PATTERNS.some((p) => p.test(text))) return 'revoke';
|
|
70
|
+
if (LIST_INVITE_PATTERNS.some((p) => p.test(text))) return 'list';
|
|
71
|
+
if (CREATE_INVITE_PATTERNS.some((p) => p.test(text))) return 'create';
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Structured intent resolver ───────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolves guardian invite management intent from user text.
|
|
79
|
+
*
|
|
80
|
+
* Pipeline:
|
|
81
|
+
* 1. Skip slash commands entirely
|
|
82
|
+
* 2. Conceptual question gate -- questions return `none`
|
|
83
|
+
* 3. Detect create/list/revoke invite patterns
|
|
84
|
+
* 4. On match, build a deterministic model instruction to load trusted-contacts
|
|
85
|
+
*/
|
|
86
|
+
export function resolveGuardianInviteIntent(text: string): GuardianInviteIntentResult {
|
|
87
|
+
const trimmed = text.trim();
|
|
88
|
+
|
|
89
|
+
// Never intercept slash commands
|
|
90
|
+
if (trimmed.startsWith('/')) {
|
|
91
|
+
return { kind: 'none' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Conceptual questions pass through to normal agent processing
|
|
95
|
+
if (isConceptualQuestion(trimmed)) {
|
|
96
|
+
return { kind: 'none' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Strip fillers for pattern matching but keep original for context
|
|
100
|
+
const withoutFillers = trimmed.replace(FILLER_PATTERN, '').replace(/\s{2,}/g, ' ').trim();
|
|
101
|
+
|
|
102
|
+
const action = detectAction(withoutFillers);
|
|
103
|
+
if (!action) {
|
|
104
|
+
return { kind: 'none' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Build the rewritten content that deterministically loads the skill
|
|
108
|
+
const actionDescriptions: Record<string, string> = {
|
|
109
|
+
create: 'The user wants to create a Telegram invite link. Create the invite, look up the bot username, and present the shareable deep link with copy-paste instructions.',
|
|
110
|
+
list: 'The user wants to see their invite links. List all invites (especially active ones for Telegram) and present them in a readable format.',
|
|
111
|
+
revoke: 'The user wants to revoke an invite link. List invites to identify the target, confirm with the user, then revoke it.',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const rewrittenContent = [
|
|
115
|
+
actionDescriptions[action],
|
|
116
|
+
'Please invoke the "Trusted Contacts" skill (ID: trusted-contacts) immediately using skill_load.',
|
|
117
|
+
].join('\n');
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
kind: 'invite_management',
|
|
121
|
+
rewrittenContent,
|
|
122
|
+
action,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as net from 'node:net';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { setAvatarTool } from '../../tools/system/avatar-generator.js';
|
|
5
|
+
import { getWorkspaceDir } from '../../util/platform.js';
|
|
6
|
+
import type { GenerateAvatarRequest } from '../ipc-contract/settings.js';
|
|
7
|
+
import { defineHandlers, type HandlerContext, log } from './shared.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Handle a client request to generate a custom avatar via Gemini.
|
|
11
|
+
* Invokes the set_avatar tool directly, sends a response to the requesting
|
|
12
|
+
* client, and broadcasts avatar_updated to all clients on success.
|
|
13
|
+
*/
|
|
14
|
+
async function handleGenerateAvatar(
|
|
15
|
+
msg: GenerateAvatarRequest,
|
|
16
|
+
socket: net.Socket,
|
|
17
|
+
ctx: HandlerContext,
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const description = msg.description?.trim();
|
|
20
|
+
if (!description) {
|
|
21
|
+
ctx.send(socket, {
|
|
22
|
+
type: 'generate_avatar_response',
|
|
23
|
+
success: false,
|
|
24
|
+
error: 'Description is required.',
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
log.info({ description }, 'Generating avatar via IPC request');
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const result = await setAvatarTool.execute(
|
|
33
|
+
{ description },
|
|
34
|
+
// Minimal tool context — avatar generation needs no session context
|
|
35
|
+
{} as Parameters<typeof setAvatarTool.execute>[1],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (result.isError) {
|
|
39
|
+
ctx.send(socket, {
|
|
40
|
+
type: 'generate_avatar_response',
|
|
41
|
+
success: false,
|
|
42
|
+
error: result.content,
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Broadcast avatar change to all connected clients
|
|
48
|
+
const avatarPath = join(getWorkspaceDir(), 'data', 'avatar', 'custom-avatar.png');
|
|
49
|
+
ctx.broadcast({ type: 'avatar_updated', avatarPath });
|
|
50
|
+
|
|
51
|
+
ctx.send(socket, {
|
|
52
|
+
type: 'generate_avatar_response',
|
|
53
|
+
success: true,
|
|
54
|
+
});
|
|
55
|
+
} catch (err) {
|
|
56
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
57
|
+
log.error({ error: message }, 'Avatar generation failed unexpectedly');
|
|
58
|
+
ctx.send(socket, {
|
|
59
|
+
type: 'generate_avatar_response',
|
|
60
|
+
success: false,
|
|
61
|
+
error: message,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const avatarHandlers = defineHandlers({
|
|
67
|
+
generate_avatar: handleGenerateAvatar,
|
|
68
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { browserManager } from '../../tools/browser/browser-manager.js';
|
|
1
|
+
import { browserManager, SCREENCAST_HEIGHT,SCREENCAST_WIDTH } from '../../tools/browser/browser-manager.js';
|
|
2
2
|
import { defineHandlers,log } from './shared.js';
|
|
3
3
|
|
|
4
4
|
export const browserHandlers = defineHandlers({
|
|
@@ -10,7 +10,7 @@ export const browserHandlers = defineHandlers({
|
|
|
10
10
|
try {
|
|
11
11
|
const page = await browserManager.getOrCreateSessionPage(msg.sessionId);
|
|
12
12
|
const viewport = await page.evaluate('(() => ({ vw: window.innerWidth, vh: window.innerHeight }))()') as { vw: number; vh: number };
|
|
13
|
-
const scale = Math.min(
|
|
13
|
+
const scale = Math.min(SCREENCAST_WIDTH / viewport.vw, SCREENCAST_HEIGHT / viewport.vh);
|
|
14
14
|
const pageX = msg.x / scale;
|
|
15
15
|
const pageY = msg.y / scale;
|
|
16
16
|
const options: Record<string, unknown> = {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { applyGuardianDecision } from '../../approvals/guardian-decision-primitive.js';
|
|
2
|
+
import { getPendingApprovalForRequest } from '../../memory/channel-guardian-store.js';
|
|
3
|
+
import type { ApprovalAction } from '../../runtime/channel-approval-types.js';
|
|
4
|
+
import { handleChannelDecision } from '../../runtime/channel-approvals.js';
|
|
5
|
+
import * as pendingInteractions from '../../runtime/pending-interactions.js';
|
|
6
|
+
import { handleAccessRequestDecision } from '../../runtime/routes/access-request-decision.js';
|
|
7
|
+
import { listGuardianDecisionPrompts } from '../../runtime/routes/guardian-action-routes.js';
|
|
8
|
+
import type { GuardianActionDecision, GuardianActionsPendingRequest } from '../ipc-protocol.js';
|
|
9
|
+
import { defineHandlers, log } from './shared.js';
|
|
10
|
+
|
|
11
|
+
const VALID_ACTIONS = new Set<string>(['approve_once', 'approve_always', 'reject']);
|
|
12
|
+
|
|
13
|
+
export const guardianActionsHandlers = defineHandlers({
|
|
14
|
+
guardian_actions_pending_request: (msg: GuardianActionsPendingRequest, socket, ctx) => {
|
|
15
|
+
const prompts = listGuardianDecisionPrompts({ conversationId: msg.conversationId });
|
|
16
|
+
ctx.send(socket, { type: 'guardian_actions_pending_response', conversationId: msg.conversationId, prompts });
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
guardian_action_decision: (msg: GuardianActionDecision, socket, ctx) => {
|
|
20
|
+
// Validate the action is one of the known actions
|
|
21
|
+
if (!VALID_ACTIONS.has(msg.action)) {
|
|
22
|
+
log.warn({ requestId: msg.requestId, action: msg.action }, 'Invalid guardian action');
|
|
23
|
+
ctx.send(socket, {
|
|
24
|
+
type: 'guardian_action_decision_response',
|
|
25
|
+
applied: false,
|
|
26
|
+
reason: 'invalid_action',
|
|
27
|
+
requestId: msg.requestId,
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Try the channel guardian approval store first (tool approval prompts)
|
|
33
|
+
const approval = getPendingApprovalForRequest(msg.requestId);
|
|
34
|
+
if (approval) {
|
|
35
|
+
// Enforce conversationId scoping when provided.
|
|
36
|
+
if (msg.conversationId && msg.conversationId !== approval.conversationId) {
|
|
37
|
+
log.warn({ requestId: msg.requestId, expected: approval.conversationId, got: msg.conversationId }, 'conversationId mismatch');
|
|
38
|
+
ctx.send(socket, {
|
|
39
|
+
type: 'guardian_action_decision_response',
|
|
40
|
+
applied: false,
|
|
41
|
+
reason: 'conversation_mismatch',
|
|
42
|
+
requestId: msg.requestId,
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Access request approvals need a separate decision path — they don't have
|
|
48
|
+
// pending interactions and use verification sessions instead.
|
|
49
|
+
if (approval.toolName === 'ingress_access_request') {
|
|
50
|
+
const mappedAction = msg.action === 'reject' ? 'deny' as const : 'approve' as const;
|
|
51
|
+
// Use 'desktop' as the actor identity because this endpoint is
|
|
52
|
+
// unauthenticated — we cannot verify the caller is the assigned
|
|
53
|
+
// guardian, so we record a generic desktop origin instead of
|
|
54
|
+
// falsely attributing the decision to guardianExternalUserId.
|
|
55
|
+
const decisionResult = handleAccessRequestDecision(
|
|
56
|
+
approval,
|
|
57
|
+
mappedAction,
|
|
58
|
+
'desktop',
|
|
59
|
+
);
|
|
60
|
+
ctx.send(socket, {
|
|
61
|
+
type: 'guardian_action_decision_response',
|
|
62
|
+
applied: decisionResult.type !== 'stale',
|
|
63
|
+
requestId: msg.requestId,
|
|
64
|
+
reason: decisionResult.type === 'stale' ? 'stale' : undefined,
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const result = applyGuardianDecision({
|
|
70
|
+
approval,
|
|
71
|
+
decision: { action: msg.action as 'approve_once' | 'approve_always' | 'reject', source: 'plain_text', requestId: msg.requestId },
|
|
72
|
+
actorExternalUserId: undefined,
|
|
73
|
+
actorChannel: 'vellum',
|
|
74
|
+
});
|
|
75
|
+
ctx.send(socket, {
|
|
76
|
+
type: 'guardian_action_decision_response',
|
|
77
|
+
applied: result.applied,
|
|
78
|
+
reason: result.reason,
|
|
79
|
+
requestId: result.requestId ?? msg.requestId,
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Fall back to the pending interactions tracker (direct confirmation requests).
|
|
85
|
+
// Route through handleChannelDecision so approve_always properly persists trust rules.
|
|
86
|
+
const interaction = pendingInteractions.get(msg.requestId);
|
|
87
|
+
if (interaction) {
|
|
88
|
+
// Enforce conversationId scoping when provided.
|
|
89
|
+
if (msg.conversationId && msg.conversationId !== interaction.conversationId) {
|
|
90
|
+
log.warn({ requestId: msg.requestId, expected: interaction.conversationId, got: msg.conversationId }, 'conversationId mismatch');
|
|
91
|
+
ctx.send(socket, {
|
|
92
|
+
type: 'guardian_action_decision_response',
|
|
93
|
+
applied: false,
|
|
94
|
+
reason: 'conversation_mismatch',
|
|
95
|
+
requestId: msg.requestId,
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const result = handleChannelDecision(
|
|
101
|
+
interaction.conversationId,
|
|
102
|
+
{ action: msg.action as ApprovalAction, source: 'plain_text', requestId: msg.requestId },
|
|
103
|
+
);
|
|
104
|
+
ctx.send(socket, {
|
|
105
|
+
type: 'guardian_action_decision_response',
|
|
106
|
+
applied: result.applied,
|
|
107
|
+
requestId: result.requestId ?? msg.requestId,
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
log.warn({ requestId: msg.requestId }, 'No pending guardian action found for requestId');
|
|
113
|
+
ctx.send(socket, {
|
|
114
|
+
type: 'guardian_action_decision_response',
|
|
115
|
+
applied: false,
|
|
116
|
+
reason: 'not_found',
|
|
117
|
+
requestId: msg.requestId,
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
});
|
|
@@ -6,6 +6,7 @@ import type { ClientMessage } from '../ipc-protocol.js';
|
|
|
6
6
|
import { handleRideShotgunStart, handleRideShotgunStop } from '../ride-shotgun-handler.js';
|
|
7
7
|
import { handleWatchObservation } from '../watch-handler.js';
|
|
8
8
|
import { appHandlers } from './apps.js';
|
|
9
|
+
import { avatarHandlers } from './avatar.js';
|
|
9
10
|
import { browserHandlers } from './browser.js';
|
|
10
11
|
import { computerUseHandlers } from './computer-use.js';
|
|
11
12
|
import { configHandlers } from './config.js';
|
|
@@ -13,6 +14,7 @@ import { inboxInviteHandlers } from './config-inbox.js';
|
|
|
13
14
|
import { diagnosticsHandlers } from './diagnostics.js';
|
|
14
15
|
import { dictationHandlers } from './dictation.js';
|
|
15
16
|
import { documentHandlers } from './documents.js';
|
|
17
|
+
import { guardianActionsHandlers } from './guardian-actions.js';
|
|
16
18
|
import { homeBaseHandlers } from './home-base.js';
|
|
17
19
|
import { identityHandlers } from './identity.js';
|
|
18
20
|
import { miscHandlers } from './misc.js';
|
|
@@ -140,6 +142,7 @@ const handlers = {
|
|
|
140
142
|
...sessionHandlers,
|
|
141
143
|
...skillHandlers,
|
|
142
144
|
...appHandlers,
|
|
145
|
+
...avatarHandlers,
|
|
143
146
|
...configHandlers,
|
|
144
147
|
...computerUseHandlers,
|
|
145
148
|
...publishHandlers,
|
|
@@ -147,6 +150,7 @@ const handlers = {
|
|
|
147
150
|
...diagnosticsHandlers,
|
|
148
151
|
...miscHandlers,
|
|
149
152
|
...documentHandlers,
|
|
153
|
+
...guardianActionsHandlers,
|
|
150
154
|
...workItemHandlers,
|
|
151
155
|
...subagentHandlers,
|
|
152
156
|
...browserHandlers,
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
HistoryRequest,
|
|
25
25
|
MessageContentRequest,
|
|
26
26
|
RegenerateRequest,
|
|
27
|
+
ReorderThreadsRequest,
|
|
27
28
|
SandboxSetRequest,
|
|
28
29
|
SecretResponse,
|
|
29
30
|
ServerMessage,
|
|
@@ -547,6 +548,7 @@ export function handleSessionList(socket: net.Socket, ctx: HandlerContext, offse
|
|
|
547
548
|
const conversationIds = conversations.map((c) => c.id);
|
|
548
549
|
const bindings = externalConversationStore.getBindingsForConversations(conversationIds);
|
|
549
550
|
const attentionStates = getAttentionStateByConversationIds(conversationIds);
|
|
551
|
+
const displayMetas = conversationStore.getDisplayMetaForConversations(conversationIds);
|
|
550
552
|
ctx.send(socket, {
|
|
551
553
|
type: 'session_list_response',
|
|
552
554
|
sessions: conversations.map((c) => {
|
|
@@ -554,6 +556,7 @@ export function handleSessionList(socket: net.Socket, ctx: HandlerContext, offse
|
|
|
554
556
|
const originChannel = parseChannelId(c.originChannel);
|
|
555
557
|
const originInterface = parseInterfaceId(c.originInterface);
|
|
556
558
|
const attn = attentionStates.get(c.id);
|
|
559
|
+
const displayMeta = displayMetas.get(c.id);
|
|
557
560
|
const assistantAttention = attn ? {
|
|
558
561
|
hasUnseenLatestAssistantMessage: attn.latestAssistantMessageAt != null &&
|
|
559
562
|
(attn.lastSeenAssistantMessageAt == null || attn.lastSeenAssistantMessageAt < attn.latestAssistantMessageAt),
|
|
@@ -581,6 +584,8 @@ export function handleSessionList(socket: net.Socket, ctx: HandlerContext, offse
|
|
|
581
584
|
...(originChannel ? { conversationOriginChannel: originChannel } : {}),
|
|
582
585
|
...(originInterface ? { conversationOriginInterface: originInterface } : {}),
|
|
583
586
|
...(assistantAttention ? { assistantAttention } : {}),
|
|
587
|
+
...(displayMeta?.displayOrder != null ? { displayOrder: displayMeta.displayOrder } : {}),
|
|
588
|
+
...(displayMeta?.isPinned ? { isPinned: displayMeta.isPinned } : {}),
|
|
584
589
|
};
|
|
585
590
|
}),
|
|
586
591
|
hasMore: offset + conversations.length < totalCount,
|
|
@@ -1145,6 +1150,19 @@ export function handleMessageContentRequest(
|
|
|
1145
1150
|
});
|
|
1146
1151
|
}
|
|
1147
1152
|
|
|
1153
|
+
export function handleReorderThreads(
|
|
1154
|
+
msg: ReorderThreadsRequest,
|
|
1155
|
+
_socket: net.Socket,
|
|
1156
|
+
_ctx: HandlerContext,
|
|
1157
|
+
): void {
|
|
1158
|
+
if (!Array.isArray(msg.updates)) {
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
conversationStore.batchSetDisplayOrders(
|
|
1162
|
+
msg.updates.map((u) => ({ id: u.sessionId, displayOrder: u.displayOrder ?? null, isPinned: u.isPinned ?? false })),
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1148
1166
|
export const sessionHandlers = defineHandlers({
|
|
1149
1167
|
user_message: handleUserMessage,
|
|
1150
1168
|
confirmation_response: handleConfirmationResponse,
|
|
@@ -1163,4 +1181,5 @@ export const sessionHandlers = defineHandlers({
|
|
|
1163
1181
|
usage_request: handleUsageRequest,
|
|
1164
1182
|
sandbox_set: handleSandboxSet,
|
|
1165
1183
|
conversation_search: handleConversationSearch,
|
|
1184
|
+
reorder_threads: handleReorderThreads,
|
|
1166
1185
|
});
|
|
@@ -177,8 +177,10 @@ export function getScreenDimensions(): { width: number; height: number } {
|
|
|
177
177
|
if (cachedScreenDims) return cachedScreenDims;
|
|
178
178
|
if (process.platform !== 'darwin') return FALLBACK_SCREEN;
|
|
179
179
|
try {
|
|
180
|
+
// Use osascript (JXA) instead of `swift` to avoid the
|
|
181
|
+
// "Install Command Line Developer Tools" popup on fresh macOS installs.
|
|
180
182
|
const out = execSync(
|
|
181
|
-
`
|
|
183
|
+
`osascript -l JavaScript -e 'ObjC.import("AppKit"); var f = $.NSScreen.mainScreen.frame; Math.round(f.size.width) + "x" + Math.round(f.size.height)'`,
|
|
182
184
|
{ timeout: 10_000, encoding: 'utf-8' },
|
|
183
185
|
).trim();
|
|
184
186
|
const [w, h] = out.split('x').map(Number);
|