@sjhmars/happy-bridge 0.3.7 → 0.3.9
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/README.md +4 -4
- package/lib/client.js +10 -10
- package/lib/index.js +142 -114
- package/lib/types/bridge.d.ts +26 -20
- package/lib/types/catalogs.d.ts +12 -0
- package/lib/types/grant.d.ts +10 -0
- package/lib/types/history.d.ts +2 -1
- package/lib/types/inbound.d.ts +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
|
|
13
13
|
- 未配对:二维码 + 可复制的手机/网页链接。请在 **Happy App 里扫**,不要用系统相机。
|
|
14
14
|
- 已配对:显示已连接,可断开或点 **重新配对** 再出一张新二维码。断开只关掉 Happy 映射,网页 UI 还在。
|
|
15
|
-
- **手机能管多深**:只看 / 能聊 / 能批 / 完整。默认「能批」。改到 `danger-full-access
|
|
15
|
+
- **手机能管多深**:只看 / 能聊 / 能批 / 完整。默认「能批」。改到 `danger-full-access`、以及让手机换模型和思考强度同步到电脑输入框,都需要「完整」。电脑上换模型和思考强度不需要这个档。**真正发给模型的,永远是网页那一栏**;手机切档会先改这一栏,再发消息。两边都能切,会互相跟着变。网页自己发布到手机的回声(同一模型、同一档)不会反过来改网页。网页加了新模型后,已经同步到手机的旧对话名单也会更新。改档位立刻生效,不必重新配对。
|
|
16
16
|
|
|
17
17
|
手机 Happy App:
|
|
18
18
|
|
|
19
19
|
- 列表里看到本机普通会话(子 agent 不镜像)。侧栏里没点开的对话同样保持在线心跳。网页上的空白「新会话」占位(没发过第一条消息、网页未选中时会藏起来)不会出现在手机上;发出第一条之后才会镜像。在手机里给这样的对话发消息,会把它在电脑上唤醒(网页不必跟着切过去),并用这场对话上次用过的模型生成回复;没有记录就用网页默认模型。连着电脑时是在线;心跳断了会显示未在线。网页侧栏还在的对话会自动把心跳续上;手机点开这条未在线对话也会重新接通,不必先去网页点开。标题优先用网页会话名,没有就用第一条用户消息;空对话和网页一样叫「新会话」。列表按网页工作区分组,不会把 Windows 盘符路径堆成一坨。网页归档的对话会停心跳、进手机归档列表;从归档里打开或再发一条,网页侧栏会把这场对话显示回来。
|
|
20
|
-
- 打开空的 Happy
|
|
20
|
+
- 打开空的 Happy 会话时,会把网页里已有的用户/助手/工具记录补进去。之后网页新消息也会推到手机。网页第一条、以及从手机新建或唤醒一场对话时,会把思考强度写进网页模型栏(网页正在用的档,否则模型默认档),请求只读这一栏,避免第一段思考被写成正文。手机消息里的 `effort` / `effortLevel` 若真是换档(不是网页刚发布的回声),会先落到网页栏再发出。
|
|
21
21
|
- 新建:只能选已经在网页登记过的工作区。不会在磁盘上 mkdir。App 里「new worktree」会失败(故意不暴露 bash)。
|
|
22
22
|
- 发文字:已注册 `/命令` 走命令通道;否则当聊天。`/skill名` 仍当普通消息,由 harness 注入。
|
|
23
23
|
- 发图片:相册/拍照/粘贴后,会随下一条文字(也可以只发图)进同一场电脑对话,模型能看见图。`.java` / `.txt` 等普通文件会先下载到工作区 `happy-inbox/`,再走电脑原来的 `read` 工具读内容(和你在仓库里打开一个文件一样)。PDF、Word 这类不是纯文本的文件,电脑的 `read` 会拒绝,插件也不会自己拆文档。远程档至少「能聊」。输入框左边应有图片按钮;发出后电脑网页会出现图片,手机会转圈等回复。电脑网页贴的图也会按同一套加密上传推到手机。
|
|
24
|
-
- 工具批准、计划审阅、选择题:和网页黄框/提问框赛跑,谁先点谁算。手机点「始终允许」只在这次网页进程里记住。选择题走 Happy
|
|
24
|
+
- 工具批准、计划审阅、选择题:和网页黄框/提问框赛跑,谁先点谁算。手机点「始终允许」只在这次网页进程里记住。选择题走 Happy 表单通道,首题末尾带一行提示「自定义回答:直接在下方聊天输入框输入并发送」——想选就点选项提交,想自由回答就直接打字发送,那条文本会作为答案并回显到卡片。**网页上填自定义答案或点选项,也会把同一份答案写回手机卡片**,不会再把手机表单标成「取消」(取消会让 App 走进输入框自定义流程,把网页模型栏锁回目录第一项 DeepSeek V4)。旧版 App 没有这条通道,见 `questionChannel`。提问还在等的时候,直接用输入框打字也等于自己填;审阅计划时打字会关掉审阅,这句话当普通聊天发出去。批完/拒完的卡片会停在「已批准/已拒绝」状态,按钮失效。远程档至少「能批」才能从手机点。
|
|
25
25
|
- 电脑正在生成时,手机可以点停止(Happy App 的 Stop)。远程档至少「能聊」。只看档点了不会停。
|
|
26
26
|
- 助手回复按整段推到手机(生成中会转圈)。思考收成一行 **Think · 摘要** 的 Note 卡片,点进去看全文。工具用 Happy 认识的名字(Grep / Read / Bash),描述跟网页那一行摘要一样。Happy 没有网页同款图标。
|
|
27
27
|
|
|
@@ -72,7 +72,7 @@ pnpm dsh web --port 3080
|
|
|
72
72
|
questionChannel: communications
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
`remoteGrant`:`watch` | `chat` | `approve` | `full`。设置页下拉框会写入这份配置。会话里也可以 `/remote approve
|
|
75
|
+
`remoteGrant`:`watch` | `chat` | `approve` | `full`。设置页下拉框会写入这份配置。会话里也可以 `/remote approve`(只改当前进程,不写设置)。档位「完整」时,手机换模型和思考强度会先 `await` 电脑同一条 `session.selectModel`(网页模型栏和推理等级跟着变),再发下一条。电脑自己换模型和思考强度同样会写回手机。发给模型的请求只读网页栏,不会另走一套手机记忆。网页加了新模型后,已镜像的手机会话会重新收到完整名单。
|
|
76
76
|
|
|
77
77
|
`questionChannel`:`communications`(默认)| `permission`。选择题走的 Happy 通道。`communications` 是 App 的表单通道:答完卡片常驻显示你选的答案,会话列表也会显示「等回答」;需要较新版 Happy App。`permission` 是旧通道:题目选项仍可见可答,答完卡片不回显。两条通道都没有内联输入框——自由回答都是直接在聊天输入框打字发送(表单首题末尾有提示行;`communications` 下零选项的题目会弹 App 全屏表单,那里有真正的输入框)。
|
|
78
78
|
|
package/lib/client.js
CHANGED
|
@@ -17,21 +17,21 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
document.head.appendChild(tag);
|
|
18
18
|
}
|
|
19
19
|
var HappyBridgeCard_module_css_default = {
|
|
20
|
-
"headText": "l9rBEq_headText",
|
|
21
|
-
"name": "l9rBEq_name",
|
|
22
20
|
"card": "l9rBEq_card",
|
|
23
|
-
"status": "l9rBEq_status",
|
|
24
21
|
"description": "l9rBEq_description",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"button": "l9rBEq_button",
|
|
29
|
-
"qr": "l9rBEq_qr",
|
|
22
|
+
"status": "l9rBEq_status",
|
|
23
|
+
"headText": "l9rBEq_headText",
|
|
24
|
+
"name": "l9rBEq_name",
|
|
30
25
|
"body": "l9rBEq_body",
|
|
31
26
|
"checkbox": "l9rBEq_checkbox",
|
|
32
|
-
"
|
|
27
|
+
"error": "l9rBEq_error",
|
|
33
28
|
"row": "l9rBEq_row",
|
|
34
|
-
"
|
|
29
|
+
"label": "l9rBEq_label",
|
|
30
|
+
"chevron": "l9rBEq_chevron",
|
|
31
|
+
"select": "l9rBEq_select",
|
|
32
|
+
"qr": "l9rBEq_qr",
|
|
33
|
+
"header": "l9rBEq_header",
|
|
34
|
+
"button": "l9rBEq_button"
|
|
35
35
|
};
|
|
36
36
|
//#endregion
|
|
37
37
|
//#region src/client/HappyBridgeCard.tsx
|
package/lib/index.js
CHANGED
|
@@ -247,7 +247,8 @@ async function buildSessionMetadata(ctx, source, machineId, title, selection, _g
|
|
|
247
247
|
value: name,
|
|
248
248
|
description: name === "danger-full-access" ? "不限制文件,且不再询问批准" : "只能改当前工作区,危险工具要批准"
|
|
249
249
|
}));
|
|
250
|
-
const selected =
|
|
250
|
+
const selected = resolvePublishedSelection(models, selection);
|
|
251
|
+
const publishedModels = selected === void 0 || models.some((model) => model.providerId === selected.row.providerId && model.id === selected.row.id) ? models : [selected.row, ...models];
|
|
251
252
|
const thoughtLevels = selected?.row.effortOptions;
|
|
252
253
|
const thought = selected === void 0 ? void 0 : selected.effort ?? selected.row.defaultThinkingLevel;
|
|
253
254
|
return {
|
|
@@ -271,7 +272,7 @@ async function buildSessionMetadata(ctx, source, machineId, title, selection, _g
|
|
|
271
272
|
happyToolsDir: VIRTUAL_HOME,
|
|
272
273
|
slashCommands,
|
|
273
274
|
skills,
|
|
274
|
-
models,
|
|
275
|
+
models: publishedModels,
|
|
275
276
|
operatingModes,
|
|
276
277
|
client: {
|
|
277
278
|
id: "rig",
|
|
@@ -329,17 +330,47 @@ async function buildSessionMetadata(ctx, source, machineId, title, selection, _g
|
|
|
329
330
|
}
|
|
330
331
|
};
|
|
331
332
|
}
|
|
332
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Pick the Happy catalog row that matches Host selection.
|
|
335
|
+
* A known Host pick that is missing from the listed rows stays that pick
|
|
336
|
+
* (synthetic row) instead of falling back to `models[0]` (usually DeepSeek V4).
|
|
337
|
+
* @param models - rows advertised to the App.
|
|
338
|
+
* @param selection - current Host provider/model/effort, when known.
|
|
339
|
+
* @returns the row to publish, plus effort when the Host named one.
|
|
340
|
+
*/
|
|
341
|
+
function resolvePublishedSelection(models, selection) {
|
|
333
342
|
if (selection !== void 0) {
|
|
334
343
|
const row = models.find((model) => model.providerId === selection.provider && model.id === selection.model) ?? models.find((model) => model.code === `${selection.provider}/${selection.model}`);
|
|
335
344
|
if (row !== void 0) return {
|
|
336
345
|
row,
|
|
337
346
|
...selection.reasoningEffort === void 0 ? {} : { effort: selection.reasoningEffort }
|
|
338
347
|
};
|
|
348
|
+
return {
|
|
349
|
+
row: syntheticHappyRow(selection),
|
|
350
|
+
...selection.reasoningEffort === void 0 ? {} : { effort: selection.reasoningEffort }
|
|
351
|
+
};
|
|
339
352
|
}
|
|
340
353
|
const first = models[0];
|
|
341
354
|
return first === void 0 ? void 0 : { row: first };
|
|
342
355
|
}
|
|
356
|
+
function syntheticHappyRow(selection) {
|
|
357
|
+
const effort = selection.reasoningEffort;
|
|
358
|
+
return {
|
|
359
|
+
code: `${selection.provider}/${selection.model}`,
|
|
360
|
+
value: selection.model,
|
|
361
|
+
id: selection.model,
|
|
362
|
+
name: selection.model,
|
|
363
|
+
providerId: selection.provider,
|
|
364
|
+
providerKind: "custom",
|
|
365
|
+
providerName: selection.provider,
|
|
366
|
+
thinkingLevels: effort === void 0 ? [] : [effort],
|
|
367
|
+
effortOptions: effort === void 0 ? [] : [{
|
|
368
|
+
code: effort,
|
|
369
|
+
value: effort
|
|
370
|
+
}],
|
|
371
|
+
...effort === void 0 ? {} : { defaultThinkingLevel: effort }
|
|
372
|
+
};
|
|
373
|
+
}
|
|
343
374
|
function listCommands(ctx, agent) {
|
|
344
375
|
const commands = ctx.get("commands");
|
|
345
376
|
if (commands === void 0) return [];
|
|
@@ -1105,6 +1136,22 @@ function sameCatalogPick(previous, next) {
|
|
|
1105
1136
|
if (previous === void 0) return false;
|
|
1106
1137
|
return catalogPickKey(previous) === catalogPickKey(next);
|
|
1107
1138
|
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Whether inbound `meta.model` is the pick we last published.
|
|
1141
|
+
* The App often echoes only `currentModelCode` (bare id) on a chat line,
|
|
1142
|
+
* while we publish `provider:id`; treating that as a user switch would
|
|
1143
|
+
* lock the web composer onto the catalog default (DeepSeek V4).
|
|
1144
|
+
* @param published - last pick we wrote, if any.
|
|
1145
|
+
* @param inbound - model code from the inbound message.
|
|
1146
|
+
* @returns true when the inbound model is our own echo.
|
|
1147
|
+
*/
|
|
1148
|
+
function isPublishedModelEcho(published, inbound) {
|
|
1149
|
+
if (published?.model === void 0) return false;
|
|
1150
|
+
if (sameCatalogPick({ model: published.model }, { model: inbound })) return true;
|
|
1151
|
+
const publishedSplit = splitModelCode(published.model);
|
|
1152
|
+
const inboundSplit = splitModelCode(inbound);
|
|
1153
|
+
return inboundSplit.provider === "" && inboundSplit.model === publishedSplit.model;
|
|
1154
|
+
}
|
|
1108
1155
|
function catalogPickKey(pick) {
|
|
1109
1156
|
const split = pick.model === void 0 ? void 0 : splitModelCode(pick.model);
|
|
1110
1157
|
return `${split === void 0 ? "" : split.provider === "" ? split.model : `${split.provider}/${split.model}`}\0${pick.effort === void 0 ? "" : pick.effort === null ? "null" : pick.effort}`;
|
|
@@ -1566,9 +1613,10 @@ function wakeModelSelection(primary, ...fallbacks) {
|
|
|
1566
1613
|
};
|
|
1567
1614
|
}
|
|
1568
1615
|
/**
|
|
1569
|
-
* Effort the first phone-spawn / phone-wake request should send.
|
|
1616
|
+
* Effort the first web / phone-spawn / phone-wake request should send.
|
|
1570
1617
|
* Current pick wins; otherwise a preferred (web) value or the model's
|
|
1571
1618
|
* advertised default, but only when the model lists that id.
|
|
1619
|
+
* Written onto the Host picker via `selectModel`; requests then read that bar.
|
|
1572
1620
|
* @param currentEffort - already chosen effort, if any.
|
|
1573
1621
|
* @param preferred - web picker effort to reuse when the model accepts it.
|
|
1574
1622
|
* @param modelDefault - `resolveModelInfo().reasoning.defaultEffort`.
|
|
@@ -1767,6 +1815,18 @@ function mergeCustomAnswers(questions, text, deferred) {
|
|
|
1767
1815
|
});
|
|
1768
1816
|
}
|
|
1769
1817
|
/**
|
|
1818
|
+
* Translate a web-side harness answer batch into Happy communications answers
|
|
1819
|
+
* so the phone card shows the same choice instead of a cancelled form.
|
|
1820
|
+
* @param answers - harness answer rows from the web composer.
|
|
1821
|
+
* @returns answers keyed by question id.
|
|
1822
|
+
*/
|
|
1823
|
+
function communicationAnswersFromWeb(answers) {
|
|
1824
|
+
return Object.fromEntries(answers.map((row) => [row.id, {
|
|
1825
|
+
options: row.selected,
|
|
1826
|
+
...row.custom === void 0 || row.custom === "" ? {} : { custom: row.custom }
|
|
1827
|
+
}]));
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1770
1830
|
* Translate Happy communications answers (`{ [question id]: { options, custom } }`)
|
|
1771
1831
|
* into harness answer rows. Options become `selected`; free text becomes `custom`.
|
|
1772
1832
|
* @param answers - communication RPC answers keyed by question id.
|
|
@@ -2761,8 +2821,6 @@ var HappyBridge = class {
|
|
|
2761
2821
|
lastPublished = /* @__PURE__ */ new Map();
|
|
2762
2822
|
/** Count of in-flight phone-originated Host `selectModel` calls. */
|
|
2763
2823
|
hostSelectFromPhone = 0;
|
|
2764
|
-
/** Per-agent selection installed on phone wake / spawn, matching Host `selectionFor`. */
|
|
2765
|
-
selections = /* @__PURE__ */ new WeakMap();
|
|
2766
2824
|
/** In-flight phone wakes, so two inbound texts do not double-resume. */
|
|
2767
2825
|
waking = /* @__PURE__ */ new Map();
|
|
2768
2826
|
/** Phone stop-session / archive: do not recreate these Happy rows. */
|
|
@@ -2773,6 +2831,10 @@ var HappyBridge = class {
|
|
|
2773
2831
|
lastArchivedIds = /* @__PURE__ */ new Set();
|
|
2774
2832
|
/** Serialize phone text so two messages cannot split one attachment batch. */
|
|
2775
2833
|
inboundTail = /* @__PURE__ */ new Map();
|
|
2834
|
+
/** One Host-picker pin per live Agent, awaited before the first prompt assembly. */
|
|
2835
|
+
hostPin = /* @__PURE__ */ new WeakMap();
|
|
2836
|
+
/** Agents that already prepend the pin onto `system-prompt/assemble`. */
|
|
2837
|
+
assemblePinBound = /* @__PURE__ */ new WeakSet();
|
|
2776
2838
|
error;
|
|
2777
2839
|
running = false;
|
|
2778
2840
|
scanTimer;
|
|
@@ -2957,6 +3019,7 @@ var HappyBridge = class {
|
|
|
2957
3019
|
this.wakePhone(existing, true);
|
|
2958
3020
|
return;
|
|
2959
3021
|
}
|
|
3022
|
+
this.watchFirstAssemble(agent);
|
|
2960
3023
|
if (isBlankSession(agent.session.events) && !this.phoneSpawned.has(agent.id)) return;
|
|
2961
3024
|
this.mirrorAgent(agent).then(() => {
|
|
2962
3025
|
if (this.isHarnessArchived(agent.id)) this.parkPhoneSession(agent.id);
|
|
@@ -3034,7 +3097,7 @@ var HappyBridge = class {
|
|
|
3034
3097
|
text: "当前没有已选模型"
|
|
3035
3098
|
};
|
|
3036
3099
|
if (id === "") {
|
|
3037
|
-
this.rememberModel(invocation.agent, {
|
|
3100
|
+
await this.rememberModel(invocation.agent, {
|
|
3038
3101
|
provider: current.provider,
|
|
3039
3102
|
model: current.model
|
|
3040
3103
|
});
|
|
@@ -3043,7 +3106,7 @@ var HappyBridge = class {
|
|
|
3043
3106
|
text: "推理档已恢复为模型默认"
|
|
3044
3107
|
};
|
|
3045
3108
|
}
|
|
3046
|
-
this.rememberModel(invocation.agent, {
|
|
3109
|
+
await this.rememberModel(invocation.agent, {
|
|
3047
3110
|
provider: current.provider,
|
|
3048
3111
|
model: current.model,
|
|
3049
3112
|
reasoningEffort: ReasoningEffortId(id)
|
|
@@ -3198,6 +3261,7 @@ var HappyBridge = class {
|
|
|
3198
3261
|
if (session.header.id !== link.dshId) return;
|
|
3199
3262
|
this.onSessionEvent(link, event);
|
|
3200
3263
|
}, { global: true });
|
|
3264
|
+
this.watchFirstAssemble(agent);
|
|
3201
3265
|
}
|
|
3202
3266
|
/** Copy log events newer than {@link Link.lastForwardedSeq} onto Happy. */
|
|
3203
3267
|
drainNewEvents(link, agent) {
|
|
@@ -3253,14 +3317,15 @@ var HappyBridge = class {
|
|
|
3253
3317
|
resumeSessionId: SessionId(link.dshId),
|
|
3254
3318
|
setup
|
|
3255
3319
|
});
|
|
3256
|
-
await this.
|
|
3320
|
+
await this.watchFirstAssemble(handle.agent);
|
|
3257
3321
|
const current = this.links.get(link.dshId);
|
|
3258
3322
|
if (current !== void 0) this.attachAgent(current, handle.agent);
|
|
3259
3323
|
return handle.agent;
|
|
3260
3324
|
}
|
|
3261
3325
|
/**
|
|
3262
|
-
* Resume/create composition matching Host `composeAgent`:
|
|
3263
|
-
* selection
|
|
3326
|
+
* Resume/create composition matching Host `composeAgent`: mount the preset
|
|
3327
|
+
* when a roster exists. Model selection is the Host picker, landed by
|
|
3328
|
+
* `selectModel` in {@link watchFirstAssemble} / {@link rememberModel}.
|
|
3264
3329
|
* @param presetHint - logged or requested preset id; omitted uses the roster default.
|
|
3265
3330
|
*/
|
|
3266
3331
|
async composeAgentSetup(presetHint) {
|
|
@@ -3268,71 +3333,9 @@ var HappyBridge = class {
|
|
|
3268
3333
|
let resolvedId;
|
|
3269
3334
|
if (presets !== void 0) resolvedId = (await presets.resolve(presetHint)).id;
|
|
3270
3335
|
return async (agentCtx) => {
|
|
3271
|
-
this.installWakeSelection(agentCtx);
|
|
3272
3336
|
if (presets !== void 0 && resolvedId !== void 0) await presets.mount(agentCtx, resolvedId);
|
|
3273
3337
|
};
|
|
3274
3338
|
}
|
|
3275
|
-
/**
|
|
3276
|
-
* Same lazy selection Host `selectionFor` installs: remembered pick, else
|
|
3277
|
-
* the session's last `request/header`, else `agentDefaultModel`. A missing
|
|
3278
|
-
* thinking level keeps the web picker's effort when it is the same model.
|
|
3279
|
-
* Unlike Host `installModelSelection`, an absent effort does not clear
|
|
3280
|
-
* inherited thinking.
|
|
3281
|
-
*/
|
|
3282
|
-
installWakeSelection(agentCtx) {
|
|
3283
|
-
const agent = agentCtx.agent;
|
|
3284
|
-
if (agent === void 0) throw new Error("happy-bridge: agent setup has no scoped agent");
|
|
3285
|
-
if (this.selections.has(agent)) return;
|
|
3286
|
-
let picked;
|
|
3287
|
-
const bridge = this;
|
|
3288
|
-
const selection = {
|
|
3289
|
-
get current() {
|
|
3290
|
-
if (picked !== void 0) return picked;
|
|
3291
|
-
return bridge.currentModel(agent);
|
|
3292
|
-
},
|
|
3293
|
-
set current(next) {
|
|
3294
|
-
picked = next;
|
|
3295
|
-
},
|
|
3296
|
-
assembled: void 0
|
|
3297
|
-
};
|
|
3298
|
-
this.bindWakeSelection(agentCtx, selection);
|
|
3299
|
-
this.selections.set(agent, selection);
|
|
3300
|
-
const current = selection.current;
|
|
3301
|
-
if (current === void 0) return;
|
|
3302
|
-
const existing = this.models.get(agent.id);
|
|
3303
|
-
if (existing === void 0 || existing.reasoningEffort === void 0 && current.reasoningEffort !== void 0) this.models.set(agent.id, current);
|
|
3304
|
-
}
|
|
3305
|
-
/**
|
|
3306
|
-
* Pin provider/model for a phone-woken agent without wiping thinking when
|
|
3307
|
-
* the selection names no effort.
|
|
3308
|
-
*/
|
|
3309
|
-
bindWakeSelection(agentCtx, selection) {
|
|
3310
|
-
agentCtx.on("system-prompt/assemble", async (_assembly, _context, next) => {
|
|
3311
|
-
const selected = selection.current;
|
|
3312
|
-
const assembled = await next();
|
|
3313
|
-
selection.assembled = selected;
|
|
3314
|
-
if (selected === void 0) return assembled;
|
|
3315
|
-
return {
|
|
3316
|
-
...assembled,
|
|
3317
|
-
variables: {
|
|
3318
|
-
...assembled.variables,
|
|
3319
|
-
provider: selected.provider,
|
|
3320
|
-
model: selected.model
|
|
3321
|
-
}
|
|
3322
|
-
};
|
|
3323
|
-
});
|
|
3324
|
-
agentCtx.on("agent/request", async (_payload, next) => {
|
|
3325
|
-
const resolved = await next();
|
|
3326
|
-
const selected = selection.assembled;
|
|
3327
|
-
if (selected === void 0) return resolved;
|
|
3328
|
-
return {
|
|
3329
|
-
...resolved,
|
|
3330
|
-
provider: selected.provider,
|
|
3331
|
-
model: selected.model,
|
|
3332
|
-
...selected.reasoningEffort === void 0 ? {} : { reasoningEffort: selected.reasoningEffort }
|
|
3333
|
-
};
|
|
3334
|
-
});
|
|
3335
|
-
}
|
|
3336
3339
|
/** Host default model, when the web profile mounted `agentDefaultModel`. */
|
|
3337
3340
|
defaultModelSelection() {
|
|
3338
3341
|
return this.ctx.get("agentDefaultModel")?.currentSelection();
|
|
@@ -3421,7 +3424,7 @@ var HappyBridge = class {
|
|
|
3421
3424
|
onAbort: () => this.onPhoneAbort(agent.id),
|
|
3422
3425
|
onArchived: () => this.onPhoneArchive(sessionId, agent.id),
|
|
3423
3426
|
onResumed: () => this.onPhoneRestore(agent.id),
|
|
3424
|
-
onCatalog: (meta) => this.
|
|
3427
|
+
onCatalog: (meta) => this.queuePhoneCatalog(agent.id, meta),
|
|
3425
3428
|
log: this.log
|
|
3426
3429
|
});
|
|
3427
3430
|
try {
|
|
@@ -3455,6 +3458,7 @@ var HappyBridge = class {
|
|
|
3455
3458
|
};
|
|
3456
3459
|
this.links.set(agent.id, link);
|
|
3457
3460
|
this.happyToDsh.set(sessionId, agent.id);
|
|
3461
|
+
await this.watchFirstAssemble(agent);
|
|
3458
3462
|
await this.pushMetadata(link);
|
|
3459
3463
|
if (seq === 0) await this.replayHistory(link);
|
|
3460
3464
|
this.log(`已镜像会话 ${agent.id} → Happy ${sessionId}(seq=${seq})`);
|
|
@@ -3490,7 +3494,7 @@ var HappyBridge = class {
|
|
|
3490
3494
|
onAbort: () => this.onPhoneAbort(dshId),
|
|
3491
3495
|
onArchived: () => this.onPhoneArchive(created.id, dshId),
|
|
3492
3496
|
onResumed: () => this.onPhoneRestore(dshId),
|
|
3493
|
-
onCatalog: (meta) => this.
|
|
3497
|
+
onCatalog: (meta) => this.queuePhoneCatalog(dshId, meta),
|
|
3494
3498
|
log: this.log
|
|
3495
3499
|
});
|
|
3496
3500
|
try {
|
|
@@ -3695,8 +3699,8 @@ var HappyBridge = class {
|
|
|
3695
3699
|
});
|
|
3696
3700
|
await (this.ctx.get("workspaceRegistry")?.list().find((item) => item.path === real))?.attachSession(sessionId);
|
|
3697
3701
|
this.phoneSpawned.add(handle.agent.id);
|
|
3698
|
-
this.applySpawnMeta(handle.agent, options);
|
|
3699
|
-
await this.
|
|
3702
|
+
await this.applySpawnMeta(handle.agent, options);
|
|
3703
|
+
await this.watchFirstAssemble(handle.agent);
|
|
3700
3704
|
await this.mirrorAgent(handle.agent, options.sessionId);
|
|
3701
3705
|
return {
|
|
3702
3706
|
type: "success",
|
|
@@ -3709,15 +3713,20 @@ var HappyBridge = class {
|
|
|
3709
3713
|
};
|
|
3710
3714
|
}
|
|
3711
3715
|
}
|
|
3712
|
-
applySpawnMeta(agent, options) {
|
|
3713
|
-
if (options.modelMode !== void 0) this.applyModel(agent, options.modelMode, options.effortLevel);
|
|
3714
|
-
else if (options.effortLevel !== void 0) this.applyEffort(agent, options.effortLevel);
|
|
3716
|
+
async applySpawnMeta(agent, options) {
|
|
3717
|
+
if (options.modelMode !== void 0) await this.applyModel(agent, options.modelMode, options.effortLevel);
|
|
3718
|
+
else if (options.effortLevel !== void 0) await this.applyEffort(agent, options.effortLevel);
|
|
3715
3719
|
if (options.permissionMode !== void 0) this.applyPermission(agent, options.permissionMode, true);
|
|
3716
3720
|
}
|
|
3717
3721
|
queueInbound(dshId, message) {
|
|
3718
3722
|
const next = (this.inboundTail.get(dshId) ?? Promise.resolve()).then(() => this.onInbound(dshId, message), () => this.onInbound(dshId, message));
|
|
3719
3723
|
this.inboundTail.set(dshId, next);
|
|
3720
3724
|
}
|
|
3725
|
+
/** Same queue as inbound text, so a catalog switch lands on the bar before the next send. */
|
|
3726
|
+
queuePhoneCatalog(dshId, meta) {
|
|
3727
|
+
const next = (this.inboundTail.get(dshId) ?? Promise.resolve()).then(() => this.applyPhoneCatalog(dshId, meta), () => this.applyPhoneCatalog(dshId, meta));
|
|
3728
|
+
this.inboundTail.set(dshId, next);
|
|
3729
|
+
}
|
|
3721
3730
|
async onInbound(dshId, message) {
|
|
3722
3731
|
const link = this.links.get(dshId);
|
|
3723
3732
|
if (link === void 0 || link.replaying) return;
|
|
@@ -3767,7 +3776,6 @@ var HappyBridge = class {
|
|
|
3767
3776
|
return;
|
|
3768
3777
|
}
|
|
3769
3778
|
const { agent } = ensured;
|
|
3770
|
-
await this.ensurePinnedEffort(agent);
|
|
3771
3779
|
const names = new Set((this.ctx.get("commands")?.list(agent) ?? []).map((command) => command.name));
|
|
3772
3780
|
const kind = classifyInboundText(message.text, names);
|
|
3773
3781
|
await this.applyMessageMeta(current, agent, message.meta, kind === "command");
|
|
@@ -3789,24 +3797,24 @@ var HappyBridge = class {
|
|
|
3789
3797
|
const effort = messageEffort(meta);
|
|
3790
3798
|
const permissionMode = meta.permissionMode;
|
|
3791
3799
|
const published = this.lastPublished.get(agent.id);
|
|
3792
|
-
const modelIsEcho = model !== void 0 &&
|
|
3800
|
+
const modelIsEcho = model !== void 0 && isPublishedModelEcho(published, model);
|
|
3793
3801
|
const effortIsEcho = effort !== void 0 && published?.effort != null && effort === published.effort;
|
|
3794
3802
|
if (model !== void 0 && !modelIsEcho) {
|
|
3795
3803
|
if (!grantAtLeast(this.config.remoteGrant, "full")) link.socket.sendText("service", "改模型需要远程档「完整」,这条消息仍会发出。");
|
|
3796
|
-
else this.applyModel(agent, model, typeof effort === "string" ? effort : effort === null ? null : void 0);
|
|
3804
|
+
else await this.applyModel(agent, model, typeof effort === "string" ? effort : effort === null ? null : void 0);
|
|
3797
3805
|
} else if ((effort === null || typeof effort === "string") && !effortIsEcho) {
|
|
3798
3806
|
if (!grantAtLeast(this.config.remoteGrant, "full")) link.socket.sendText("service", "改思考强度需要远程档「完整」,这条消息仍会发出。");
|
|
3799
3807
|
else if (effort === null) {
|
|
3800
3808
|
const current = this.currentModel(agent);
|
|
3801
|
-
if (current !== void 0) this.rememberModel(agent, {
|
|
3809
|
+
if (current !== void 0) await this.rememberModel(agent, {
|
|
3802
3810
|
provider: current.provider,
|
|
3803
3811
|
model: current.model
|
|
3804
3812
|
});
|
|
3805
|
-
} else this.applyEffort(agent, effort);
|
|
3813
|
+
} else await this.applyEffort(agent, effort);
|
|
3806
3814
|
}
|
|
3807
3815
|
if (typeof permissionMode === "string" && permissionMode !== "") this.applyPermission(agent, permissionMode, isCommand);
|
|
3808
3816
|
}
|
|
3809
|
-
applyPhoneCatalog(dshId, meta) {
|
|
3817
|
+
async applyPhoneCatalog(dshId, meta) {
|
|
3810
3818
|
if (!grantAtLeast(this.config.remoteGrant, "full")) return;
|
|
3811
3819
|
const pick = catalogModelPick(meta);
|
|
3812
3820
|
if (pick === void 0) return;
|
|
@@ -3817,20 +3825,20 @@ var HappyBridge = class {
|
|
|
3817
3825
|
const agent = link === void 0 ? void 0 : this.requireAgent(link);
|
|
3818
3826
|
if (agent === void 0) return;
|
|
3819
3827
|
if (pick.model !== void 0) {
|
|
3820
|
-
this.applyModel(agent, pick.model, pick.effort);
|
|
3828
|
+
await this.applyModel(agent, pick.model, pick.effort);
|
|
3821
3829
|
return;
|
|
3822
3830
|
}
|
|
3823
3831
|
if (pick.effort === null) {
|
|
3824
3832
|
const current = this.currentModel(agent);
|
|
3825
|
-
if (current !== void 0) this.rememberModel(agent, {
|
|
3833
|
+
if (current !== void 0) await this.rememberModel(agent, {
|
|
3826
3834
|
provider: current.provider,
|
|
3827
3835
|
model: current.model
|
|
3828
3836
|
});
|
|
3829
3837
|
return;
|
|
3830
3838
|
}
|
|
3831
|
-
if (typeof pick.effort === "string") this.applyEffort(agent, pick.effort);
|
|
3839
|
+
if (typeof pick.effort === "string") await this.applyEffort(agent, pick.effort);
|
|
3832
3840
|
}
|
|
3833
|
-
applyModel(agent, code, effort) {
|
|
3841
|
+
async applyModel(agent, code, effort) {
|
|
3834
3842
|
const split = splitModelCode(code);
|
|
3835
3843
|
const current = this.currentModel(agent);
|
|
3836
3844
|
const provider = split.provider === "" ? current?.provider ?? "" : split.provider;
|
|
@@ -3842,24 +3850,25 @@ var HappyBridge = class {
|
|
|
3842
3850
|
model: split.model,
|
|
3843
3851
|
...reasoningEffort === void 0 ? {} : { reasoningEffort }
|
|
3844
3852
|
};
|
|
3845
|
-
this.rememberModel(agent, next);
|
|
3853
|
+
await this.rememberModel(agent, next);
|
|
3846
3854
|
}
|
|
3847
|
-
applyEffort(agent, effort) {
|
|
3855
|
+
async applyEffort(agent, effort) {
|
|
3848
3856
|
const current = this.currentModel(agent);
|
|
3849
3857
|
if (current === void 0) return;
|
|
3850
|
-
this.rememberModel(agent, {
|
|
3858
|
+
await this.rememberModel(agent, {
|
|
3851
3859
|
...current,
|
|
3852
3860
|
reasoningEffort: ReasoningEffortId(effort)
|
|
3853
3861
|
});
|
|
3854
3862
|
}
|
|
3855
|
-
/**
|
|
3856
|
-
|
|
3857
|
-
|
|
3863
|
+
/**
|
|
3864
|
+
* Remember a phone-originated pick and land it on the Host picker before
|
|
3865
|
+
* the next request. `onHostModelSelected` is skipped while this runs, so
|
|
3866
|
+
* {@link models} is written here.
|
|
3867
|
+
*/
|
|
3868
|
+
async rememberModel(agent, next) {
|
|
3869
|
+
if (sameModelOverride(this.models.get(agent.id), next)) return;
|
|
3858
3870
|
this.models.set(agent.id, next);
|
|
3859
|
-
|
|
3860
|
-
if (selection !== void 0) selection.current = next;
|
|
3861
|
-
if (sameModelOverride(previous, next)) return;
|
|
3862
|
-
this.syncHostSelection(agent, next);
|
|
3871
|
+
await this.syncHostSelection(agent, next);
|
|
3863
3872
|
}
|
|
3864
3873
|
/**
|
|
3865
3874
|
* Write the web picker's Host selection (`sessionController.selectModel`) so
|
|
@@ -3898,16 +3907,36 @@ var HappyBridge = class {
|
|
|
3898
3907
|
};
|
|
3899
3908
|
this.models.set(sessionId, next);
|
|
3900
3909
|
const link = this.links.get(sessionId);
|
|
3901
|
-
const agent = link?.agent;
|
|
3902
|
-
if (agent !== void 0) {
|
|
3903
|
-
const current = this.selections.get(agent);
|
|
3904
|
-
if (current !== void 0) current.current = next;
|
|
3905
|
-
}
|
|
3906
3910
|
if (link !== void 0 && !link.parked) this.pushMetadata(link);
|
|
3907
3911
|
}
|
|
3908
3912
|
/**
|
|
3909
|
-
*
|
|
3910
|
-
*
|
|
3913
|
+
* Land explicit effort on the Host picker and wait for that pin before this
|
|
3914
|
+
* Agent's first prompt assembly, so the request reads the web bar.
|
|
3915
|
+
* @param agent - live agent whose picker should carry explicit effort.
|
|
3916
|
+
*/
|
|
3917
|
+
watchFirstAssemble(agent) {
|
|
3918
|
+
const pending = this.pinHostPicker(agent);
|
|
3919
|
+
if (this.assemblePinBound.has(agent)) return pending;
|
|
3920
|
+
this.assemblePinBound.add(agent);
|
|
3921
|
+
agent.ctx.on("system-prompt/assemble", async (_assembly, _context, next) => {
|
|
3922
|
+
await this.pinHostPicker(agent);
|
|
3923
|
+
return next();
|
|
3924
|
+
}, { prepend: true });
|
|
3925
|
+
return pending;
|
|
3926
|
+
}
|
|
3927
|
+
pinHostPicker(agent) {
|
|
3928
|
+
const existing = this.hostPin.get(agent);
|
|
3929
|
+
if (existing !== void 0) return existing;
|
|
3930
|
+
const pending = this.ensurePinnedEffort(agent);
|
|
3931
|
+
this.hostPin.set(agent, pending);
|
|
3932
|
+
return pending;
|
|
3933
|
+
}
|
|
3934
|
+
/**
|
|
3935
|
+
* Write a concrete reasoningEffort onto the Host picker so the next request
|
|
3936
|
+
* reads the web bar. Always lands via `selectModel`: phone resume/spawn have
|
|
3937
|
+
* no Host interceptor until then, and a live web agent with no conversation
|
|
3938
|
+
* effort would otherwise send the first call without thinking.
|
|
3939
|
+
* @param agent - live agent whose picker should carry explicit effort.
|
|
3911
3940
|
*/
|
|
3912
3941
|
async ensurePinnedEffort(agent) {
|
|
3913
3942
|
const current = this.currentModel(agent);
|
|
@@ -3921,14 +3950,13 @@ var HappyBridge = class {
|
|
|
3921
3950
|
if (reasoning?.efforts !== void 0 && reasoning.efforts.length > 0) supported = reasoning.efforts.map((effort) => effort.id);
|
|
3922
3951
|
} catch {}
|
|
3923
3952
|
const effort = pinWakeEffort(current.reasoningEffort, preferred, modelDefault, supported);
|
|
3924
|
-
if (effort === void 0 || effort === current.reasoningEffort) return;
|
|
3925
3953
|
const next = {
|
|
3926
|
-
|
|
3927
|
-
|
|
3954
|
+
provider: current.provider,
|
|
3955
|
+
model: current.model,
|
|
3956
|
+
...effort === void 0 ? {} : { reasoningEffort: ReasoningEffortId(effort) }
|
|
3928
3957
|
};
|
|
3929
3958
|
this.models.set(agent.id, next);
|
|
3930
|
-
|
|
3931
|
-
if (selection !== void 0) selection.current = next;
|
|
3959
|
+
await this.syncHostSelection(agent, next);
|
|
3932
3960
|
}
|
|
3933
3961
|
applyPermission(agent, mode, fromCommand) {
|
|
3934
3962
|
const presets = this.ctx.get("permissionPresets");
|
|
@@ -4253,8 +4281,8 @@ var HappyBridge = class {
|
|
|
4253
4281
|
const pending = link.pendingHuman;
|
|
4254
4282
|
if (pending !== void 0) {
|
|
4255
4283
|
delete link.pendingHuman;
|
|
4256
|
-
if (pending.kind === "ask" && pending.communication !== void 0) this.completeCommunication(link, pending.id, pending.communication, "
|
|
4257
|
-
else this.clearRequest(link, pending.id, "
|
|
4284
|
+
if (pending.kind === "ask" && pending.communication !== void 0) this.completeCommunication(link, pending.id, pending.communication, "answered", communicationAnswersFromWeb(answer.answers));
|
|
4285
|
+
else this.clearRequest(link, pending.id, "approved", pending.request);
|
|
4258
4286
|
link.socket.sendToolEnd(pending.id);
|
|
4259
4287
|
}
|
|
4260
4288
|
return answer;
|
package/lib/types/bridge.d.ts
CHANGED
|
@@ -18,8 +18,6 @@ export declare class HappyBridge {
|
|
|
18
18
|
private readonly lastPublished;
|
|
19
19
|
/** Count of in-flight phone-originated Host `selectModel` calls. */
|
|
20
20
|
private hostSelectFromPhone;
|
|
21
|
-
/** Per-agent selection installed on phone wake / spawn, matching Host `selectionFor`. */
|
|
22
|
-
private readonly selections;
|
|
23
21
|
/** In-flight phone wakes, so two inbound texts do not double-resume. */
|
|
24
22
|
private readonly waking;
|
|
25
23
|
/** Phone stop-session / archive: do not recreate these Happy rows. */
|
|
@@ -30,6 +28,10 @@ export declare class HappyBridge {
|
|
|
30
28
|
private lastArchivedIds;
|
|
31
29
|
/** Serialize phone text so two messages cannot split one attachment batch. */
|
|
32
30
|
private readonly inboundTail;
|
|
31
|
+
/** One Host-picker pin per live Agent, awaited before the first prompt assembly. */
|
|
32
|
+
private readonly hostPin;
|
|
33
|
+
/** Agents that already prepend the pin onto `system-prompt/assemble`. */
|
|
34
|
+
private readonly assemblePinBound;
|
|
33
35
|
private error;
|
|
34
36
|
private running;
|
|
35
37
|
private scanTimer;
|
|
@@ -98,24 +100,12 @@ export declare class HappyBridge {
|
|
|
98
100
|
private wakeAgent;
|
|
99
101
|
private resumeSession;
|
|
100
102
|
/**
|
|
101
|
-
* Resume/create composition matching Host `composeAgent`:
|
|
102
|
-
* selection
|
|
103
|
+
* Resume/create composition matching Host `composeAgent`: mount the preset
|
|
104
|
+
* when a roster exists. Model selection is the Host picker, landed by
|
|
105
|
+
* `selectModel` in {@link watchFirstAssemble} / {@link rememberModel}.
|
|
103
106
|
* @param presetHint - logged or requested preset id; omitted uses the roster default.
|
|
104
107
|
*/
|
|
105
108
|
private composeAgentSetup;
|
|
106
|
-
/**
|
|
107
|
-
* Same lazy selection Host `selectionFor` installs: remembered pick, else
|
|
108
|
-
* the session's last `request/header`, else `agentDefaultModel`. A missing
|
|
109
|
-
* thinking level keeps the web picker's effort when it is the same model.
|
|
110
|
-
* Unlike Host `installModelSelection`, an absent effort does not clear
|
|
111
|
-
* inherited thinking.
|
|
112
|
-
*/
|
|
113
|
-
private installWakeSelection;
|
|
114
|
-
/**
|
|
115
|
-
* Pin provider/model for a phone-woken agent without wiping thinking when
|
|
116
|
-
* the selection names no effort.
|
|
117
|
-
*/
|
|
118
|
-
private bindWakeSelection;
|
|
119
109
|
/** Host default model, when the web profile mounted `agentDefaultModel`. */
|
|
120
110
|
private defaultModelSelection;
|
|
121
111
|
/** Registered workspace for this session, or `undefined` when it is not in the sidebar. */
|
|
@@ -161,12 +151,18 @@ export declare class HappyBridge {
|
|
|
161
151
|
private spawn;
|
|
162
152
|
private applySpawnMeta;
|
|
163
153
|
private queueInbound;
|
|
154
|
+
/** Same queue as inbound text, so a catalog switch lands on the bar before the next send. */
|
|
155
|
+
private queuePhoneCatalog;
|
|
164
156
|
private onInbound;
|
|
165
157
|
private applyMessageMeta;
|
|
166
158
|
private applyPhoneCatalog;
|
|
167
159
|
private applyModel;
|
|
168
160
|
private applyEffort;
|
|
169
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* Remember a phone-originated pick and land it on the Host picker before
|
|
163
|
+
* the next request. `onHostModelSelected` is skipped while this runs, so
|
|
164
|
+
* {@link models} is written here.
|
|
165
|
+
*/
|
|
170
166
|
private rememberModel;
|
|
171
167
|
/**
|
|
172
168
|
* Write the web picker's Host selection (`sessionController.selectModel`) so
|
|
@@ -179,8 +175,18 @@ export declare class HappyBridge {
|
|
|
179
175
|
*/
|
|
180
176
|
private onHostModelSelected;
|
|
181
177
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
178
|
+
* Land explicit effort on the Host picker and wait for that pin before this
|
|
179
|
+
* Agent's first prompt assembly, so the request reads the web bar.
|
|
180
|
+
* @param agent - live agent whose picker should carry explicit effort.
|
|
181
|
+
*/
|
|
182
|
+
private watchFirstAssemble;
|
|
183
|
+
private pinHostPicker;
|
|
184
|
+
/**
|
|
185
|
+
* Write a concrete reasoningEffort onto the Host picker so the next request
|
|
186
|
+
* reads the web bar. Always lands via `selectModel`: phone resume/spawn have
|
|
187
|
+
* no Host interceptor until then, and a live web agent with no conversation
|
|
188
|
+
* effort would otherwise send the first call without thinking.
|
|
189
|
+
* @param agent - live agent whose picker should carry explicit effort.
|
|
184
190
|
*/
|
|
185
191
|
private ensurePinnedEffort;
|
|
186
192
|
private applyPermission;
|
package/lib/types/catalogs.d.ts
CHANGED
|
@@ -117,4 +117,16 @@ export declare function buildSessionMetadata(ctx: Context, source: {
|
|
|
117
117
|
}[];
|
|
118
118
|
agent?: Agent;
|
|
119
119
|
}, machineId: string, title: string, selection: HappySelection | undefined, _grant: RemoteGrant): Promise<SessionMetadata>;
|
|
120
|
+
/**
|
|
121
|
+
* Pick the Happy catalog row that matches Host selection.
|
|
122
|
+
* A known Host pick that is missing from the listed rows stays that pick
|
|
123
|
+
* (synthetic row) instead of falling back to `models[0]` (usually DeepSeek V4).
|
|
124
|
+
* @param models - rows advertised to the App.
|
|
125
|
+
* @param selection - current Host provider/model/effort, when known.
|
|
126
|
+
* @returns the row to publish, plus effort when the Host named one.
|
|
127
|
+
*/
|
|
128
|
+
export declare function resolvePublishedSelection(models: HappyModelRow[], selection: HappySelection | undefined): {
|
|
129
|
+
row: HappyModelRow;
|
|
130
|
+
effort?: string;
|
|
131
|
+
} | undefined;
|
|
120
132
|
//# sourceMappingURL=catalogs.d.ts.map
|
package/lib/types/grant.d.ts
CHANGED
|
@@ -84,4 +84,14 @@ export interface CatalogModelPick {
|
|
|
84
84
|
* @returns true when the inbound pick is our own echo.
|
|
85
85
|
*/
|
|
86
86
|
export declare function sameCatalogPick(previous: CatalogModelPick | undefined, next: CatalogModelPick): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Whether inbound `meta.model` is the pick we last published.
|
|
89
|
+
* The App often echoes only `currentModelCode` (bare id) on a chat line,
|
|
90
|
+
* while we publish `provider:id`; treating that as a user switch would
|
|
91
|
+
* lock the web composer onto the catalog default (DeepSeek V4).
|
|
92
|
+
* @param published - last pick we wrote, if any.
|
|
93
|
+
* @param inbound - model code from the inbound message.
|
|
94
|
+
* @returns true when the inbound model is our own echo.
|
|
95
|
+
*/
|
|
96
|
+
export declare function isPublishedModelEcho(published: CatalogModelPick | undefined, inbound: string): boolean;
|
|
87
97
|
//# sourceMappingURL=grant.d.ts.map
|
package/lib/types/history.d.ts
CHANGED
|
@@ -155,9 +155,10 @@ export interface WakeModelSelection {
|
|
|
155
155
|
*/
|
|
156
156
|
export declare function wakeModelSelection(primary: WakeModelSelection | undefined, ...fallbacks: Array<WakeModelSelection | undefined>): WakeModelSelection | undefined;
|
|
157
157
|
/**
|
|
158
|
-
* Effort the first phone-spawn / phone-wake request should send.
|
|
158
|
+
* Effort the first web / phone-spawn / phone-wake request should send.
|
|
159
159
|
* Current pick wins; otherwise a preferred (web) value or the model's
|
|
160
160
|
* advertised default, but only when the model lists that id.
|
|
161
|
+
* Written onto the Host picker via `selectModel`; requests then read that bar.
|
|
161
162
|
* @param currentEffort - already chosen effort, if any.
|
|
162
163
|
* @param preferred - web picker effort to reuse when the model accepts it.
|
|
163
164
|
* @param modelDefault - `resolveModelInfo().reasoning.defaultEffort`.
|
package/lib/types/inbound.d.ts
CHANGED
|
@@ -110,6 +110,20 @@ export declare function mergeCustomAnswers(questions: readonly {
|
|
|
110
110
|
selected: string[];
|
|
111
111
|
custom?: string;
|
|
112
112
|
}[];
|
|
113
|
+
/**
|
|
114
|
+
* Translate a web-side harness answer batch into Happy communications answers
|
|
115
|
+
* so the phone card shows the same choice instead of a cancelled form.
|
|
116
|
+
* @param answers - harness answer rows from the web composer.
|
|
117
|
+
* @returns answers keyed by question id.
|
|
118
|
+
*/
|
|
119
|
+
export declare function communicationAnswersFromWeb(answers: readonly {
|
|
120
|
+
id: string;
|
|
121
|
+
selected: string[];
|
|
122
|
+
custom?: string;
|
|
123
|
+
}[]): Record<string, {
|
|
124
|
+
options: string[];
|
|
125
|
+
custom?: string;
|
|
126
|
+
}>;
|
|
113
127
|
/**
|
|
114
128
|
* Translate Happy communications answers (`{ [question id]: { options, custom } }`)
|
|
115
129
|
* into harness answer rows. Options become `selected`; free text becomes `custom`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjhmars/happy-bridge",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "Remote-control bridge: pair a running dsh web/desktop client with the Happy mobile app by QR code, then remote-control the same harness sessions from the phone (send messages, read replies, approve tool calls, switch models)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|