@preprocess-cn/nano-code-web 0.1.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.
- package/README.md +199 -0
- package/dist/app.js +432 -0
- package/dist/dialog.js +273 -0
- package/dist/display.js +464 -0
- package/dist/index.html +66 -0
- package/dist/server.js +331 -0
- package/dist/style.css +182 -0
- package/dist/tool-display.js +112 -0
- package/dist/vendor/highlight.min.js +1244 -0
- package/dist/vendor/markdown-it.min.js +3 -0
- package/dist/vendor/purify.min.js +3 -0
- package/package.json +29 -0
package/dist/display.js
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { NanoCodeWebServer } from './server.js';
|
|
2
|
+
import { getToolDisplayName, getToolArgsPreview } from './tool-display.js';
|
|
3
|
+
// ── ThinkFilter: 按 showThink 配置过滤 <think>...</think> 块(可跨 chunk)──
|
|
4
|
+
export class ThinkFilter {
|
|
5
|
+
inThink = false;
|
|
6
|
+
pending = '';
|
|
7
|
+
filter(text, showThink) {
|
|
8
|
+
if (showThink)
|
|
9
|
+
return text;
|
|
10
|
+
// 将上一 chunk 未完成的标签前缀拼接回来
|
|
11
|
+
text = this.pending + text;
|
|
12
|
+
this.pending = '';
|
|
13
|
+
let result = '';
|
|
14
|
+
let remaining = text;
|
|
15
|
+
while (remaining) {
|
|
16
|
+
if (this.inThink) {
|
|
17
|
+
const end = remaining.indexOf('</think>');
|
|
18
|
+
if (end === -1) {
|
|
19
|
+
// 全部是 think 内容,只保留尾部可能的 </think> 前缀
|
|
20
|
+
this.pending = partialTagPrefix(remaining, '</think>');
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
remaining = remaining.slice(end + 8);
|
|
24
|
+
this.inThink = false;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const start = remaining.indexOf('<think>');
|
|
28
|
+
if (start === -1) {
|
|
29
|
+
// 没有 think 标签,检查尾部是否可能是 <think> 或 </think> 前缀
|
|
30
|
+
const thinkPending = partialTagPrefix(remaining, '<think>');
|
|
31
|
+
const closeThinkPending = partialTagPrefix(remaining, '</think>');
|
|
32
|
+
this.pending = thinkPending || closeThinkPending;
|
|
33
|
+
return result + remaining.slice(0, remaining.length - this.pending.length).replace(/<\/think>/g, '');
|
|
34
|
+
}
|
|
35
|
+
result += remaining.slice(0, start);
|
|
36
|
+
remaining = remaining.slice(start + 7);
|
|
37
|
+
this.inThink = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
result = result.replace(/<\/think>/g, '');
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
reset() {
|
|
44
|
+
this.inThink = false;
|
|
45
|
+
this.pending = '';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// ── ToolCallBroadcaster: 工具调用事件广播(带双向路径去重)──
|
|
49
|
+
export class ToolCallBroadcaster {
|
|
50
|
+
bcIds = new Set();
|
|
51
|
+
historyCb = null;
|
|
52
|
+
setHistoryCallback(cb) {
|
|
53
|
+
this.historyCb = cb;
|
|
54
|
+
}
|
|
55
|
+
/** 返回是否真正广播了(false = 重复跳过) */
|
|
56
|
+
broadcastCall(server, id, toolName, args, agentName, extra) {
|
|
57
|
+
if (this.bcIds.has(id))
|
|
58
|
+
return false;
|
|
59
|
+
this.bcIds.add(id);
|
|
60
|
+
const data = { id, toolName, args, agentName, ...extra };
|
|
61
|
+
server.broadcast('tool:call', data);
|
|
62
|
+
this.historyCb?.('tool:call', data);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
/** 返回是否真正广播了(false = 没有对应的 call) */
|
|
66
|
+
broadcastResult(server, id, toolName, status, message, agentName) {
|
|
67
|
+
if (!this.bcIds.has(id))
|
|
68
|
+
return false;
|
|
69
|
+
this.bcIds.delete(id);
|
|
70
|
+
const data = { id, toolName, status, message, agentName };
|
|
71
|
+
server.broadcast('tool:result', data);
|
|
72
|
+
this.historyCb?.('tool:result', data);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
reset() { this.bcIds.clear(); }
|
|
76
|
+
}
|
|
77
|
+
/** 检查 text 尾部是否匹配 tag 的前缀(即可能跨 chunk 的标签起始部分),返回匹配长度 */
|
|
78
|
+
function partialTagPrefix(text, tag) {
|
|
79
|
+
const maxLen = Math.min(tag.length - 1, text.length);
|
|
80
|
+
for (let len = maxLen; len >= 1; len--) {
|
|
81
|
+
if (tag.startsWith(text.slice(-len)))
|
|
82
|
+
return text.slice(-len);
|
|
83
|
+
}
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
// ── Store key 常量(与 nano-code store-keys.ts 对齐)──
|
|
87
|
+
const agentCancelledKey = (name) => `agent:cancelled:${name}`;
|
|
88
|
+
const agentAbortKey = (name) => `agent:abort:${name}`;
|
|
89
|
+
const SK_MODE = 'task-plan:mode';
|
|
90
|
+
const SK_PRE_MODE = 'task-plan:preMode';
|
|
91
|
+
// ── 工厂函数:创建 DisplayPlugin 实例 ──
|
|
92
|
+
function createWebDisplay() {
|
|
93
|
+
const server = new NanoCodeWebServer();
|
|
94
|
+
let promptResolve = null;
|
|
95
|
+
let registry = null;
|
|
96
|
+
let agentName = 'main';
|
|
97
|
+
let schemas = [];
|
|
98
|
+
let toolDefsSent = false;
|
|
99
|
+
// 授权确认状态
|
|
100
|
+
let confirmState = null;
|
|
101
|
+
let confirmIdCounter = 0;
|
|
102
|
+
// 问询对话框状态
|
|
103
|
+
let questionDialogResolve = null;
|
|
104
|
+
// 工具调用 ID 追踪(串行场景下完全正确)
|
|
105
|
+
let currentToolId = null;
|
|
106
|
+
let currentToolName = null;
|
|
107
|
+
let showThink = false;
|
|
108
|
+
const thinkFilter = new ThinkFilter();
|
|
109
|
+
const toolCallBc = new ToolCallBroadcaster();
|
|
110
|
+
toolCallBc.setHistoryCallback(pushHistory);
|
|
111
|
+
// 事件历史环形缓冲区(用于前端重连时回放)
|
|
112
|
+
const MAX_EVENT_HISTORY = 500;
|
|
113
|
+
const eventRing = new Array(MAX_EVENT_HISTORY);
|
|
114
|
+
let ringHead = 0;
|
|
115
|
+
let ringCount = 0;
|
|
116
|
+
function pushHistory(type, data) {
|
|
117
|
+
eventRing[(ringHead + ringCount) % MAX_EVENT_HISTORY] = { type, data };
|
|
118
|
+
if (ringCount < MAX_EVENT_HISTORY)
|
|
119
|
+
ringCount++;
|
|
120
|
+
else
|
|
121
|
+
ringHead = (ringHead + 1) % MAX_EVENT_HISTORY;
|
|
122
|
+
}
|
|
123
|
+
// 会话状态快照(用于新 SSE 客户端连入时重放)
|
|
124
|
+
let lastSessionStart = null;
|
|
125
|
+
let isReady = false;
|
|
126
|
+
// 新 SSE 客户端连入时发送当前状态
|
|
127
|
+
server.onConnect((client) => {
|
|
128
|
+
if (lastSessionStart) {
|
|
129
|
+
const msg = `event: session:start\ndata: ${JSON.stringify(lastSessionStart)}\n\n`;
|
|
130
|
+
client.res.write(msg);
|
|
131
|
+
}
|
|
132
|
+
// 发送工具定义(含 displayName),供前端友好展示
|
|
133
|
+
if (schemas.length > 0) {
|
|
134
|
+
client.res.write(`event: tool:definitions\ndata: ${JSON.stringify({ definitions: schemas })}\n\n`);
|
|
135
|
+
}
|
|
136
|
+
// 重放历史事件,合并连续的 stream:chunk(大量小块合并为一条完整消息)
|
|
137
|
+
let i = 0;
|
|
138
|
+
while (i < ringCount) {
|
|
139
|
+
const evt = eventRing[(ringHead + i) % MAX_EVENT_HISTORY];
|
|
140
|
+
if (evt.type === 'stream:chunk') {
|
|
141
|
+
let text = '';
|
|
142
|
+
const agentName = evt.data.agentName;
|
|
143
|
+
while (i < ringCount && eventRing[(ringHead + i) % MAX_EVENT_HISTORY].type === 'stream:chunk') {
|
|
144
|
+
text += eventRing[(ringHead + i) % MAX_EVENT_HISTORY].data.text;
|
|
145
|
+
i++;
|
|
146
|
+
}
|
|
147
|
+
client.res.write(`event: stream:chunk\ndata: ${JSON.stringify({ text, agentName })}\n\n`);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
client.res.write(`event: ${evt.type}\ndata: ${JSON.stringify(evt.data)}\n\n`);
|
|
151
|
+
i++;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (isReady) {
|
|
155
|
+
client.res.write('event: session:ready\ndata: {}\n\n');
|
|
156
|
+
}
|
|
157
|
+
// 新客户端连入时发送当前 mode 状态(兜底,以防历史缓冲区中无 status:bar)
|
|
158
|
+
const curMode = registry?.store?.get(SK_MODE) || 'normal';
|
|
159
|
+
if (curMode === 'plan') {
|
|
160
|
+
client.res.write(`event: status:bar\ndata: ${JSON.stringify({ segments: { mode: '● PLAN' } })}\n\n`);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
// ── SIGINT 处理(Ctrl+C 退出提示状态)──
|
|
164
|
+
let sigintHandler = null;
|
|
165
|
+
function cleanupPrompt() {
|
|
166
|
+
promptResolve = null;
|
|
167
|
+
isReady = false;
|
|
168
|
+
if (sigintHandler) {
|
|
169
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
170
|
+
sigintHandler = null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// ── 注册 HTTP 回调 ──
|
|
174
|
+
server.onInput((text) => {
|
|
175
|
+
if (promptResolve) {
|
|
176
|
+
const r = promptResolve;
|
|
177
|
+
cleanupPrompt();
|
|
178
|
+
r(text);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
server.onCancel(() => {
|
|
182
|
+
if (!registry)
|
|
183
|
+
return;
|
|
184
|
+
registry.store.set(agentCancelledKey(agentName), true);
|
|
185
|
+
const abortCtrl = registry.store.get(agentAbortKey(agentName));
|
|
186
|
+
if (abortCtrl && !abortCtrl.signal.aborted)
|
|
187
|
+
abortCtrl.abort();
|
|
188
|
+
});
|
|
189
|
+
// ── DisplayPlugin 对象 ──
|
|
190
|
+
const display = {
|
|
191
|
+
name: 'nano-code-web',
|
|
192
|
+
ownsOutput: true,
|
|
193
|
+
rawInput: false,
|
|
194
|
+
async onInit(r) {
|
|
195
|
+
registry = r;
|
|
196
|
+
// 获取工具定义(含 displayName),供前端友好展示
|
|
197
|
+
schemas = registry.getAllSchemas?.() ?? [];
|
|
198
|
+
if (schemas.length > 0 && !toolDefsSent) {
|
|
199
|
+
server.broadcast('tool:definitions', { definitions: schemas });
|
|
200
|
+
toolDefsSent = true;
|
|
201
|
+
}
|
|
202
|
+
// 注册工具调用追踪 NanoPlugin
|
|
203
|
+
const toolTracker = {
|
|
204
|
+
name: 'web-tool-tracker',
|
|
205
|
+
description: '转发工具调用事件到 SSE(带 ID)',
|
|
206
|
+
getTools: () => [],
|
|
207
|
+
execute: async (_name, _args, _ctx) => ({
|
|
208
|
+
status: 'error',
|
|
209
|
+
message: 'tool-tracker 不提供任何工具',
|
|
210
|
+
}),
|
|
211
|
+
onBeforeToolCall(toolCall) {
|
|
212
|
+
currentToolName = toolCall.function?.name || 'unknown';
|
|
213
|
+
currentToolId = toolCall.id;
|
|
214
|
+
let args;
|
|
215
|
+
try {
|
|
216
|
+
args = JSON.parse(toolCall.function?.arguments || '{}');
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
args = {};
|
|
220
|
+
}
|
|
221
|
+
// 将 displayName 和 argsPreview 附加到广播中
|
|
222
|
+
const displayName = getToolDisplayName(currentToolName ?? 'unknown', schemas);
|
|
223
|
+
const argsPreview = getToolArgsPreview(args);
|
|
224
|
+
const extra = { displayName, argsPreview: argsPreview ?? undefined };
|
|
225
|
+
toolCallBc.broadcastCall(server, toolCall.id || 'no-id', currentToolName, args, agentName, extra);
|
|
226
|
+
return toolCall;
|
|
227
|
+
},
|
|
228
|
+
onAfterToolCall(result) {
|
|
229
|
+
if (currentToolId) {
|
|
230
|
+
toolCallBc.broadcastResult(server, currentToolId, currentToolName, result.status, result.message, agentName);
|
|
231
|
+
currentToolId = null;
|
|
232
|
+
// 保留 currentToolName — DisplayPlugin.onToolResult 仍可能引用
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
await registry.register(toolTracker);
|
|
238
|
+
// 注册授权确认回调
|
|
239
|
+
registry.setConfirmCallback(async (req) => {
|
|
240
|
+
// 如有旧的 pending 确认,先拒绝
|
|
241
|
+
if (confirmState) {
|
|
242
|
+
confirmState.resolve(false);
|
|
243
|
+
confirmState = null;
|
|
244
|
+
}
|
|
245
|
+
const id = 'cf_' + (++confirmIdCounter) + '_' + Date.now().toString(36);
|
|
246
|
+
return new Promise(resolve => {
|
|
247
|
+
confirmState = { id, resolve };
|
|
248
|
+
const confirmData = {
|
|
249
|
+
id, toolName: req.toolName, displayName: req.displayName, message: req.message,
|
|
250
|
+
details: req.details, diff: req.diff, filePath: req.filePath, agentName,
|
|
251
|
+
};
|
|
252
|
+
server.broadcast('confirmation:request', confirmData);
|
|
253
|
+
pushHistory('confirmation:request', confirmData);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
// 处理前端发回的确认结果
|
|
257
|
+
server.onConfirm((id, approved) => {
|
|
258
|
+
if (confirmState && confirmState.id === id) {
|
|
259
|
+
const r = confirmState.resolve;
|
|
260
|
+
confirmState = null;
|
|
261
|
+
r(approved);
|
|
262
|
+
server.broadcast('confirmation:resolved', { id });
|
|
263
|
+
pushHistory('confirmation:resolved', { id });
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
// 注册 ask_user_question 交互式 handler
|
|
267
|
+
registry.registerInteractiveHandler('ask_user_question', async (args) => {
|
|
268
|
+
const { questions } = args || {};
|
|
269
|
+
return new Promise(resolve => {
|
|
270
|
+
const id = 'qd_' + (++confirmIdCounter) + '_' + Date.now().toString(36);
|
|
271
|
+
questionDialogResolve = (answers) => {
|
|
272
|
+
resolve({ status: 'success', data: JSON.stringify({ questions, answers }) });
|
|
273
|
+
};
|
|
274
|
+
server.broadcast('question:dialog', { id, questions });
|
|
275
|
+
pushHistory('question:dialog', { id, questions });
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
// 处理前端发回的对话框答案
|
|
279
|
+
server.onQuestionAnswer((id, answers) => {
|
|
280
|
+
if (questionDialogResolve) {
|
|
281
|
+
const r = questionDialogResolve;
|
|
282
|
+
questionDialogResolve = null;
|
|
283
|
+
r(answers);
|
|
284
|
+
server.broadcast('question:resolved', { id });
|
|
285
|
+
pushHistory('question:resolved', { id });
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
// 注册 mode toggle 回调
|
|
289
|
+
server.onModeToggle(() => {
|
|
290
|
+
const currentMode = registry.store.get(SK_MODE) || 'normal';
|
|
291
|
+
if (currentMode === 'plan') {
|
|
292
|
+
const preMode = registry.store.get(SK_PRE_MODE) || 'normal';
|
|
293
|
+
registry.store.set(SK_MODE, preMode);
|
|
294
|
+
registry.store.set(SK_PRE_MODE, undefined);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
registry.store.set(SK_PRE_MODE, currentMode);
|
|
298
|
+
registry.store.set(SK_MODE, 'plan');
|
|
299
|
+
}
|
|
300
|
+
// 通知所有在线客户端 mode 变化
|
|
301
|
+
const newMode = registry.store.get(SK_MODE) || 'normal';
|
|
302
|
+
server.broadcast('status:bar', { segments: { mode: newMode === 'plan' ? '● PLAN' : '○ normal' } });
|
|
303
|
+
});
|
|
304
|
+
// 注册 output handler:将命令 stdout/stderr 转发为 SSE,避免直写终端
|
|
305
|
+
r.setOutputHandler({
|
|
306
|
+
stdout(chunk) {
|
|
307
|
+
server.broadcast('tool:stdout', { text: chunk, agentName });
|
|
308
|
+
},
|
|
309
|
+
stderr(chunk) {
|
|
310
|
+
server.broadcast('tool:stderr', { text: chunk, agentName });
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
// 启动 HTTP/SSE 服务器
|
|
314
|
+
try {
|
|
315
|
+
const port = await server.start();
|
|
316
|
+
process.stderr.write(`\n nano-code-web UI: http://localhost:${port} (远程访问使用服务器 IP)\n\n`);
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
const portHint = err?.port ? `端口 ${err.port}` : '端口';
|
|
320
|
+
if (err?.code === 'EADDRINUSE') {
|
|
321
|
+
process.stderr.write(`\n 错误:${portHint} 已被占用,请关闭其他进程后重试\n\n`);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
process.stderr.write(`\n 错误:无法启动 Web 服务器 - ${err?.message || err}\n\n`);
|
|
325
|
+
}
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
// ── 会话生命周期 ──
|
|
330
|
+
onStart(config) {
|
|
331
|
+
agentName = config.agentName || 'main';
|
|
332
|
+
showThink = config.showThink === true;
|
|
333
|
+
thinkFilter.reset();
|
|
334
|
+
toolCallBc.reset();
|
|
335
|
+
isReady = false;
|
|
336
|
+
ringHead = 0;
|
|
337
|
+
ringCount = 0; // 新 session / /clear 时清空历史缓冲区
|
|
338
|
+
lastSessionStart = {
|
|
339
|
+
greeting: config.greeting,
|
|
340
|
+
agentName,
|
|
341
|
+
profileName: config.profileName,
|
|
342
|
+
hasTools: config.hasTools,
|
|
343
|
+
showThink: config.showThink,
|
|
344
|
+
debug: config.debug,
|
|
345
|
+
};
|
|
346
|
+
server.broadcast('session:start', lastSessionStart);
|
|
347
|
+
},
|
|
348
|
+
onStop(message) {
|
|
349
|
+
server.broadcast('session:stop', { message });
|
|
350
|
+
server.stop();
|
|
351
|
+
},
|
|
352
|
+
prompt() {
|
|
353
|
+
return new Promise(resolve => {
|
|
354
|
+
promptResolve = resolve;
|
|
355
|
+
isReady = true;
|
|
356
|
+
server.broadcast('session:ready', {});
|
|
357
|
+
// Ctrl+C → 退出
|
|
358
|
+
const onSigint = () => {
|
|
359
|
+
process.stderr.write('\n');
|
|
360
|
+
if (promptResolve) {
|
|
361
|
+
const r = promptResolve;
|
|
362
|
+
cleanupPrompt();
|
|
363
|
+
r(null);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
sigintHandler = onSigint;
|
|
367
|
+
process.on('SIGINT', onSigint);
|
|
368
|
+
// 30 秒无客户端连入则退出
|
|
369
|
+
const timer = setTimeout(() => {
|
|
370
|
+
if (!server.hasClients() && promptResolve) {
|
|
371
|
+
const r = promptResolve;
|
|
372
|
+
cleanupPrompt();
|
|
373
|
+
r(null);
|
|
374
|
+
}
|
|
375
|
+
}, 30000);
|
|
376
|
+
// 有客户端连入时取消超时
|
|
377
|
+
const ival = setInterval(() => {
|
|
378
|
+
if (server.hasClients()) {
|
|
379
|
+
clearTimeout(timer);
|
|
380
|
+
clearInterval(ival);
|
|
381
|
+
}
|
|
382
|
+
}, 500);
|
|
383
|
+
});
|
|
384
|
+
},
|
|
385
|
+
onUserInput(_input, _sourcePlugin) {
|
|
386
|
+
currentToolId = null;
|
|
387
|
+
currentToolName = null;
|
|
388
|
+
server.broadcast('user:input', { text: _input, agentName });
|
|
389
|
+
pushHistory('user:input', { text: _input, agentName });
|
|
390
|
+
},
|
|
391
|
+
// ── 显示事件(全部转发为 SSE)──
|
|
392
|
+
onStatus(event) {
|
|
393
|
+
const data = { level: event.level, message: event.message, agentName: event.agentName };
|
|
394
|
+
server.broadcast('status', data);
|
|
395
|
+
pushHistory('status', data);
|
|
396
|
+
},
|
|
397
|
+
onStreamChunk(event) {
|
|
398
|
+
const text = thinkFilter.filter(event.text, showThink);
|
|
399
|
+
if (text) {
|
|
400
|
+
server.broadcast('stream:chunk', { text, agentName: event.agentName });
|
|
401
|
+
pushHistory('stream:chunk', { text, agentName: event.agentName });
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
onToolCall(event) {
|
|
405
|
+
const id = event.id || event.toolCallId;
|
|
406
|
+
if (!id)
|
|
407
|
+
return;
|
|
408
|
+
currentToolId = id;
|
|
409
|
+
currentToolName = event.toolName || event.name || event.function?.name || 'unknown';
|
|
410
|
+
const args = event.args || event.input || {};
|
|
411
|
+
const displayName = getToolDisplayName(currentToolName ?? 'unknown', schemas);
|
|
412
|
+
const argsPreview = getToolArgsPreview(args);
|
|
413
|
+
toolCallBc.broadcastCall(server, id, currentToolName, args, agentName, { displayName, argsPreview: argsPreview ?? undefined });
|
|
414
|
+
},
|
|
415
|
+
onToolResult(event) {
|
|
416
|
+
const id = event.id || event.toolCallId;
|
|
417
|
+
if (!id)
|
|
418
|
+
return;
|
|
419
|
+
// ToolResultEvent 已无 toolName 字段,使用 currentToolName(由 onToolCall/NanoPlugin 设置)
|
|
420
|
+
toolCallBc.broadcastResult(server, id, currentToolName, event.status, event.message || event.error, agentName);
|
|
421
|
+
},
|
|
422
|
+
onError(event) {
|
|
423
|
+
const data = { message: event.message, stack: event.stack, agentName: event.agentName };
|
|
424
|
+
server.broadcast('error', data);
|
|
425
|
+
pushHistory('error', data);
|
|
426
|
+
},
|
|
427
|
+
onDebug(event) {
|
|
428
|
+
server.broadcast('debug', {
|
|
429
|
+
data: event.data,
|
|
430
|
+
agentName: event.agentName,
|
|
431
|
+
});
|
|
432
|
+
},
|
|
433
|
+
onBackgroundTask(event) {
|
|
434
|
+
server.broadcast('background:task', {
|
|
435
|
+
taskId: event.taskId,
|
|
436
|
+
taskStatus: event.taskStatus,
|
|
437
|
+
message: event.message,
|
|
438
|
+
agentName: event.agentName,
|
|
439
|
+
});
|
|
440
|
+
},
|
|
441
|
+
onAgentTurnStart(event) {
|
|
442
|
+
const data = { agentName: event.agentName };
|
|
443
|
+
server.broadcast('agent:turn_start', data);
|
|
444
|
+
pushHistory('agent:turn_start', data);
|
|
445
|
+
},
|
|
446
|
+
onAgentTurnEnd(event) {
|
|
447
|
+
const data = { agentName: event.agentName };
|
|
448
|
+
server.broadcast('agent:turn_end', data);
|
|
449
|
+
pushHistory('agent:turn_end', data);
|
|
450
|
+
},
|
|
451
|
+
onStateSnapshot(snapshot) {
|
|
452
|
+
server.broadcast('state:snapshot', {
|
|
453
|
+
agentName: snapshot.agentName,
|
|
454
|
+
messageCount: snapshot.messageCount,
|
|
455
|
+
});
|
|
456
|
+
},
|
|
457
|
+
setStatusBar(segments) {
|
|
458
|
+
server.broadcast('status:bar', { segments });
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
return display;
|
|
462
|
+
}
|
|
463
|
+
const plugin = createWebDisplay();
|
|
464
|
+
export default plugin;
|
package/dist/index.html
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>nano-code</title>
|
|
7
|
+
<link rel="stylesheet" href="style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
|
|
11
|
+
<header>
|
|
12
|
+
<h1>nano-code</h1>
|
|
13
|
+
<span class="dot connecting" id="status-dot"></span>
|
|
14
|
+
<span id="status-text">Connecting...</span>
|
|
15
|
+
<span id="mode-indicator" onclick="toggleMode()"></span>
|
|
16
|
+
<button id="cancel-btn" onclick="sendCancel()">Cancel</button>
|
|
17
|
+
</header>
|
|
18
|
+
|
|
19
|
+
<div id="welcome">
|
|
20
|
+
<div class="logo">nano-code</div>
|
|
21
|
+
<div class="hint">Connected · waiting for input<br>Type a message to start</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div id="bg-tasks"></div>
|
|
25
|
+
<div id="status-bar"></div>
|
|
26
|
+
<div id="messages"></div>
|
|
27
|
+
<div id="thinking"><span class="label">Thinking</span><div class="think-dots"><span></span><span></span><span></span></div></div>
|
|
28
|
+
|
|
29
|
+
<!-- ── Question dialog overlay ── -->
|
|
30
|
+
<div id="qd-overlay">
|
|
31
|
+
<div id="qd-dialog">
|
|
32
|
+
<div id="qd-header">
|
|
33
|
+
<span class="icon">❓</span>
|
|
34
|
+
<span class="title" id="qd-header-title">Question</span>
|
|
35
|
+
<span class="progress" id="qd-progress"></span>
|
|
36
|
+
</div>
|
|
37
|
+
<div id="qd-question"></div>
|
|
38
|
+
<div id="qd-options"></div>
|
|
39
|
+
<div id="qd-desc"></div>
|
|
40
|
+
<div id="qd-hint"></div>
|
|
41
|
+
<div id="qd-custom-area">
|
|
42
|
+
<textarea id="qd-custom-input" rows="2" placeholder="输入自定义文本..."></textarea>
|
|
43
|
+
</div>
|
|
44
|
+
<div id="qd-custom-hint">Enter 确认 · Esc 返回</div>
|
|
45
|
+
<div id="qd-confirm-area"></div>
|
|
46
|
+
<div id="qd-footer">
|
|
47
|
+
<button class="qd-btn danger" id="qd-cancel" onclick="qdCancel()">Cancel</button>
|
|
48
|
+
<button class="qd-btn secondary" id="qd-back" onclick="qdBack()" style="display:none">Back</button>
|
|
49
|
+
<button class="qd-btn primary" id="qd-next" onclick="qdNext()">Next</button>
|
|
50
|
+
<button class="qd-btn primary" id="qd-submit" onclick="qdSubmit()" style="display:none">Submit</button>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div id="input-area">
|
|
56
|
+
<textarea id="input" placeholder="Type a message... (Enter to send, Shift+Enter for newline)" disabled></textarea>
|
|
57
|
+
<button id="send-btn" disabled onclick="sendInput()">Send</button>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<script src="dialog.js"></script>
|
|
61
|
+
<script src="app.js"></script>
|
|
62
|
+
<script defer src="vendor/markdown-it.min.js"></script>
|
|
63
|
+
<script defer src="vendor/highlight.min.js"></script>
|
|
64
|
+
<script defer src="vendor/purify.min.js"></script>
|
|
65
|
+
</body>
|
|
66
|
+
</html>
|