@my-life-buddies/cli 0.13.0 → 0.13.3

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/dist/web/app.js CHANGED
@@ -5,6 +5,7 @@
5
5
  const hostedBuddyId = hosted ? new URL(location.href).searchParams.get("buddyId") : "";
6
6
  const connectionUrl = hosted ? new URL(`../api/buddies/${encodeURIComponent(hostedBuddyId)}/preview`, location.href) : undefined;
7
7
  let connectionInstance = "", connectionReady = !hosted, connectionBusy = false, devPollBusy = false;
8
+ let connectionChecked = false, connectionFailure = "", devAction = "";
8
9
  function previewApiUrl(path) {
9
10
  if (!hosted) return path;
10
11
  const url = new URL(connectionUrl.href + path);
@@ -64,6 +65,7 @@
64
65
  buddy: undefined,
65
66
  dev: { phase: "idle", revision: 0 },
66
67
  simulator: { messages: [] },
68
+ simulatorLoaded: false,
67
69
  debug: {
68
70
  source: "observed",
69
71
  generatedAt: new Date().toISOString(),
@@ -109,7 +111,7 @@
109
111
  },
110
112
  });
111
113
  const value = await response.json().catch(() => ({}));
112
- if (hosted && (!connectionReady || instance !== connectionInstance)) throw new Error("本地连接已改变,请等待重新连接");
114
+ if (hosted && (!connectionReady || instance !== connectionInstance)) throw Object.assign(new Error("本地连接已改变,请等待重新连接"), {status:409, code:"PREVIEW_CONNECTION_CHANGED"});
113
115
  if (!response.ok) {
114
116
  const error = new Error((typeof value?.error === "string" ? value.error : value?.error?.message) || `请求失败(HTTP ${response.status})`);
115
117
  error.code = value?.error?.code || "PREVIEW_REQUEST_FAILED";
@@ -134,14 +136,14 @@
134
136
  }
135
137
 
136
138
  function formatDuration(value) {
137
- if (!Number.isFinite(value)) return "";
139
+ if (!Number.isFinite(value)) return "未记录";
138
140
  if (value < 1000) return `${Math.round(value)} ms`;
139
141
  return `${(value / 1000).toFixed(value < 10_000 ? 2 : 1)} s`;
140
142
  }
141
143
 
