magic-agent 1.0.0 → 1.0.2

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.
Files changed (41) hide show
  1. package/CLIENT-CONTRACT.md +298 -0
  2. package/README.md +35 -0
  3. package/client-ui/assets/OFL-Caveat.txt +93 -0
  4. package/client-ui/assets/caveat.woff2 +0 -0
  5. package/client-ui/index.html +246 -0
  6. package/client-ui/pc/app.css +5110 -0
  7. package/client-ui/pc/assets/damiao-mascot.png +0 -0
  8. package/client-ui/pc/assets/icon.png +0 -0
  9. package/client-ui/pc/index.html +154 -0
  10. package/client-ui/pc/js/00-base.js +52 -0
  11. package/client-ui/pc/js/03-chat-message.js +880 -0
  12. package/client-ui/pc/js/04-task-list.js +430 -0
  13. package/client-ui/pc/js/05-task-list-render.js +812 -0
  14. package/client-ui/pc/js/06-ask-answer.js +204 -0
  15. package/client-ui/pc/js/07-chat-stream-render.js +493 -0
  16. package/client-ui/pc/js/08-ask-event-map.js +171 -0
  17. package/client-ui/pc/js/09-stream-apply.js +367 -0
  18. package/client-ui/pc/js/10-markdown.js +430 -0
  19. package/client-ui/pc/js/18-askbox.js +262 -0
  20. package/client-ui/pc/js/19-attachments.js +679 -0
  21. package/client-ui/pc/js/29-msg-variants.js +462 -0
  22. package/client-ui/pc/js/90-glue.js +212 -0
  23. package/client-ui/pc/js/91-workbench-stubs.js +134 -0
  24. package/client-ui/pc/js/92-composer.js +406 -0
  25. package/client-ui/pc/js/93-rail.js +362 -0
  26. package/client-ui/pc/vendor/highlight.min.js +1262 -0
  27. package/client-ui/pc/vendor/marked.min.js +6 -0
  28. package/client-ui/pc/vendor/purify.min.js +3 -0
  29. package/client-ui/shared/conversation.css +256 -0
  30. package/client-ui/shared/conversation.js +1225 -0
  31. package/client-ui/shared/transport-http.js +123 -0
  32. package/client-ui-mobile/assets/OFL-Caveat.txt +93 -0
  33. package/client-ui-mobile/assets/caveat.woff2 +0 -0
  34. package/client-ui-mobile/index.html +173 -0
  35. package/npm/bin/magic-agent.js +43 -0
  36. package/npm/dist/darwin-arm64/magic-agent +0 -0
  37. package/npm/dist/darwin-x64/magic-agent +0 -0
  38. package/npm/dist/linux-arm64/magic-agent +0 -0
  39. package/npm/dist/linux-x64/magic-agent +0 -0
  40. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  41. package/package.json +4 -1