142
144
  function formatTime(value, seconds = true) {
143
145
  const date = new Date(value);
144
- if (!Number.isFinite(date.getTime())) return "";
146
+ if (!Number.isFinite(date.getTime())) return "未记录";
145
147
  return new Intl.DateTimeFormat("zh-CN", {
146
148
  hour: "2-digit",
147
149
  minute: "2-digit",
@@ -197,6 +199,7 @@
197
199
  function applyDevState(next) {
198
200
  if (!next || !Number.isFinite(next.revision)) return false;
199
201
  if (Number.isFinite(state.dev?.revision) && next.revision < state.dev.revision) return false;
202
+ if (next.phase === "ready" && state.dev.phase !== "ready") state.simulatorLoaded = false;
200
203
  state.dev = next;
201
204
  if (next.ready?.buddy) state.buddy = next.ready.buddy;
202
205
  return true;
@@ -244,18 +247,20 @@
244
247
  let pairingRequest = 0;
245
248
 
246
249
  function renderDev() {
247
- const phase = connectionReady ? state.dev.phase : "disconnected";
250
+ const phase = connectionReady ? (devAction === "start" ? "starting" : devAction === "stop" ? "stopping" : state.dev.phase) : "disconnected";
251
+ renderWorkbench();
248
252
  if (!elements.pairingPanel.hidden && pairingRevision !== state.dev.revision) void togglePairing(true);
249
253
  elements.connectionState.dataset.phase = phase;
250
254
  elements.connectionLabel.textContent = phase === "disconnected" ? "未连接本地工程" : phaseCopy(phase);
251
- elements.devControl.disabled = !connectionReady || phase === "starting" || phase === "stopping";
255
+ elements.devControl.disabled = !connectionReady || Boolean(devAction) || phase === "starting" || phase === "stopping";
252
256
  elements.devControl.dataset.action = phase === "ready" ? "stop" : "start";
253
- elements.devControl.textContent = phase === "ready" ? "停止 DEV" : phase === "starting" ? "正在启动…" : phase === "stopping" ? "正在停止…" : "启动 DEV";
257
+ elements.devControl.textContent = devAction === "start" ? "正在启动…" : devAction === "stop" ? "正在停止…" : phase === "failed" ? "重试启动" : phase === "ready" ? "停止 DEV" : phase === "starting" ? "正在启动…" : phase === "stopping" ? "正在停止…" : "启动 DEV";
254
258
  elements.environmentAgent.textContent = phase === "ready"
255
259
  ? `${state.dev.ready?.agentId || "已就绪"} · CLI 托管`
256
260
  : phaseCopy(phase);
257
261
 
258
262
  const active = state.simulator.activeRun;
263
+ elements.pairDevice.disabled = !connectionReady || phase !== "ready";
259
264
  elements.buddyStatus.textContent = phase !== "ready"
260
265
  ? phaseCopy(phase)
261
266
  : active
@@ -266,7 +271,7 @@
266
271
  elements.resetConversation.disabled = phase !== "ready" || Boolean(active) || state.resetting || state.sending || state.resetUnavailable;
267
272
  elements.resetConversation.title = state.resetUnavailable ? "当前平台暂不支持重开会话" : active ? "请等待当前回复完成后再清空" : "结束当前开发版对话,开始一段全新会话";
268
273
  elements.devControl.disabled ||= state.resetting;
269
- elements.messageInput.disabled = phase !== "ready" || Boolean(active) || state.resetting || state.sending;
274
+ elements.messageInput.disabled = phase !== "ready" || !state.simulatorLoaded || Boolean(active) || state.resetting || state.sending;
270
275
  elements.sendMessage.hidden = Boolean(active);
271
276
  updateSendButton();
272
277
  syncPolling();
@@ -280,6 +285,45 @@
280
285
  }
281
286
  }
282
287
 
288
+ function renderWorkbench() {
289
+ if (!hosted) return;
290
+ const phase = connectionReady ? (devAction === "start" ? "starting" : devAction === "stop" ? "stopping" : state.dev.phase) : "disconnected";
291
+ const runs = state.debug.runs || [];
292
+ const active = state.simulator.activeRun;
293
+ document.body.dataset.connection = connectionReady ? "connected" : "disconnected";
294
+ document.body.dataset.devPhase = phase;
295
+ document.body.dataset.hasRuns = String(runs.length > 0);
296
+ document.body.dataset.busy = String(phase === "starting" || phase === "stopping" || state.sending || Boolean(active));
297
+ const guide = $("#workbench-state");
298
+ const history = Boolean(state.simulator.messages?.length);
299
+ $("#debug-buddy-name").textContent = state.buddy?.name || new URL(location.href).searchParams.get("buddyName") || "我的搭子";
300
+ const copy = !connectionChecked ? ["正在检查连接", "读取本地工程的连接状态。"]
301
+ : phase === "disconnected" ? [connectionInstance ? "本地连接已断开" : "连接本地工程", connectionFailure || (connectionInstance ? "正在等待重新连接。保留当前记录,连接恢复后继续调试。" : "点击上方「连接工程」,复制指令到终端运行。")]
302
+ : phase === "starting" ? ["正在启动调试", "正在准备运行环境,启动完成后即可发送消息。"]
303
+ : phase === "stopping" ? ["正在停止调试", "等待运行环境退出,当前记录会保留。"]
304
+ : phase === "failed" ? ["调试未能启动", "查看上方错误信息,修复工程后点击「重试启动」。"]
305
+ : phase === "idle" ? ["工程已连接", "点击上方「启动 DEV」,开始体验搭子。"]
306
+ : !state.simulatorLoaded ? ["正在读取对话", "同步当前会话,完成后即可发送消息。"]
307
+ : state.sending ? ["正在发送消息", "提交成功后,这里会显示本次执行过程。"]
308
+ : active ? ["等待执行记录", "消息已提交,正在同步本次执行过程。"]
309
+ : ["开始一轮调试", history ? "右侧保留了历史对话。本地执行记录从本次连接开始,发一条新消息即可查看。" : "在右侧发一条消息,观察搭子的回复与执行过程。"];
310
+ guide.hidden = phase === "ready" && runs.length > 0;
311
+ $("#workbench-state-title").textContent = copy[0];
312
+ $("#workbench-state-copy").textContent = copy[1];
313
+ $("#connection-description").textContent = !connectionChecked ? "检查中" : phase === "disconnected" ? "等待连接" : "仅用于本地调试";
314
+ const connectButton = $("#get-connect-command");
315
+ connectButton.className = `button ${connectionReady ? "button-secondary" : "button-primary"}`;
316
+ connectButton.textContent = connectionReady ? "连接指令" : connectionInstance ? "重新连接" : "连接工程";
317
+ const hint = $("#preview-hint");
318
+ hint.textContent = phase !== "ready" ? (phase === "disconnected" ? "连接工程后开始体验" : phase === "starting" ? "正在启动,稍后即可发送" : "启动 DEV 后开始体验")
319
+ : !state.simulatorLoaded ? "正在读取对话,请稍候" : state.resetting ? "正在清空对话,请稍候" : state.sending ? "正在发送,请稍候"
320
+ : active ? (state.simulator.streaming?.text ? "正在生成回复" : "消息已送达,等待回复") : "Enter 发送,Shift + Enter 换行";
321
+ if (!state.project) elements.buddyName.textContent = new URL(location.href).searchParams.get("buddyName") || "我的搭子";
322
+ const empty = elements.chatMessages.querySelector(".chat-empty");
323
+ if (empty) empty.textContent = phase === "ready" ? "发一条消息,开始体验" : phase === "disconnected" ? "连接后,在这里体验搭子的回复" : "启动 DEV 后,开始对话";
324
+ elements.messageInput.placeholder = active ? "等待当前回复完成" : phase === "ready" ? "给搭子发一条消息" : "等待调试就绪";
325
+ }
326
+
283
327
  function runOrdinal(run, index, runs) {
284
328
  // Older running preview servers have no ordinal yet. Never infer a message
285
329
  // association from list positions or timestamps; this is a display number only.
@@ -331,8 +375,8 @@
331
375
  elements.runStatus.textContent = runStatusCopy(run?.status || "idle");
332
376
  elements.metricAccept.textContent = formatDuration(run?.acceptLatencyMs);
333
377
  elements.metricFirst.textContent = formatDuration(run?.firstTextLatencyMs);
334
- elements.metricResponse.textContent = run?.completion === "no_reply" ? "—" : formatDuration(run?.responseTimeMs);
335
- elements.metricMessages.textContent = String(state.simulator.messages?.length || 0);
378
+ elements.metricResponse.textContent = formatDuration(run?.responseTimeMs);
379
+ elements.metricMessages.textContent = state.simulatorLoaded ? String(state.simulator.messages?.length || 0) : "读取中";
336
380
  renderOverview(run);
337
381
  renderTimeline(run);
338
382
  }
@@ -365,46 +409,54 @@
365
409
  elements.overviewEmpty.hidden = Boolean(run);
366
410
  elements.overviewContent.hidden = !run;
367
411
  if (!run) return;
412
+ const finished = ["completed", "failed", "cancelled"].includes(run.status);
413
+ const measuredFirstText = Number.isFinite(run.firstTextLatencyMs);
414
+ const hasText = measuredFirstText || (state.simulator.streaming?.runId === run.runId && Boolean(state.simulator.streaming.text));
415
+ const noReply = run.completion === "no_reply";
416
+ const interrupted = ["failed", "cancelled", "cancel_requested"].includes(run.status);
417
+ const progress = $("#run-progress");
418
+ if (progress) {
419
+ clear(progress);
420
+ const stages = [
421
+ ["消息已提交", "done"],
422
+ ["等待回复", hasText || finished ? "done" : "active"],
423
+ [finished && !hasText ? "未记录流式输出" : "生成回复", hasText ? (finished ? "done" : "active") : finished ? "skipped" : "waiting"],
424
+ [run.status === "failed" ? "执行失败" : run.status === "cancelled" ? "已停止" : noReply ? "未产生回复" : "完成", finished ? (interrupted ? "error" : "done") : "waiting"],
425
+ ];
426
+ if (interrupted) stages.forEach(stage => { if (stage[1] === "active") stage[1] = "error"; });
427
+ for (const [label, status] of stages) progress.append(node("li", { "data-stage": status }, label));
428
+ }
368
429
  clear(elements.waterfall);
369
- const accept = Number.isFinite(run.acceptLatencyMs) ? run.acceptLatencyMs : 0;
370
- const first = Number.isFinite(run.firstTextLatencyMs) ? run.firstTextLatencyMs : undefined;
371
- const total = Number.isFinite(run.responseTimeMs)
372
- ? run.responseTimeMs
373
- : Math.max(accept, Date.now() - new Date(run.startedAt).getTime());
374
- const waitingForText = Math.max(0, (first ?? total) - accept);
375
- const streaming = first === undefined ? 0 : Math.max(0, total - first);
376
- const maximum = Math.max(total, 1);
377
- const rows = [
378
- ["App Server 接收", accept],
379
- [first === undefined ? "等待首段文字" : "首段文字前", waitingForText],
380
- [run.status === "completed" ? "完整回复生成" : "回复生成中", streaming],
381
- ];
382
- for (const [label, duration] of rows) {
430
+ // Only report observed timings. A finished run with missing telemetry never keeps counting.
431
+ const timingRows = [["消息接收", run.acceptLatencyMs], ["首段文字", run.firstTextLatencyMs], ["总耗时", run.responseTimeMs]];
432
+ const maximum = Math.max(1, ...timingRows.map(([,value]) => Number.isFinite(value) ? value : 0));
433
+ elements.waterfall.hidden = !timingRows.some(([,value]) => Number.isFinite(value));
434
+ for (const [label, duration] of timingRows) {
435
+ if (!Number.isFinite(duration)) continue;
383
436
  const row = node("div", { class: "waterfall-row" });
384
437
  row.append(node("span", {}, label));
385
438
  const track = node("span", { class: "waterfall-track" });
386
439
  const fill = node("span", { class: "waterfall-fill" });
387
- fill.style.transform = `scaleX(${Math.max(0.015, duration / maximum)})`;
440
+ fill.style.transform = `scaleX(${duration / maximum})`;
388
441
  track.append(fill);
389
442
  row.append(track, node("strong", {}, formatDuration(duration)));
390
443
  elements.waterfall.append(row);
391
444
  }
392
445
  elements.runSummaryCopy.textContent = run.status === "failed"
393
- ? "这个本地回合在 Agent 侧执行失败,没有形成完整回复;请到执行时间线和协议日志查看失败位置。"
394
- : run.status === "cancelled"
395
- ? "这个本地回合已被停止,尚未完成的回复已经作废;此前已经生效的平台写操作不会自动回滚。"
396
- : run.completion === "no_reply"
397
- ? "这个本地回合已经结束,但没有观察到新的完整回复。"
398
- : run.status === "completed"
399
- ? `本次本地回合在 ${formatDuration(run.firstTextLatencyMs)} 收到首段文字,并在 ${formatDuration(run.responseTimeMs)} 完成正式回复。`
400
- : run.status === "cancel_requested"
401
- ? "停止请求已经发往 App Server;输入框会在本地回合真正结束后重新开放。"
402
- : first === undefined
403
- ? "当前本地回合正在等待第一段真实输出。收到后,模拟器会立即开始追加文字。"
404
- : "当前本地回合正在生成回复,调试器与 App 模拟器会同步展示文字增量。";
446
+ ? "本轮执行失败。查看执行时间线定位问题,或展开协议日志中的错误详情。"
447
+ : run.status === "cancelled" ? "本轮已停止。已完成的平台写入不会自动撤销。"
448
+ : noReply ? "本轮已结束,没有产生新的完整回复。"
449
+ : run.status === "completed" ? (measuredFirstText ? `回复已完成,首段文字耗时 ${formatDuration(run.firstTextLatencyMs)}。` : "回复已完成,本轮未记录首段文字耗时。")
450
+ : run.status === "cancel_requested" ? "正在停止,结束后可以继续发送消息。"
451
+ : hasText ? "正在生成回复,右侧同步展示输出。" : "消息已提交,等待搭子开始回复。";
405
452
  }
406
453
 
454
+ let timelineSignature = "", contextSignature = "", protocolSignature = "";
455
+ const expandedLogs = new Map();
407
456
  function renderTimeline(run) {
457
+ const signature = JSON.stringify([run?.runId, state.debug.events]);
458
+ if (signature === timelineSignature) return;
459
+ timelineSignature = signature;
408
460
  clear(elements.timelineList);
409
461
  const events = (state.debug.events || []).filter((event) => !run || event.runId === run.runId);
410
462
  if (!events.length) {
@@ -424,6 +476,9 @@
424
476
  }
425
477
 
426
478
  function renderContext() {
479
+ const signature = JSON.stringify(state.simulator.messages);
480
+ if (signature === contextSignature) return;
481
+ contextSignature = signature;
427
482
  clear(elements.contextList);
428
483
  elements.contextSource.textContent = "平台聊天记录";
429
484
  const messages = state.simulator.messages || [];
@@ -433,7 +488,7 @@
433
488
  }
434
489
  for (const message of messages) {
435
490
  const row = node("article", { class: "context-row", "data-role": message.role });
436
- row.append(node("span", { class: "context-role" }, message.role));
491
+ row.append(node("span", { class: "context-role" }, message.role === "user" ? "用户" : message.role === "assistant" ? "搭子" : message.role));
437
492
  row.append(node("p", { class: "context-text" }, message.text));
438
493
  row.append(node("time", { class: "context-time", dateTime: message.createdAt }, formatTime(message.createdAt)));
439
494
  elements.contextList.append(row);
@@ -441,13 +496,33 @@
441
496
  }
442
497
 
443
498
  function renderProtocol() {
499
+ const signature = JSON.stringify(state.debug.events);
500
+ if (signature === protocolSignature) return;
501
+ protocolSignature = signature;
444
502
  clear(elements.protocolLog);
445
503
  const events = state.debug.events || [];
446
504
  if (!events.length) {
447
505
  elements.protocolLog.append(node("p", { class: "timeline-empty" }, "本机还没有协议事件。"));
448
506
  return;
449
507
  }
450
- for (const event of events) {
508
+ for (const [index, event] of events.entries()) {
509
+ if (hosted) {
510
+ const key = event.eventId || `${event.timestamp}:${event.type}:${index}`;
511
+ const row = node("details", { class: "protocol-entry" });
512
+ const error = event.type === "agent.stderr" || /failed|error/.test(event.type);
513
+ row.open = expandedLogs.get(key) ?? error;
514
+ row.dataset.level = error ? "error" : "info";
515
+ row.addEventListener("toggle", () => expandedLogs.set(key, row.open));
516
+ const summary = node("summary");
517
+ summary.append(node("time", { dateTime: event.timestamp }, formatTime(event.timestamp)));
518
+ summary.append(node("span", { class: "log-summary" }, (event.summary || event.type).split("\n")[0]));
519
+ summary.append(node("span", { class: "log-channel" }, event.channel));
520
+ row.append(summary);
521
+ row.append(node("p", { class: "log-meta" }, `${event.type}${event.runId ? ` / ${event.runId}` : ""}${Number.isFinite(event.durationMs) ? ` / ${formatDuration(event.durationMs)}` : ""}`));
522
+ if (event.summary?.includes("\n")) row.append(node("pre", { class: "runtime-log-text" }, event.summary));
523
+ elements.protocolLog.append(row);
524
+ continue;
525
+ }
451
526
  const row = node("div", { class: "protocol-row" });
452
527
  row.append(node("time", { class: "protocol-time", dateTime: event.timestamp }, formatTime(event.timestamp)));
453
528
  row.append(node("span", { class: "protocol-channel" }, event.channel));
@@ -465,6 +540,20 @@
465
540
 
466
541
  let messageSignature = "";
467
542
  let followMessages = true;
543
+ let liveReply, liveReplyText = "";
544
+ function renderLiveReply() {
545
+ if (!liveReply) return;
546
+ const text = state.simulator.streaming?.runId === state.simulator.activeRun?.runId ? state.simulator.streaming?.text || "" : "";
547
+ if (text === liveReplyText) return;
548
+ liveReplyText = text;
549
+ clear(liveReply);
550
+ if (text) { liveReply.className = "message-content"; liveReply.append(node("div", { class: "message-bubble", "aria-label": "搭子正在回复" }, text)); }
551
+ else {
552
+ const indicator = node("div", { class: "replying-indicator", "aria-label": "搭子正在准备回复" });
553
+ indicator.append(node("i"), node("i"), node("i")); liveReply.append(indicator);
554
+ }
555
+ if (followMessages) followLatestMessages();
556
+ }
468
557
 
469
558
  function followLatestMessages() {
470
559
  followMessages = true;
@@ -474,14 +563,15 @@
474
563
 
475
564
  function renderMessages() {
476
565
  renderWidgets();
477
- const signature = JSON.stringify([state.simulator.messages, Boolean(state.simulator.activeRun)]);
478
- if (signature === messageSignature) return;
566
+ const signature = JSON.stringify([state.simulator.messages, state.simulator.activeRun?.runId]);
567
+ if (signature === messageSignature) { renderLiveReply(); return; }
479
568
  messageSignature = signature;
480
569
  const previousTop = elements.chatMessages.scrollTop;
481
570
  clear(elements.chatMessages);
571
+ liveReply = undefined; liveReplyText = "";
482
572
  const messages = state.simulator.messages || [];
483
573
  if (!messages.length && !state.simulator.activeRun) {
484
- elements.chatMessages.append(node("div", { class: "chat-empty", "aria-label": "暂无消息" }));
574
+ elements.chatMessages.append(node("div", { class: "chat-empty", "aria-label": "暂无消息" }, hosted ? (state.dev.phase === "ready" ? "发一条消息,开始体验" : "启动 DEV 后,开始对话") : ""));
485
575
  }
486
576
  let renderedDay;
487
577
  for (const message of messages) {
@@ -499,9 +589,10 @@
499
589
  }
500
590
  if (state.simulator.activeRun) {
501
591
  const row = node("div", { class: "message-row typing-row", "data-role": "assistant" });
502
- const indicator = node("div", { class: "replying-indicator", "aria-label": "搭子正在准备回复" });
503
- indicator.append(node("i"), node("i"), node("i"));
504
- row.append(messageAvatar("assistant"), indicator);
592
+ liveReply = node("div", { class: "message-content" });
593
+ liveReplyText = "\0";
594
+ renderLiveReply();
595
+ row.append(messageAvatar("assistant"), liveReply);
505
596
  elements.chatMessages.append(row);
506
597
  }
507
598
  if (followMessages) followLatestMessages();
@@ -515,6 +606,7 @@
515
606
  renderRuns();
516
607
  renderContext();
517
608
  renderProtocol();
609
+ renderWorkbench();
518
610
  }
519
611
 
520
612
  function renderAll() {
@@ -545,7 +637,8 @@
545
637
  renderContext();
546
638
  await refreshDebug();
547
639
  } catch (cause) {
548
- if (cause?.code !== "SIMULATOR_NOT_READY") showError(cause);
640
+ if (hosted && (!connectionReady || cause?.status === 409)) void refreshConnection();
641
+ else if (cause?.code !== "SIMULATOR_NOT_READY") showError(cause);
549
642
  } finally {
550
643
  state.pollingBusy = false;
551
644
  }
@@ -563,20 +656,26 @@
563
656
 
564
657
  async function controlDev() {
565
658
  if (!connectionReady || state.dev.phase === "starting" || state.dev.phase === "stopping") return;
566
- elements.devControl.disabled = true;
659
+ if (devAction) return;
660
+ devAction = state.dev.phase === "ready" ? "stop" : "start";
661
+ elements.errorBanner.hidden = true;
662
+ renderDev();
567
663
  try {
568
- const path = state.dev.phase === "ready" ? "/api/dev/stop" : "/api/dev/start";
664
+ const path = `/api/dev/${devAction}`;
569
665
  const value = await api(path, { method: "POST" });
570
666
  applyDevState(value.state);
571
667
  if (state.dev.phase !== "ready" && path.endsWith("stop")) state.simulator = { messages: state.simulator.messages || [] };
572
668
  renderAll();
573
669
  } catch (cause) {
574
670
  showError(cause);
671
+ } finally {
672
+ devAction = "";
673
+ renderDev();
575
674
  }
576
675
  }
577
676
 
578
677
  function updateSendButton() {
579
- const ready = state.dev.phase === "ready" && !state.simulator.activeRun && !state.resetting && !state.sending;
678
+ const ready = connectionReady && !devAction && state.simulatorLoaded && state.dev.phase === "ready" && !state.simulator.activeRun && !state.resetting && !state.sending;
580
679
  elements.sendMessage.disabled = !ready || !elements.messageInput.value.trim();
581
680
  }
582
681
 
@@ -585,8 +684,9 @@
585
684
  async function sendMessage(event) {
586
685
  event.preventDefault();
587
686
  const text = elements.messageInput.value;
588
- if (!text.trim() || state.dev.phase !== "ready" || state.simulator.activeRun || state.resetting || state.sending) return;
687
+ if (!text.trim() || !state.simulatorLoaded || devAction || state.dev.phase !== "ready" || state.simulator.activeRun || state.resetting || state.sending) return;
589
688
  state.sending = true;
689
+ elements.errorBanner.hidden = true;
590
690
  followLatestMessages();
591
691
  renderDev();
592
692
  elements.messageInput.disabled = true;
@@ -693,6 +793,7 @@
693
793
  function applySimulatorState(next) {
694
794
  if ((next.conversationRevision || 0) < (state.simulator.conversationRevision || 0)) return false;
695
795
  state.simulator = next;
796
+ state.simulatorLoaded = true;
696
797
  return true;
697
798
  }
698
799
 
@@ -700,6 +801,7 @@
700
801
  if (elements.resetConversation.disabled) return;
701
802
  if (!window.confirm("清空当前对话并重新开始?\n这会结束当前开发版会话,重置聊天上下文、记忆和该会话的主动服务。旧记录仅在服务端留档;搭子资料和工程文件会保留。")) return;
702
803
  state.resetting = true;
804
+ elements.errorBanner.hidden = true;
703
805
  widgets?.reset();
704
806
  state.simulatorRequest++;
705
807
  elements.resetConversation.textContent = "正在清空…";
@@ -781,7 +883,7 @@
781
883
  widgets?.reset();
782
884
  void togglePairing(false);
783
885
  renderDev(); syncPolling();
784
- $("#local-status").textContent = message;
886
+ $("#local-status").textContent = "本地工程";
785
887
  }
786
888
 
787
889
  async function refreshConnection() {
@@ -791,6 +893,7 @@
791
893
  const response = await fetch(connectionUrl, { signal: AbortSignal.timeout(5000) });
792
894
  const value = await response.json();
793
895
  if (!response.ok) throw new Error(value.error || "连接状态读取失败");
896
+ connectionChecked = true; connectionFailure = "";
794
897
  $("#connect-command").textContent = value.command;
795
898
  $("#copy-connect-command").disabled = false;
796
899
  if (!value.connected) { disconnected(); return; }
@@ -798,17 +901,29 @@
798
901
  const newlyConnected = !connectionReady || changed;
799
902
  connectionInstance = value.instanceId;
800
903
  connectionReady = true;
801
- $("#local-status").textContent = ["已连接本地工程", value.machineName, value.projectRoot].filter(Boolean).join(" · ");
904
+ const projectName = (value.projectRoot || "").split(/[\\/]/).filter(Boolean).at(-1) || "本地工程";
905
+ $("#local-status").textContent = projectName;
906
+ $("#local-status").title = value.projectRoot || "本地工程";
907
+ $("#machine-name").textContent = value.machineName || "未提供";
908
+ $("#project-root").textContent = value.projectRoot || "未提供";
802
909
  if (newlyConnected) {
910
+ $("#connect-instructions").hidden = true;
911
+ $("#get-connect-command").setAttribute("aria-expanded", "false");
803
912
  state.dev = { phase: "idle", revision: -1 };
804
913
  if (changed) {
805
- state.simulator = { messages: [] }; state.debug = { runs: [], events: [], messages: [] };
914
+ state.simulator = { messages: [] }; state.simulatorLoaded = false; state.debug = { runs: [], events: [], messages: [] };
806
915
  state.selectedRunId = ""; pendingSend = undefined; state.buddy = undefined;
807
916
  }
808
917
  elements.errorBanner.hidden = true;
918
+ renderDev();
809
919
  await boot();
810
920
  }
811
- } catch (cause) { disconnected(cause.message || "连接已中断,正在重试…"); }
921
+ } catch (cause) {
922
+ const message = cause.message || "连接已中断,正在重试…";
923
+ connectionChecked = true; connectionFailure = message;
924
+ disconnected(message);
925
+ if (!$("#connect-instructions").hidden) { $("#connect-command").textContent = message; $("#copy-connect-command").disabled = true; }
926
+ }
812
927
  finally { connectionBusy = false; }
813
928
  }
814
929
 
@@ -818,14 +933,29 @@
818
933
  try {
819
934
  const [dev, debug] = await Promise.all([api("/api/dev"), api("/api/debug")]);
820
935
  applyDevState(dev.state); state.debug = debug.debug; renderAll();
821
- } catch (cause) { showError(cause); }
936
+ } catch (cause) {
937
+ // Polling conflicts are connection transitions, already explained by the workbench state.
938
+ if (!connectionReady || cause?.status === 409) void refreshConnection();
939
+ else showError(cause);
940
+ }
822
941
  finally { devPollBusy = false; }
823
942
  }
824
943
 
825
944
  if (hosted) {
826
945
  renderDev();
946
+ const engineeringDetails = $(".connection-details");
947
+ document.addEventListener("pointerdown", event => {
948
+ if (engineeringDetails.open && !engineeringDetails.contains(event.target)) engineeringDetails.open = false;
949
+ });
950
+ document.addEventListener("keydown", event => {
951
+ if (event.key === "Escape" && engineeringDetails.open) {
952
+ engineeringDetails.open = false;
953
+ engineeringDetails.querySelector("summary").focus();
954
+ }
955
+ });
827
956
  $("#get-connect-command").addEventListener("click", () => {
828
957
  $("#connect-instructions").hidden = !$("#connect-instructions").hidden;
958
+ $("#get-connect-command").setAttribute("aria-expanded", String(!$("#connect-instructions").hidden));
829
959
  void refreshConnection();
830
960
  });
831
961
  $("#copy-connect-command").addEventListener("click", async () => {
@@ -93,10 +93,10 @@
93
93
  <h2 id="run-title">尚无本地回合</h2>
94
94
  <span class="status-pill" id="run-status" data-status="idle">等待消息</span>
95
95
  </div>
96
- <div class="run-reference" id="run-reference" hidden>
96
+ <details class="run-reference" id="run-reference" hidden><summary>回合标识</summary>
97
97
  <span id="run-id"></span>
98
98
  <button type="button" class="text-button" id="copy-run-id" disabled>复制 ID</button>
99
- </div>
99
+ </details>
100
100
  </div>
101
101
  <div class="run-controls">
102
102
  <label class="run-picker-label" for="run-picker">切换回合</label>
@@ -112,10 +112,10 @@
112
112
  </div>
113
113
 
114
114
  <dl class="run-metrics" aria-label="本地回合指标">
115
- <div><dt>接收</dt><dd id="metric-accept">—</dd></div>
116
- <div><dt>首段文字</dt><dd id="metric-first">—</dd></div>
117
- <div><dt>完整耗时</dt><dd id="metric-response">—</dd></div>
118
- <div><dt>消息</dt><dd id="metric-messages">0</dd></div>
115
+ <div><dt>接收</dt><dd id="metric-accept">未记录</dd></div>
116
+ <div><dt>首段文字</dt><dd id="metric-first">未记录</dd></div>
117
+ <div><dt>完整耗时</dt><dd id="metric-response">未记录</dd></div>
118
+ <div><dt>会话消息</dt><dd id="metric-messages">0</dd></div>
119
119
  </dl>
120
120
 
121
121
  </div>
@@ -125,15 +125,14 @@
125
125
  <div class="overview-grid">
126
126
  <article class="run-summary">
127
127
  <div class="section-heading">
128
- <div><h3>这一次发生了什么</h3><p>从消息到回复,按真实事件还原。</p></div>
129
- <span class="source-label">本机观测</span>
128
+ <div><h3>执行过程</h3></div>
130
129
  </div>
131
130
  <div class="empty-state" id="overview-empty">
132
- <span class="empty-glyph" aria-hidden="true">↗</span>
133
131
  <h4>发一条消息,开始记录</h4>
134
- <p>这里会按本地回合展示真实的接收、首段文字、生成过程和完成状态。</p>
132
+ <p>右侧发送消息后,这里显示本次执行过程。</p>
135
133
  </div>
136
134
  <div id="overview-content" hidden>
135
+ <ol class="run-progress" id="run-progress" aria-label="回合执行进度"></ol>
137
136
  <div class="waterfall" id="waterfall" aria-label="本地回合耗时分布"></div>
138
137
  <p class="run-summary-copy" id="run-summary-copy"></p>
139
138
  </div>
@@ -144,8 +143,8 @@
144
143
  <h3>DEV · 私聊</h3>
145
144
  <dl>
146
145
  <div><dt>Agent</dt><dd id="environment-agent">等待启动</dd></div>
147
- <div><dt>Runtime</dt><dd id="environment-runtime">—</dd></div>
148
- <div><dt>模型</dt><dd id="environment-model">—</dd></div>
146
+ <div><dt>Runtime</dt><dd id="environment-runtime">未记录</dd></div>
147
+ <div><dt>模型</dt><dd id="environment-model">未记录</dd></div>
149
148
  </dl>
150
149
  </article>
151
150
  </div>
@@ -153,15 +152,14 @@
153
152
 
154
153
  <section id="panel-timeline" class="debug-panel" role="tabpanel" aria-labelledby="tab-timeline" hidden>
155
154
  <div class="section-heading">
156
- <div><h3>执行时间线</h3><p>按发生顺序查看消息发送、流式输出和会话状态。</p></div>
157
- <span class="source-label">真实事件</span>
155
+ <div><h3>执行时间线</h3><p>查看当前回合的事件。</p></div>
158
156
  </div>
159
157
  <ol class="timeline" id="timeline-list"></ol>
160
158
  </section>
161
159
 
162
160
  <section id="panel-context" class="debug-panel" role="tabpanel" aria-labelledby="tab-context" hidden>
163
161
  <div class="section-heading">
164
- <div><h3>当前对话记录</h3><p>展示平台保存的当前对话消息。</p></div>
162
+ <div><h3>当前对话记录</h3><p>包含当前会话的历史消息,不限于所选回合。</p></div>
165
163
  <span class="source-label" id="context-source">平台聊天记录</span>
166
164
  </div>
167
165
  <div class="context-list" id="context-list"></div>
@@ -169,7 +167,7 @@
169
167
 
170
168
  <section id="panel-protocol" class="debug-panel" role="tabpanel" aria-labelledby="tab-protocol" hidden>
171
169
  <div class="section-heading">
172
- <div><h3>协议日志</h3><p>Runtime 输出、错误堆栈与 MLB 通信事件。</p></div>
170
+ <div><h3>协议日志</h3><p>查看当前本地连接的完整日志。</p></div>
173
171
  <button class="text-button" id="copy-protocol" type="button">复制可见日志</button>
174
172
  </div>
175
173
  <div class="protocol-log" id="protocol-log" role="log" aria-live="polite"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@my-life-buddies/cli",
3
- "version": "0.13.0",
3
+ "version": "0.13.3",
4
4
  "type": "module",
5
5
  "description": "搭搭 Developer Platform 命令行工具",
6
6
  "publishConfig": {
@@ -39,7 +39,7 @@
39
39
  "ws": "8.21.3"
40
40
  },
41
41
  "devDependencies": {
42
- "@buddy/cli-core": "0.13.0",
43
- "@buddy/preview": "0.13.0"
42
+ "@buddy/cli-core": "0.13.3",
43
+ "@buddy/preview": "0.13.3"
44
44
  }
45
45
  }