@@ -0,0 +1,204 @@
1
+ /* ---------- 提问卡 → 回答(AskUserQuestion 的落笔)----------
2
+ 2026-09-18 用户:「返回了 askuserquestion,需要 ui 在对话框中增加选择显示 而不是显示这个 json」。
3
+ 卡落在对话框里,答案也要回到对话框:点选项 = 把这一问的回答作为**追问**发给同一个会话
4
+ (走 qaSend 的 followup 语义、沿用 sessionId 续接,不在左栏另开一条会话)。
5
+
6
+ ⚠️ 发送时机 —— **不打断在跑的那一轮**(用户 2026-09-18 定稿「对话中不允许上一条没结束
7
+ 直接输入下1个」;这条会话的撰写区在它自己生成中锁死):这一轮还在跑就先把答案排进
8
+ qaAskQueue,等它收尾(onDone / onError / 用户点停止)或用户切回该会话时由 qaAskFlush
9
+ 自动发出。
10
+ 空闲时(引擎早就不等回答了 —— 非交互模式下很常见)点一下立即发出,不排队。
11
+ 排队态在卡上已经是「已答」(选中项高亮 + 标题变「已回答」),不会让用户以为点了没反应。 */
12
+ const qaAskQueue = new Map(); // taskId → [等着发出去的答案](一轮里可能问好几次)
13
+ function qaAskAnswered(card){ return !!card && card.dataset.answered === "1"; }
14
+ /** 收这一张卡上的答案,形状与 magic-agent 的 `EncodeAskFollowUp` **逐字对齐**:
15
+ * 【用户选择】
16
+ * - <问题原文> → <答案>
17
+ * 多选多个 label 用 `", "` 连接(Go 侧 askMultiSelectSep)。
18
+ * 为什么必须有这个前缀:答案是以**新的一条 user 消息**追加进会话的(见 agent-cli.cjs 那一节),
19
+ * 没有前缀模型会把它读成一条新需求;两端文案漂移就等于白答。
20
+ * 单问单选也不再只回一个裸标签(原先回的就是「推送到远程」四个字,模型只能靠猜)。 */
21
+ function qaAskText(card){
22
+ const qbs = [...card.querySelectorAll(".qa-ask-qb")];
23
+ const lines = qbs.map(qb=>{
24
+ const labels = [...qb.querySelectorAll(".qa-ask-opt.sel")]
25
+ .map(o=>o.dataset.label || "").filter(Boolean);
26
+ if(!labels.length) return "";
27
+ return "- " + (qb.dataset.q || "") + " → " + labels.join(", ");
28
+ }).filter(Boolean);
29
+ return lines.length ? "【用户选择】\n" + lines.join("\n") + "\n" : "";
30
+ }
31
+ function qaAskAllDone(card){
32
+ const qbs = [...card.querySelectorAll(".qa-ask-qb")];
33
+ return qbs.length > 0 && qbs.every(qb=>qb.querySelector(".qa-ask-opt.sel"));
34
+ }
35
+ function qaAskSyncSub(card){
36
+ const b = card.querySelector(".qa-ask-sub");
37
+ if(b) b.disabled = !qaAskAllDone(card);
38
+ card.querySelectorAll(".qa-ask-opt").forEach(o=>
39
+ o.setAttribute("aria-checked", o.classList.contains("sel") ? "true" : "false"));
40
+ }
41
+ /** 标记「这一问已答」并把答案发出去。
42
+ * text = 真正发出去的内容(`【用户选择】…` 信封,见 qaAskText);
43
+ * display = 卡上「你的回答」那一行显示什么(只有「其他」自由输入需要它:
44
+ * 信封里的题面不适合当展示文案;不传就不显示这行,选项高亮本身已经是答案)。 */
45
+ function qaAskMark(card, text, display){
46
+ const t = String(text || "").trim(); if(!card || !t) return;
47
+ card.dataset.answered = "1";
48
+ const ttl = card.querySelector(".qa-ask-ttl"); if(ttl) ttl.textContent = "提问 · 已回答";
49
+ const shown = String(display == null ? "" : display).trim();
50
+ if(shown){
51
+ const ans = card.querySelector(".qa-ask-ans");
52
+ if(ans){ ans.hidden = false; ans.querySelector(".v").textContent = shown; }
53
+ }
54
+ qaAskSyncSub(card);
55
+ /* sid / rid 从**卡自己**身上取(dataset,见 askCard):这样快照恢复出来的历史卡也答得回去。 */
56
+ qaAskDeliver(qaActiveId, t, { sid: card.dataset.sid || "", rid: card.dataset.rid || "" });
57
+ }
58
+ /** 把答案交给会话:**这条会话**空闲立即发;它自己的那一轮还在跑就排队(收尾/切回时由
59
+ * qaAskFlush 发出)。别的会话在跑**不拦它**(2026-09-18 晚定稿:锁只跟会话走,跨会话不互锁)。
60
+ * 一轮里被问了多次(多张卡)→ 按先后顺序逐条发:前一条发出后新一轮又跑起来,
61
+ * 后一条继续排队,等那一轮收尾再发(同一把闸门,不绕开「生成中不发下一条」)。 */
62
+ function qaAskDeliver(taskId, text, opts){
63
+ const id = taskId || qaActiveId || "";
64
+ const o = opts || {};
65
+ if(qaTaskRunning(id)){
66
+ const q = qaAskQueue.get(id) || [];
67
+ q.push({ text, sid: o.sid || "", rid: o.rid || "" });
68
+ qaAskQueue.set(id, q);
69
+ toast("已选:<b>" + escHtml(text) + "</b><br>这一轮结束后自动发出", true);
70
+ return;
71
+ }
72
+ toast("已选:<b>" + escHtml(text) + "</b>", true);
73
+ qaAskSend(text, o);
74
+ }
75
+ /** 答案的统一出口:**能追加就追加**(答案落进那条还活着的会话、引擎当下一轮处理),
76
+ * 追加不了就新起一轮 + `--session` 续接(老版 CLI / 会话已结束 / 没有会话 id)。
77
+ * 判据是「拿到 sid 且拿到 rid」:sid 来自统一 ask 事件(tool_use 那条老路没有它)——
78
+ * 所以老引擎自动走回落路径,行为与改造前完全一致。 */
79
+ function qaAskSend(text, opts){
80
+ const o = opts || {};
81
+ const sid = String(o.sid || "").trim();
82
+ /* ⚠️ rid 从 dataset 读回来是**字符串**,而事件路由(qaRuns / bindHomeAsk 的 `qaRuns.get(ev.rid)`)
83
+ 用的是**数字** —— 不归一化就会「qaRuns.get(1) 查不到 '1'」,追加回来的那一轮整轮被当过期
84
+ 事件丢掉(界面表现:追加成功、却永远看不到引擎的回答)。 */
85
+ const ridNum = (o.rid == null || o.rid === "") ? NaN : Number(o.rid);
86
+ if(sid && Number.isFinite(ridNum) && window.desk && typeof window.desk.askAppend === "function"){
87
+ qaSend(text, { followup: true, from: "home", append: { sid, rid: ridNum } });
88
+ return;
89
+ }
90
+ qaSend(text, { followup: true, from: "home" });
91
+ }
92
+ /** 排队的答案该不该现在发:**这条会话**没有一轮在跑 + 用户正看着这条会话
93
+ * (qaSend 是按 qaActiveId 落笔的,不能在别的会话的画布里替它发)。
94
+ * 别的会话还在跑**不拦** —— 答案发给的是空闲的这条,并行是会话之间的事。 */
95
+ function qaAskFlush(taskId){
96
+ const id = taskId || "";
97
+ const q = qaAskQueue.get(id);
98
+ if(!q || !q.length || qaTaskRunning(id)) return;
99
+ if(id !== (qaActiveId || "")) return;
100
+ const item = q.shift();
101
+ if(!q.length) qaAskQueue.delete(id);
102
+ qaAskSend(item.text, { sid: item.sid, rid: item.rid });
103
+ }
104
+
105
+ /* ---------- 会话排队:处理中提交的下一条 ----------
106
+ 用户 2026-09-18:「会话处理中时 改成支持输入 但是新需求排列在会话框上面默认等待上一个
107
+ 结束后自动提交」。
108
+ 改前:这一轮没跑完,撰写区整块锁死(输入域 disabled + 提交钮变停止钮)—— 想追问只能干等。
109
+ 改后:输入域照常可写(qaBusyEx 不再 disabled 它),处理中点发送 = **排进队列**:
110
+ · 条目渲染在**会话框上方**(#home-queue,与附件条同在 .home-entry 里),标「等待中」;
111
+ · 这一轮收尾(onDone / onError / 用户点停止)或用户切回这条会话时,由 qaMsgFlush
112
+ **自动提交队首那一条** —— 这就是用户说的「默认等待上一个结束后自动提交」。
113
+ ⚠️ 一次只放一条出去:每条消息都会起**新的一轮**,一次放多条 = 后一条把前一条杀掉
114
+ (主进程 `desk:ask` 是单会话槽)。剩下的等那一轮收尾再放 —— 队列自己就串起来了。
115
+ ⚠️ 提问卡答案优先:qaAskQueue 里还有没发出去的答案时先不发队列消息(那是引擎正等着的
116
+ 回答,抢在它前面发会把这一问的上下文冲掉)。
117
+ ⚠️ 只在**用户正看着这条会话**时发:qaSend 是按 qaActiveId 落笔的(画布只有一份),
118
+ 在别的会话的画布里替它发就是张冠李戴 —— 与 qaAskFlush 同一条判据。切回来即发。
119
+ ⚠️ 附件不进队列:处理中贴图仍被拦(见 askAttBind 的 busyGate),所以条目只有文字。 */
120
+ const qaMsgQueue = new Map(); // taskId → [{ text }]
121
+ /** 队列条目画在**哪只实例**上:队列跟会话走,而「会话框」= home 实例。
122
+ * home2 是新建对话入口 —— 它提交的永远是**新会话**,不排队,也不该显示别人的队列。 */
123
+ const QA_QUEUE_INSTANCE = "home";
124
+ function qaMsgQueueRender(taskId){
125
+ const el = $("#" + QA_QUEUE_INSTANCE + "-queue");
126
+ if(!el) return;
127
+ const id = taskId == null ? (qaActiveId || "") : taskId;
128
+ // 只画「当前显示的这条会话」的队列:切到别的会话时不该看见上一条会话的待发条目
129
+ const list = (id && id === (qaActiveId || "")) ? (qaMsgQueue.get(id) || []) : [];
130
+ el.hidden = !list.length;
131
+ el.innerHTML = "";
132
+ list.forEach((it, i)=>{
133
+ const row = document.createElement("div");
134
+ row.className = "ag-q";
135
+ row.dataset.i = String(i);
136
+ row.innerHTML = `<span class="no"></span><span class="tx"></span>`
137
+ + `<span class="st">等待中 · 上一条结束后自动发送</span>`
138
+ + `<button class="x" type="button" aria-label="取消这条待发消息">×</button>`;
139
+ row.querySelector(".no").textContent = String(i + 1);
140
+ row.querySelector(".tx").textContent = it.text || "";
141
+ /* 直接绑监听(不走 #qa-stream 事件委托):这一行在画布**之外**,不参与快照重建,
142
+ 每次渲染都是新节点 —— 与附件条的 × 同一套做法。 */
143
+ row.querySelector(".x").addEventListener("click", (e)=>{ e.stopPropagation(); qaMsgQueueDrop(id, i); });
144
+ el.appendChild(row);
145
+ });
146
+ }
147
+ /** 取消一条待发消息(点条目尾的 ×):列表空了就把这条会话的队列键删掉,不留空数组。 */
148
+ function qaMsgQueueDrop(taskId, idx){
149
+ const id = taskId || ""; if(!id) return;
150
+ const q = qaMsgQueue.get(id); if(!q || idx < 0 || idx >= q.length) return;
151
+ q.splice(idx, 1);
152
+ if(!q.length) qaMsgQueue.delete(id);
153
+ qaMsgQueueRender(id);
154
+ toast("已取消这条待发消息");
155
+ }
156
+ /** 处理中提交 → 入队(**不打断**在跑的那一轮)。 */
157
+ function qaMsgEnqueue(taskId, item){
158
+ const id = taskId || ""; if(!id) return;
159
+ const q = qaMsgQueue.get(id) || [];
160
+ q.push({ text: String((item && item.text) || "") });
161
+ qaMsgQueue.set(id, q);
162
+ qaMsgQueueRender(id);
163
+ toast("已排队 · 这一轮结束后自动发送<br><b>" + escHtml((item && item.text) || "") + "</b>", true);
164
+ }
165
+ /** 排队的下一条该不该现在发:这条会话没有一轮在跑 + 用户正看着它 + 没有待发的提问卡答案
166
+ * (见上面三条 ⚠️)。发一条即返回 —— 剩下的等这一轮收尾再来。 */
167
+ function qaMsgFlush(taskId){
168
+ const id = taskId || "";
169
+ const q = qaMsgQueue.get(id);
170
+ if(!q || !q.length || qaTaskRunning(id)) return;
171
+ if(id !== (qaActiveId || "")) return;
172
+ if((qaAskQueue.get(id) || []).length) return;
173
+ const item = q.shift();
174
+ if(!q.length) qaMsgQueue.delete(id);
175
+ qaMsgQueueRender(id);
176
+ qaSend(item.text, { followup: true, from: QA_QUEUE_INSTANCE });
177
+ }
178
+ /** 会话删了 → 它排的队也没意义了(不留悬挂条目)。 */
179
+ function qaMsgQueueDropFor(taskId){ qaMsgQueue.delete(taskId || ""); qaMsgQueueRender(); }
180
+ /** 点选项:多选只勾(等「提交」);单选点一下即答 */
181
+ function qaAskPick(card, optEl){
182
+ if(!card || !optEl || qaAskAnswered(card)) return;
183
+ const qb = optEl.closest(".qa-ask-qb"); if(!qb) return;
184
+ if(qb.dataset.multi === "1"){
185
+ optEl.classList.toggle("sel");
186
+ qaAskSyncSub(card);
187
+ return;
188
+ }
189
+ qb.querySelectorAll(".qa-ask-opt").forEach(o=>o.classList.remove("sel"));
190
+ optEl.classList.add("sel");
191
+ qaAskSyncSub(card);
192
+ if(qaAskAllDone(card)) qaAskMark(card, qaAskText(card));
193
+ }
194
+ /** 「其他」自由输入:不在清单里的答案直接写(回车 / 点确认都走这里)。
195
+ * 发出去的是**同一份信封**(引擎只认 `【用户选择】` 前缀,见 qaAskText);卡上「你的回答」
196
+ * 那一行显示用户写的原文(信封里的题面当展示文案太啰嗦)。 */
197
+ function qaAskFree(card, val){
198
+ const t = String(val || "").trim();
199
+ if(!card || !t || qaAskAnswered(card)) return;
200
+ const qbs = [...card.querySelectorAll(".qa-ask-qb")];
201
+ const q = qbs.length === 1 ? (qbs[0].dataset.q || "") : "";
202
+ qaAskMark(card, "【用户选择】\n" + (q ? "- " + q + " → " + t : "- " + t) + "\n", t);
203
+ }
204
+