dsh-ntr 0.3.0 → 0.3.1

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/lib/bridge.js CHANGED
@@ -329,7 +329,7 @@ export class AgentBridge {
329
329
  this.touch();
330
330
  return;
331
331
  }
332
- this.releaseAll(`agent "${this.def.id}" was released after ${String(Math.round(this.currentIdleTimeoutMs / 1000))}s idle`);
332
+ this.releaseAll(`agent "${this.def.id}" was released after ${String(Math.round(this.currentIdleTimeoutMs / 1000))}s idle`, 'idle');
333
333
  }
334
334
  /**
335
335
  * Claim this bridge's single prompt slot for one turn, waiting (bounded) for
@@ -532,7 +532,7 @@ export class AgentBridge {
532
532
  if (this.state === 'ready' && this.conn !== undefined && this.spawnCwd !== spawnCwd && this.isQuiet()) {
533
533
  const reason = `agent "${this.def.id}" restarts in ${spawnCwd} (the workspace of the session using it)`;
534
534
  this.hub.broadcast({ type: 'agent/notice', agentId: this.def.id, level: 'info', text: `acp: ${reason}` });
535
- this.releaseAll(reason);
535
+ this.releaseAll(reason, 'workspace');
536
536
  }
537
537
  }
538
538
  if (this.state === 'ready' && this.conn !== undefined)
@@ -979,12 +979,12 @@ export class AgentBridge {
979
979
  this.sessions.delete(session.sessionId);
980
980
  session.released = true;
981
981
  this.released.set(session.sessionId, session);
982
- this.hub.broadcast({ type: 'session/released', sessionId: session.sessionId, session: snapshot(session), reason: 'resume failed' });
982
+ this.hub.broadcast({ type: 'session/released', sessionId: session.sessionId, session: snapshot(session), reason: 'resume failed', cause: 'resume-failed' });
983
983
  throw error;
984
984
  }
985
985
  }
986
986
  /** Release all live sessions (process died or went idle): keep them resumable, settle permissions, drop the connection. */
987
- releaseAll(reason) {
987
+ releaseAll(reason, cause) {
988
988
  this.clearIdleTimer();
989
989
  for (const session of [...this.sessions.values()]) {
990
990
  // A session already being closed is `closeSession`'s to delete. Marking
@@ -1005,7 +1005,7 @@ export class AgentBridge {
1005
1005
  session.released = true;
1006
1006
  this.settlePermissions(session);
1007
1007
  this.released.set(session.sessionId, session);
1008
- this.hub.broadcast({ type: 'session/released', sessionId: session.sessionId, session: snapshot(session), reason });
1008
+ this.hub.broadcast({ type: 'session/released', sessionId: session.sessionId, session: snapshot(session), reason, cause });
1009
1009
  }
1010
1010
  this.sessions.clear();
1011
1011
  this.acpIndex.clear();
@@ -4654,7 +4654,7 @@ export class BridgeManager {
4654
4654
  // here (their `runPrompt` promise also rejects into settle, but this also
4655
4655
  // covers a promise the connection teardown leaves pending).
4656
4656
  this.disposeTurnStreamsOf(controller => controller.session.agentId === bridge.def.id);
4657
- bridge.releaseAll(reason);
4657
+ bridge.releaseAll(reason, 'died');
4658
4658
  }
4659
4659
  agentViews() {
4660
4660
  return [...this.defsMap.values()].map(def => (this.bridges.get(def.id) ?? this.makeBridge(def, () => { })).view());
@@ -5370,7 +5370,7 @@ export class BridgeManager {
5370
5370
  // rejection does not broadcast a second `turn/end` for a turn already
5371
5371
  // finalized by the watchdog (`LiveSession.reclaimedTurnSeq`).
5372
5372
  session.reclaimedTurnSeq = session.turnSeq;
5373
- bridge.releaseAll('agent did not acknowledge the stalled turn');
5373
+ bridge.releaseAll('agent did not acknowledge the stalled turn', 'stalled');
5374
5374
  session.orphanPromptTurnSeq = undefined;
5375
5375
  return { ok: true };
5376
5376
  }
@@ -17,7 +17,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
17
17
  */
18
18
  import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
19
19
  import { formatCommandLine, isAbsolutePathLike, splitCommandLine } from "./acp-ui.js";
20
- import { DEFAULT_KNOWN_PRESETS } from "./store.js";
20
+ import { DEFAULT_KNOWN_PRESETS, LOG_MAX_ENTRIES } from "./store.js";
21
21
  /**
22
22
  * Mirrors `AGENT_ID_PATTERN` in impl.ts — the client cannot import host-side
23
23
  * modules, so this copy must stay in sync (same deal as the preset copies,
@@ -242,6 +242,17 @@ const STYLE = `
242
242
  .dwa-deadline-flag { margin-top: 4px; font-size: var(--ntr-font-xs); font-weight: 550; color: var(--ntr-warn); }
243
243
  .dwa-deadline-warn { margin-top: 10px; padding: 9px 12px; border-left: 3px solid var(--dsw-alias-state-warn-primary, #d97706); background: var(--dsw-alias-state-warn-secondary, #fffbeb); border-radius: var(--ntr-radius-sm); font-size: var(--ntr-font-xs); line-height: 1.55; }
244
244
  .dwa-deadline-reset { display: flex; justify-content: flex-end; margin-top: 8px; }
245
+ /* Runtime log: one bounded list, newest first. The time column never wraps;
246
+ the message is the only cell that may break anywhere. */
247
+ .dwa-log-list { list-style: none; margin: 0; padding: 0; border: var(--ntr-line); border-radius: var(--ntr-radius); overflow: hidden; }
248
+ .dwa-log-row { display: flex; align-items: baseline; gap: 8px; padding: 6px 10px; font-size: var(--ntr-font-sm); border-top: var(--ntr-line); min-width: 0; }
249
+ .dwa-log-row:first-child { border-top: 0; }
250
+ .dwa-log-row.is-error { background: var(--dsw-alias-state-error-tertiary, #fef2f2); }
251
+ .dwa-log-time { color: var(--ntr-muted); white-space: nowrap; }
252
+ .dwa-log-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
253
+ .dwa-log-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ntr-danger); display: inline-block; margin-left: 6px; }
254
+ .dwa-log-level.is-info { background: var(--dsw-alias-state-business-tertiary, #dbeafe); color: var(--dsw-alias-state-business-primary, #2563eb); }
255
+ .dwa-log-level.is-error { background: var(--dsw-alias-state-error-tertiary, #fef2f2); color: var(--ntr-danger); }
245
256
  @media (max-width: 640px) {
246
257
  .dwa-root { padding: 20px 16px; }
247
258
  }
@@ -726,6 +737,17 @@ function ApprovalDeadlinesCard(props) {
726
737
  const waitMode = deadlines.mode === 'wait';
727
738
  return (_jsxs("section", { className: "dwa-section", children: [_jsx("div", { className: "dwa-section-heading", children: _jsxs("div", { children: [_jsx("h3", { className: "dwa-section-title", children: acp.label('审批时限', 'Approval deadlines') }), _jsx("p", { className: "dwa-section-copy", children: acp.label('权限请求的等待时长与超时处理方式。', 'How long to wait for a permission request, and what happens when nobody answers.') })] }) }), _jsxs("div", { className: "dwa-preferences", children: [_jsx(DeadlineModeBlock, { acp: acp, mode: deadlines.mode, override: modeNote }), _jsx(DeadlineSliderBlock, { acp: acp, value: waitMode ? undefined : deadlines.permissionTimeoutMs, mode: deadlines.mode, override: baselineNote }), facts.overridden && (_jsx("div", { className: "dwa-deadline-reset", children: _jsx("button", { type: "button", className: "dwa-btn dwa-btn-secondary", onClick: () => void acp.resetApprovalDeadlines(), children: acp.label('恢复配置文件的值', 'Use config file value') }) }))] })] }));
728
739
  }
740
+ /**
741
+ * Local wall-clock `HH:MM:SS` for one runtime-log line. Hand-built on purpose:
742
+ * `toLocaleTimeString` may drop seconds or pick a 12-hour clock depending on
743
+ * the browser locale, which makes the column ragged and ambiguous.
744
+ */
745
+ function logClock(at) {
746
+ const date = new Date(at);
747
+ return [date.getHours(), date.getMinutes(), date.getSeconds()]
748
+ .map(value => String(value).padStart(2, '0'))
749
+ .join(':');
750
+ }
729
751
  /** The states that carry an update badge next to the version badge. */
730
752
  function updateBadgeText(update, acp) {
731
753
  if (update.status === 'available') {
@@ -835,6 +857,12 @@ export function AcpSettingsSection(props) {
835
857
  useEffect(() => {
836
858
  void acp.checkPluginUpdate();
837
859
  }, [acp]);
860
+ // Opening the log is the read receipt. The store does not mutate at zero,
861
+ // so this cannot loop.
862
+ useEffect(() => {
863
+ if (page === 'logs' && ui.logUnread > 0)
864
+ acp.markLogsRead();
865
+ }, [acp, page, ui.logUnread]);
838
866
  const { configured, available } = partitionAgents(ui.detectedAgents, ui.agents);
839
867
  const selected = addTarget ?? available[0] ?? NEW_AGENT_SEED;
840
868
  const openAdd = () => {
@@ -855,7 +883,7 @@ export function AcpSettingsSection(props) {
855
883
  && update.status !== 'not-installed';
856
884
  return (_jsxs("div", { className: "dwa-root", children: [_jsxs("header", { className: "dwa-page-header", children: [_jsxs("div", { className: "dwa-page-heading", children: [_jsxs("h2", { className: "dwa-page-title", "aria-label": acp.label('NTR 设置', 'NTR settings'), children: [acp.label('NTR 设置', 'NTR settings'), ui.pluginVersion !== undefined && (versionOpensUpdate
857
885
  ? (_jsx("button", { type: "button", className: "dwa-badge dwa-version dwa-version-btn", title: versionTitle, "aria-expanded": updateOpen, "aria-controls": "ntr-plugin-update-card", onClick: () => setUpdateOpen(open => !open), children: `v${ui.pluginVersion}` }))
858
- : _jsx("span", { className: "dwa-badge dwa-version", title: versionTitle, children: `v${ui.pluginVersion}` })), update !== undefined && (_jsx(PluginUpdateBadge, { acp: acp, update: update, open: updateOpen, onToggle: () => setUpdateOpen(open => !open) }))] }), _jsx("p", { className: "dwa-page-description", children: acp.label('连接智能体,设置你的对话偏好。', 'Connect agents and set your conversation preferences.') })] }), page === 'agents' && configured.length > 0 && (_jsx("button", { type: "button", className: "dwa-btn dwa-btn-primary", onClick: openAdd, children: acp.label('添加智能体', 'Add agent') }))] }), update !== undefined && updateCardVisible(update, updateOpen) && (_jsx(PluginUpdateCard, { acp: acp, update: update, onClose: () => setUpdateOpen(false) })), ledger.visible && (_jsxs("div", { className: "dwa-card dwa-card-warn", style: { marginBottom: 16 }, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 4 }, children: acp.label('待处理权限请求', 'Pending Permission Requests') }), _jsx("div", { className: "dwa-hint", style: { marginBottom: 8 }, children: acp.label('对话打开时在那边的卡片上回答;这里兜底显示其他会话的询问。', 'Answer in the conversation card while that conversation is open; this list covers the asks whose conversation is not.') }), ui.permissions.map(permission => (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6, flexWrap: 'wrap' }, children: [_jsxs("span", { style: { flex: 1, minWidth: 160 }, children: [_jsx("span", { style: { fontWeight: 600 }, children: permission.toolName }), permission.reason !== undefined && (_jsxs("span", { style: { color: 'var(--ntr-muted)' }, children: [" \u00B7 ", permission.reason] }))] }), permission.options.map(option => (_jsx("button", { type: "button", className: "dwa-btn", onClick: () => { void acp.respondPermission(permission.requestId, option.optionId).catch(() => undefined); }, children: option.name }, option.optionId))), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-quiet", onClick: () => { void acp.respondPermission(permission.requestId).catch(() => undefined); }, children: acp.label('取消', 'Cancel') })] }, permission.requestId)))] })), ui.notice !== undefined && (_jsxs("div", { className: "dwa-card dwa-card-warn", style: { marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }, children: [_jsx("span", { style: { flex: 1 }, children: ui.notice }), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-quiet", "aria-label": acp.label('关闭通知', 'Dismiss notice'), onClick: () => acp.clearNotice(), children: "\u2715" })] })), presetProblem && (_jsxs("div", { className: "dwa-card dwa-card-warn dwa-notice", role: "status", children: [_jsx("span", { children: acp.label('NTR 模式需要检查,当前可能无法开始对话。', 'NTR mode needs attention; new conversations may be unavailable.') }), _jsx("button", { type: "button", className: "dwa-btn", onClick: () => setPage('diagnostics'), children: acp.label('查看诊断', 'View diagnostics') })] })), _jsxs("nav", { className: "dwa-tabs", "aria-label": acp.label('NTR 设置分类', 'NTR settings categories'), children: [_jsx("button", { type: "button", "aria-pressed": page === 'agents', onClick: () => setPage('agents'), children: acp.label('智能体', 'Agents') }), ui.approvalDeadlines !== undefined && _jsx("button", { type: "button", "aria-pressed": page === 'approvals', onClick: () => setPage('approvals'), children: acp.label('审批时限', 'Approval deadlines') }), _jsx("button", { type: "button", "aria-pressed": page === 'appearance', onClick: () => setPage('appearance'), children: acp.label('对话外观', 'Appearance') })] }), page === 'diagnostics' && (_jsxs("section", { className: "dwa-section", children: [_jsx("h3", { className: "dwa-section-title", children: acp.label('连接诊断', 'Connection diagnostics') }), _jsxs("div", { className: "dwa-preference-row", children: [_jsx("span", { children: acp.label('宿主事件流', 'Host event stream') }), _jsx("span", { className: "dwa-hint", children: streamText(ui.stream, acp) })] }), _jsxs("div", { className: "dwa-row-actions", children: [_jsx("button", { type: "button", className: "dwa-btn", onClick: () => { void acp.resync(); void acp.detectAgents(); }, disabled: ui.detecting, children: ui.detecting ? acp.label('检测中...', 'Detecting...') : acp.label('重新检测与刷新', 'Re-detect & Refresh') }), ui.detectSummary !== undefined && _jsx("span", { className: "dwa-hint", role: "status", children: ui.detectSummary })] })] })), page === 'diagnostics' && ui.preset !== undefined && (_jsxs("div", { className: "dwa-card dwa-mode-banner", children: [_jsxs("div", { className: "dwa-mode-title", children: [acp.label('NTR 模式', 'NTR mode'), _jsx("span", { className: "dwa-badge", children: ui.preset.id })] }), _jsx("span", { className: `dwa-badge ${ui.preset.installed ? 'dwa-badge-success' : 'dwa-badge-warn'}`, children: ui.preset.installed ? acp.label('已就绪', 'Ready') : acp.label('需检查', 'Needs attention') }), _jsxs("div", { className: "dwa-mode-copy", children: [ui.preset.installed
886
+ : _jsx("span", { className: "dwa-badge dwa-version", title: versionTitle, children: `v${ui.pluginVersion}` })), update !== undefined && (_jsx(PluginUpdateBadge, { acp: acp, update: update, open: updateOpen, onToggle: () => setUpdateOpen(open => !open) }))] }), _jsx("p", { className: "dwa-page-description", children: acp.label('连接智能体,设置你的对话偏好。', 'Connect agents and set your conversation preferences.') })] }), page === 'agents' && configured.length > 0 && (_jsx("button", { type: "button", className: "dwa-btn dwa-btn-primary", onClick: openAdd, children: acp.label('添加智能体', 'Add agent') }))] }), update !== undefined && updateCardVisible(update, updateOpen) && (_jsx(PluginUpdateCard, { acp: acp, update: update, onClose: () => setUpdateOpen(false) })), ledger.visible && (_jsxs("div", { className: "dwa-card dwa-card-warn", style: { marginBottom: 16 }, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 4 }, children: acp.label('待处理权限请求', 'Pending Permission Requests') }), _jsx("div", { className: "dwa-hint", style: { marginBottom: 8 }, children: acp.label('对话打开时在那边的卡片上回答;这里兜底显示其他会话的询问。', 'Answer in the conversation card while that conversation is open; this list covers the asks whose conversation is not.') }), ui.permissions.map(permission => (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6, flexWrap: 'wrap' }, children: [_jsxs("span", { style: { flex: 1, minWidth: 160 }, children: [_jsx("span", { style: { fontWeight: 600 }, children: permission.toolName }), permission.reason !== undefined && (_jsxs("span", { style: { color: 'var(--ntr-muted)' }, children: [" \u00B7 ", permission.reason] }))] }), permission.options.map(option => (_jsx("button", { type: "button", className: "dwa-btn", onClick: () => { void acp.respondPermission(permission.requestId, option.optionId).catch(() => undefined); }, children: option.name }, option.optionId))), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-quiet", onClick: () => { void acp.respondPermission(permission.requestId).catch(() => undefined); }, children: acp.label('取消', 'Cancel') })] }, permission.requestId)))] })), ui.notice !== undefined && (_jsxs("div", { className: "dwa-card dwa-card-warn", style: { marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }, children: [_jsx("span", { style: { flex: 1 }, children: ui.notice }), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-quiet", "aria-label": acp.label('关闭通知', 'Dismiss notice'), onClick: () => acp.clearNotice(), children: "\u2715" })] })), presetProblem && (_jsxs("div", { className: "dwa-card dwa-card-warn dwa-notice", role: "status", children: [_jsx("span", { children: acp.label('NTR 模式需要检查,当前可能无法开始对话。', 'NTR mode needs attention; new conversations may be unavailable.') }), _jsx("button", { type: "button", className: "dwa-btn", onClick: () => setPage('diagnostics'), children: acp.label('查看诊断', 'View diagnostics') })] })), _jsxs("nav", { className: "dwa-tabs", "aria-label": acp.label('NTR 设置分类', 'NTR settings categories'), children: [_jsx("button", { type: "button", "aria-pressed": page === 'agents', onClick: () => setPage('agents'), children: acp.label('智能体', 'Agents') }), ui.approvalDeadlines !== undefined && _jsx("button", { type: "button", "aria-pressed": page === 'approvals', onClick: () => setPage('approvals'), children: acp.label('审批时限', 'Approval deadlines') }), _jsx("button", { type: "button", "aria-pressed": page === 'appearance', onClick: () => setPage('appearance'), children: acp.label('对话外观', 'Appearance') }), _jsxs("button", { type: "button", "aria-pressed": page === 'logs', onClick: () => setPage('logs'), children: [acp.label('日志', 'Logs'), ui.logUnread > 0 && _jsx("span", { className: "dwa-log-dot", "aria-hidden": "true" })] })] }), page === 'diagnostics' && (_jsxs("section", { className: "dwa-section", children: [_jsx("h3", { className: "dwa-section-title", children: acp.label('连接诊断', 'Connection diagnostics') }), _jsxs("div", { className: "dwa-preference-row", children: [_jsx("span", { children: acp.label('宿主事件流', 'Host event stream') }), _jsx("span", { className: "dwa-hint", children: streamText(ui.stream, acp) })] }), _jsxs("div", { className: "dwa-row-actions", children: [_jsx("button", { type: "button", className: "dwa-btn", onClick: () => { void acp.resync(); void acp.detectAgents(); }, disabled: ui.detecting, children: ui.detecting ? acp.label('检测中...', 'Detecting...') : acp.label('重新检测与刷新', 'Re-detect & Refresh') }), ui.detectSummary !== undefined && _jsx("span", { className: "dwa-hint", role: "status", children: ui.detectSummary })] })] })), page === 'diagnostics' && ui.preset !== undefined && (_jsxs("div", { className: "dwa-card dwa-mode-banner", children: [_jsxs("div", { className: "dwa-mode-title", children: [acp.label('NTR 模式', 'NTR mode'), _jsx("span", { className: "dwa-badge", children: ui.preset.id })] }), _jsx("span", { className: `dwa-badge ${ui.preset.installed ? 'dwa-badge-success' : 'dwa-badge-warn'}`, children: ui.preset.installed ? acp.label('已就绪', 'Ready') : acp.label('需检查', 'Needs attention') }), _jsxs("div", { className: "dwa-mode-copy", children: [ui.preset.installed
859
887
  ? acp.label('已加入宿主模式列表,新建会话时选择「NTR 模式」即可使用。', 'Added to the harness roster. Pick “NTR mode” when creating a session.')
860
888
  : acp.label('宿主模式列表中没有可用的「NTR 模式」,请检查预设声明。', 'The harness roster has no usable “NTR mode”. Check the preset declaration.'), _jsx("code", { className: "dwa-mode-source dwa-mono", children: ui.preset.source })] }), ui.preset.problem !== undefined && (_jsx("div", { className: "dwa-hint", style: { gridColumn: '1 / -1', color: 'var(--ntr-warn)' }, children: acp.label(`宿主报告:${ui.preset.problem}`, `Harness reported: ${ui.preset.problem}`) }))] })), _jsxs("section", { className: "dwa-section", hidden: page !== 'agents', "aria-label": acp.label('已接入的智能体', 'Connected agents'), children: [_jsxs("div", { className: "dwa-section-heading", children: [_jsx("div", { children: _jsxs("h3", { className: "dwa-section-title", children: [acp.label('已接入的智能体', 'Connected agents'), ui.agentsLoaded && _jsx("span", { className: "dwa-badge", children: configured.length })] }) }), configured.length > 0 && _jsx("span", { className: "dwa-hint", children: acp.label('默认项用于新的 NTR 会话', 'The default serves new NTR conversations') })] }), !ui.agentsLoaded && _jsx("div", { className: "dwa-empty", role: "status", children: acp.label('正在读取智能体…', 'Loading agents…') }), ui.agentsLoaded && configured.length === 0 && (_jsxs("div", { className: "dwa-empty", children: [_jsx("h3", { children: acp.label('接入第一个智能体', 'Connect your first agent') }), _jsx("p", { className: "dwa-section-copy", children: acp.label('选择已有预设,或填写自己的 ACP 启动命令。', 'Choose a preset or enter your own ACP launch command.') }), _jsx("button", { type: "button", className: "dwa-btn dwa-btn-primary", onClick: openAdd, children: acp.label('添加智能体', 'Add agent') })] })), _jsx("div", { className: "dwa-agent-list", children: configured.map(({ info, synthesized }) => {
861
889
  const activeAgent = ui.agents.find(a => a.id === info.id);
@@ -864,7 +892,9 @@ export function AcpSettingsSection(props) {
864
892
  setAddTarget(undefined);
865
893
  setAddOpen(false);
866
894
  setPage('agents');
867
- } }, selected.id)] }))] })] }), _jsx("div", { hidden: page !== 'approvals', children: _jsx(ApprovalDeadlinesCard, { acp: acp, ui: ui }) }), _jsxs("section", { className: "dwa-section", hidden: page !== 'appearance', "aria-label": acp.label('对话外观', 'Appearance'), children: [_jsx("div", { className: "dwa-section-heading", children: _jsxs("div", { children: [_jsx("h3", { className: "dwa-section-title", children: acp.label('对话外观', 'Conversation appearance') }), _jsx("p", { className: "dwa-section-copy", children: acp.label('设置对话中 NTR 挂牌的进程存活提示。', 'Choose how the NTR tag indicates a running agent process.') })] }) }), _jsxs("div", { className: "dwa-preferences", children: [_jsxs("div", { className: "dwa-preference-row", children: [_jsxs("div", { children: [_jsx("div", { className: "dwa-preference-label", children: acp.label('挂牌光晕', 'Tag halo') }), _jsx("div", { className: "dwa-preference-copy", children: acp.label('呼吸为脉动光,常亮为恒定光。', 'Breathe pulses gently; steady keeps a constant glow.') })] }), _jsx("div", { className: "dwa-seg", children: ['breath', 'steady'].map(mode => (_jsx("button", { type: "button", className: ui.tagHalo.mode === mode ? 'is-on' : '', onClick: () => void acp.setTagHalo({ ...ui.tagHalo, mode }), children: mode === 'breath' ? acp.label('呼吸', 'Breathe') : acp.label('常亮', 'Steady') }, mode))) })] }), ui.tagHalo.mode === 'breath' && (_jsxs("div", { className: "dwa-preference-row", children: [_jsxs("div", { children: [_jsx("div", { className: "dwa-preference-label", children: acp.label('呼吸频率', 'Breathing speed') }), _jsx("div", { className: "dwa-preference-copy", children: acp.label('控制光晕的脉动速度。', 'Controls how quickly the halo pulses.') })] }), _jsx("div", { className: "dwa-seg", children: ['slow', 'normal', 'fast'].map(speed => (_jsx("button", { type: "button", className: ui.tagHalo.speed === speed ? 'is-on' : '', onClick: () => void acp.setTagHalo({ ...ui.tagHalo, speed }), children: speed === 'slow' ? acp.label('慢速', 'Slow') : speed === 'normal' ? acp.label('正常', 'Normal') : acp.label('快速', 'Fast') }, speed))) })] }))] })] }), _jsxs("footer", { className: "dwa-health", children: [_jsxs("div", { className: "dwa-health-status", children: [_jsx("span", { className: "dwa-dot", style: { background: ui.stream === 'live' ? 'var(--ntr-success)' : ui.stream === 'down' ? 'var(--ntr-danger)' : 'var(--ntr-muted)' } }), _jsx("span", { children: streamText(ui.stream, acp) }), ui.preset !== undefined && _jsx("span", { children: ui.preset.installed && !presetProblem ? acp.label('NTR 模式已就绪', 'NTR mode ready') : acp.label('NTR 模式需检查', 'NTR mode needs attention') })] }), page !== 'diagnostics' && _jsx("button", { type: "button", className: "dwa-btn dwa-btn-quiet", onClick: () => setPage('diagnostics'), children: acp.label('查看诊断', 'View diagnostics') })] })] }));
895
+ } }, selected.id)] }))] })] }), _jsx("div", { hidden: page !== 'approvals', children: _jsx(ApprovalDeadlinesCard, { acp: acp, ui: ui }) }), _jsxs("section", { className: "dwa-section", hidden: page !== 'appearance', "aria-label": acp.label('对话外观', 'Appearance'), children: [_jsx("div", { className: "dwa-section-heading", children: _jsxs("div", { children: [_jsx("h3", { className: "dwa-section-title", children: acp.label('对话外观', 'Conversation appearance') }), _jsx("p", { className: "dwa-section-copy", children: acp.label('设置对话中 NTR 挂牌的进程存活提示。', 'Choose how the NTR tag indicates a running agent process.') })] }) }), _jsxs("div", { className: "dwa-preferences", children: [_jsxs("div", { className: "dwa-preference-row", children: [_jsxs("div", { children: [_jsx("div", { className: "dwa-preference-label", children: acp.label('挂牌光晕', 'Tag halo') }), _jsx("div", { className: "dwa-preference-copy", children: acp.label('呼吸为脉动光,常亮为恒定光。', 'Breathe pulses gently; steady keeps a constant glow.') })] }), _jsx("div", { className: "dwa-seg", children: ['breath', 'steady'].map(mode => (_jsx("button", { type: "button", className: ui.tagHalo.mode === mode ? 'is-on' : '', onClick: () => void acp.setTagHalo({ ...ui.tagHalo, mode }), children: mode === 'breath' ? acp.label('呼吸', 'Breathe') : acp.label('常亮', 'Steady') }, mode))) })] }), ui.tagHalo.mode === 'breath' && (_jsxs("div", { className: "dwa-preference-row", children: [_jsxs("div", { children: [_jsx("div", { className: "dwa-preference-label", children: acp.label('呼吸频率', 'Breathing speed') }), _jsx("div", { className: "dwa-preference-copy", children: acp.label('控制光晕的脉动速度。', 'Controls how quickly the halo pulses.') })] }), _jsx("div", { className: "dwa-seg", children: ['slow', 'normal', 'fast'].map(speed => (_jsx("button", { type: "button", className: ui.tagHalo.speed === speed ? 'is-on' : '', onClick: () => void acp.setTagHalo({ ...ui.tagHalo, speed }), children: speed === 'slow' ? acp.label('慢速', 'Slow') : speed === 'normal' ? acp.label('正常', 'Normal') : acp.label('快速', 'Fast') }, speed))) })] }))] })] }), page === 'logs' && (_jsxs("section", { className: "dwa-section", "aria-label": acp.label('运行日志', 'Runtime log'), children: [_jsxs("div", { className: "dwa-section-heading", children: [_jsxs("div", { children: [_jsx("h3", { className: "dwa-section-title", children: acp.label('运行日志', 'Runtime log') }), _jsx("p", { className: "dwa-section-copy", children: acp.label(`agent 运行消息,最多 ${String(LOG_MAX_ENTRIES)} 条。`, `Agent runtime messages, up to ${String(LOG_MAX_ENTRIES)}.`) })] }), ui.logs.length > 0 && (_jsx("button", { type: "button", className: "dwa-btn dwa-btn-quiet", onClick: () => acp.clearLogs(), children: acp.label('清空', 'Clear') }))] }), ui.logs.length === 0
896
+ ? _jsx("div", { className: "dwa-empty", role: "status", children: acp.label('暂无运行日志。', 'No runtime messages yet.') })
897
+ : (_jsx("ul", { className: "dwa-log-list", children: [...ui.logs].reverse().map(entry => (_jsxs("li", { className: entry.level === 'error' ? 'dwa-log-row is-error' : 'dwa-log-row', children: [_jsx("span", { className: "dwa-log-time dwa-mono", children: logClock(entry.at) }), _jsx("span", { className: `dwa-badge dwa-log-level ${entry.level === 'error' ? 'is-error' : 'is-info'}`, children: entry.level === 'error' ? acp.label('错误', 'Error') : acp.label('信息', 'Info') }), entry.agentId !== undefined && _jsx("span", { className: "dwa-badge", children: entry.agentId }), _jsx("span", { className: "dwa-log-text", children: entry.text })] }, entry.id))) }))] })), _jsxs("footer", { className: "dwa-health", children: [_jsxs("div", { className: "dwa-health-status", children: [_jsx("span", { className: "dwa-dot", style: { background: ui.stream === 'live' ? 'var(--ntr-success)' : ui.stream === 'down' ? 'var(--ntr-danger)' : 'var(--ntr-muted)' } }), _jsx("span", { children: streamText(ui.stream, acp) }), ui.preset !== undefined && _jsx("span", { children: ui.preset.installed && !presetProblem ? acp.label('NTR 模式已就绪', 'NTR mode ready') : acp.label('NTR 模式需检查', 'NTR mode needs attention') })] }), page !== 'diagnostics' && _jsx("button", { type: "button", className: "dwa-btn dwa-btn-quiet", onClick: () => setPage('diagnostics'), children: acp.label('查看诊断', 'View diagnostics') })] })] }));
868
898
  }
869
899
  /** One row per agent: compact when collapsed, the full form when expanded. */
870
900
  function AgentRow({ detected, activeAgent, acp, isDefault, synthesized, active, newCustom = false, onCreated, }) {
@@ -39,6 +39,8 @@ export function deadlineRemainingLabel(deadlineAt, now, mode) {
39
39
  const rest = seconds % 60;
40
40
  return rest === 0 ? `${String(minutes)}m` : `${String(minutes)}m ${String(rest)}s`;
41
41
  }
42
+ /** Cap on retained runtime-log lines; older entries fall off the front. */
43
+ export const LOG_MAX_ENTRIES = 200;
42
44
  const MAX_BLOCKS = 400;
43
45
  /** How many observed ACP transcripts stay live at once (insertion-ordered eviction). */
44
46
  const MAX_WATCHED = 6;
@@ -339,6 +341,8 @@ export class AcpStore {
339
341
  blocks: [],
340
342
  permissions: [],
341
343
  busy: false,
344
+ logs: [],
345
+ logUnread: 0,
342
346
  detectedAgents: [...DEFAULT_KNOWN_PRESETS],
343
347
  detecting: false,
344
348
  modelCatalogs: {},
@@ -360,6 +364,8 @@ export class AcpStore {
360
364
  };
361
365
  listeners = new Set();
362
366
  source;
367
+ /** Monotonic id source for {@link AcpLogEntry}s (never reset by `clearLogs`). */
368
+ logSeq = 0;
363
369
  lastSeq = new Map();
364
370
  /** Sessions with a finalized turn (dedups the cancel-fallback + late real turn/end pair). */
365
371
  openTurns = new Set();
@@ -445,6 +451,23 @@ export class AcpStore {
445
451
  for (const listener of [...this.listeners])
446
452
  listener();
447
453
  }
454
+ /**
455
+ * Build the log patch for one line. Does NOT mutate: the caller merges it
456
+ * into the single `mutate` call it was already making for the same event.
457
+ */
458
+ logPatch(level, text, agentId) {
459
+ const entry = {
460
+ id: ++this.logSeq,
461
+ at: Date.now(),
462
+ level,
463
+ text,
464
+ ...(agentId === undefined ? {} : { agentId }),
465
+ };
466
+ return {
467
+ logs: [...this.state.logs, entry].slice(-LOG_MAX_ENTRIES),
468
+ logUnread: this.state.logUnread + 1,
469
+ };
470
+ }
448
471
  // ---- Streaming update coalescing ----
449
472
  /**
450
473
  * Live `session/update` frames waiting to be folded. Agents stream at token
@@ -1628,6 +1651,16 @@ export class AcpStore {
1628
1651
  clearNotice() {
1629
1652
  this.mutate({ notice: undefined });
1630
1653
  }
1654
+ /** Drop every retained runtime-log line (the unread counter restarts too). */
1655
+ clearLogs() {
1656
+ this.mutate({ logs: [], logUnread: 0 });
1657
+ }
1658
+ /** The user looked at the log: clear the unread badge. */
1659
+ markLogsRead() {
1660
+ if (this.state.logUnread === 0)
1661
+ return;
1662
+ this.mutate({ logUnread: 0 });
1663
+ }
1631
1664
  flashError(error) {
1632
1665
  this.mutate({ notice: error instanceof Error ? error.message : String(error) });
1633
1666
  }
@@ -1635,7 +1668,7 @@ export class AcpStore {
1635
1668
  applyEvent(event) {
1636
1669
  switch (event.type) {
1637
1670
  case 'agent/notice': {
1638
- this.mutate({ notice: event.text });
1671
+ this.mutate(this.logPatch(event.level, event.text, event.agentId));
1639
1672
  return Promise.resolve();
1640
1673
  }
1641
1674
  case 'agent/status': {
@@ -1666,6 +1699,7 @@ export class AcpStore {
1666
1699
  return Promise.resolve();
1667
1700
  }
1668
1701
  case 'session/closed': {
1702
+ const closingSession = this.state.sessions.find(s => s.sessionId === event.sessionId);
1669
1703
  const sessions = this.state.sessions.filter(s => s.sessionId !== event.sessionId);
1670
1704
  const active = this.state.activeSessionId === event.sessionId ? sessions[0]?.sessionId : this.state.activeSessionId;
1671
1705
  this.openTurns.delete(event.sessionId);
@@ -1681,7 +1715,9 @@ export class AcpStore {
1681
1715
  chatTranscripts: closedTranscripts,
1682
1716
  ...(chatSessions.length !== this.state.chatSessions.length ? { chatSessions } : {}),
1683
1717
  permissions: this.state.permissions.filter(p => p.sessionId !== event.sessionId),
1684
- ...(event.reason === 'agent-died' ? { notice: this.locale('会话因 agent 进程退出而终止', 'session ended: agent process died') } : {}),
1718
+ ...(event.reason === 'agent-died'
1719
+ ? this.logPatch('error', this.locale('会话因 agent 进程退出而终止', 'session ended: agent process died'), closingSession?.agentId)
1720
+ : {}),
1685
1721
  });
1686
1722
  return Promise.resolve();
1687
1723
  }
@@ -1698,6 +1734,22 @@ export class AcpStore {
1698
1734
  const chatSessions = this.state.chatSessions.some(s => s.sessionId === event.sessionId)
1699
1735
  ? this.state.chatSessions.map(s => (s.sessionId === event.sessionId ? { ...s, released: true } : s))
1700
1736
  : this.state.chatSessions;
1737
+ // One runtime-log line per release cause; `idle` stays silent on
1738
+ // purpose (an expected lifecycle transition, not a fault).
1739
+ const cause = event.cause ?? 'died';
1740
+ let log = {};
1741
+ if (cause === 'workspace') {
1742
+ log = this.logPatch('info', this.locale('agent 为切换工作区重启,会话已挂起;再次发送消息将自动恢复。', 'agent restarted for its workspace; the session is paused and will resume on your next message.'), event.session.agentId);
1743
+ }
1744
+ else if (cause === 'resume-failed') {
1745
+ log = this.logPatch('error', this.locale('会话恢复失败;再次发送消息将重试恢复。', 'session resume failed; your next message will retry it.'), event.session.agentId);
1746
+ }
1747
+ else if (cause === 'stalled') {
1748
+ log = this.logPatch('error', this.locale('agent 未响应卡死的回合,会话已挂起;再次发送消息将自动恢复。', 'agent did not acknowledge the stalled turn; the session is paused and will resume on your next message.'), event.session.agentId);
1749
+ }
1750
+ else if (cause === 'died') {
1751
+ log = this.logPatch('error', this.locale('agent 进程已退出,会话已挂起;再次发送消息将自动恢复。', 'agent process died; the session is paused and will resume on your next message.'), event.session.agentId);
1752
+ }
1701
1753
  this.mutate({
1702
1754
  sessions,
1703
1755
  ...(chatSessions !== this.state.chatSessions ? { chatSessions } : {}),
@@ -1705,7 +1757,7 @@ export class AcpStore {
1705
1757
  // An observed transcript keeps its buffer but flips to released, so
1706
1758
  // the ACP view can say "会恢复" instead of silently looking dead.
1707
1759
  ...this.watchedSessionPatch(event.sessionId, event.session),
1708
- notice: this.locale('agent 进程已退出,会话已挂起;再次发送消息将自动恢复。', 'agent process died; the session is paused and will resume on your next message.'),
1760
+ ...log,
1709
1761
  });
1710
1762
  return Promise.resolve();
1711
1763
  }
package/lib/client.js CHANGED
@@ -322,6 +322,8 @@ window.__ModuleLoader__.load({
322
322
  blocks: [],
323
323
  permissions: [],
324
324
  busy: false,
325
+ logs: [],
326
+ logUnread: 0,
325
327
  detectedAgents: [...DEFAULT_KNOWN_PRESETS],
326
328
  detecting: false,
327
329
  modelCatalogs: {},
@@ -343,6 +345,8 @@ window.__ModuleLoader__.load({
343
345
  };
344
346
  listeners = /* @__PURE__ */ new Set();
345
347
  source;
348
+ /** Monotonic id source for {@link AcpLogEntry}s (never reset by `clearLogs`). */
349
+ logSeq = 0;
346
350
  lastSeq = /* @__PURE__ */ new Map();
347
351
  /** Sessions with a finalized turn (dedups the cancel-fallback + late real turn/end pair). */
348
352
  openTurns = /* @__PURE__ */ new Set();
@@ -429,6 +433,23 @@ window.__ModuleLoader__.load({
429
433
  for (const listener of [...this.listeners]) listener();
430
434
  }
431
435
  /**
436
+ * Build the log patch for one line. Does NOT mutate: the caller merges it
437
+ * into the single `mutate` call it was already making for the same event.
438
+ */
439
+ logPatch(level, text, agentId) {
440
+ const entry = {
441
+ id: ++this.logSeq,
442
+ at: Date.now(),
443
+ level,
444
+ text,
445
+ ...agentId === void 0 ? {} : { agentId }
446
+ };
447
+ return {
448
+ logs: [...this.state.logs, entry].slice(-200),
449
+ logUnread: this.state.logUnread + 1
450
+ };
451
+ }
452
+ /**
432
453
  * Live `session/update` frames waiting to be folded. Agents stream at token
433
454
  * granularity — hundreds of frames a second with usage ticks interleaved —
434
455
  * and folding plus notifying listeners per frame re-rendered every store
@@ -1479,13 +1500,25 @@ window.__ModuleLoader__.load({
1479
1500
  clearNotice() {
1480
1501
  this.mutate({ notice: void 0 });
1481
1502
  }
1503
+ /** Drop every retained runtime-log line (the unread counter restarts too). */
1504
+ clearLogs() {
1505
+ this.mutate({
1506
+ logs: [],
1507
+ logUnread: 0
1508
+ });
1509
+ }
1510
+ /** The user looked at the log: clear the unread badge. */
1511
+ markLogsRead() {
1512
+ if (this.state.logUnread === 0) return;
1513
+ this.mutate({ logUnread: 0 });
1514
+ }
1482
1515
  flashError(error) {
1483
1516
  this.mutate({ notice: error instanceof Error ? error.message : String(error) });
1484
1517
  }
1485
1518
  applyEvent(event) {
1486
1519
  switch (event.type) {
1487
1520
  case "agent/notice":
1488
- this.mutate({ notice: event.text });
1521
+ this.mutate(this.logPatch(event.level, event.text, event.agentId));
1489
1522
  return Promise.resolve();
1490
1523
  case "agent/status":
1491
1524
  if (event.agent !== void 0) {
@@ -1510,6 +1543,7 @@ window.__ModuleLoader__.load({
1510
1543
  return Promise.resolve();
1511
1544
  }
1512
1545
  case "session/closed": {
1546
+ const closingSession = this.state.sessions.find((s) => s.sessionId === event.sessionId);
1513
1547
  const sessions = this.state.sessions.filter((s) => s.sessionId !== event.sessionId);
1514
1548
  const active = this.state.activeSessionId === event.sessionId ? sessions[0]?.sessionId : this.state.activeSessionId;
1515
1549
  this.openTurns.delete(event.sessionId);
@@ -1522,7 +1556,7 @@ window.__ModuleLoader__.load({
1522
1556
  chatTranscripts: closedTranscripts,
1523
1557
  ...chatSessions.length !== this.state.chatSessions.length ? { chatSessions } : {},
1524
1558
  permissions: this.state.permissions.filter((p) => p.sessionId !== event.sessionId),
1525
- ...event.reason === "agent-died" ? { notice: this.locale("会话因 agent 进程退出而终止", "session ended: agent process died") } : {}
1559
+ ...event.reason === "agent-died" ? this.logPatch("error", this.locale("会话因 agent 进程退出而终止", "session ended: agent process died"), closingSession?.agentId) : {}
1526
1560
  });
1527
1561
  return Promise.resolve();
1528
1562
  }
@@ -1533,12 +1567,18 @@ window.__ModuleLoader__.load({
1533
1567
  ...s,
1534
1568
  released: true
1535
1569
  } : s) : this.state.chatSessions;
1570
+ const cause = event.cause ?? "died";
1571
+ let log = {};
1572
+ if (cause === "workspace") log = this.logPatch("info", this.locale("agent 为切换工作区重启,会话已挂起;再次发送消息将自动恢复。", "agent restarted for its workspace; the session is paused and will resume on your next message."), event.session.agentId);
1573
+ else if (cause === "resume-failed") log = this.logPatch("error", this.locale("会话恢复失败;再次发送消息将重试恢复。", "session resume failed; your next message will retry it."), event.session.agentId);
1574
+ else if (cause === "stalled") log = this.logPatch("error", this.locale("agent 未响应卡死的回合,会话已挂起;再次发送消息将自动恢复。", "agent did not acknowledge the stalled turn; the session is paused and will resume on your next message."), event.session.agentId);
1575
+ else if (cause === "died") log = this.logPatch("error", this.locale("agent 进程已退出,会话已挂起;再次发送消息将自动恢复。", "agent process died; the session is paused and will resume on your next message."), event.session.agentId);
1536
1576
  this.mutate({
1537
1577
  sessions,
1538
1578
  ...chatSessions !== this.state.chatSessions ? { chatSessions } : {},
1539
1579
  permissions: this.state.permissions.filter((p) => p.sessionId !== event.sessionId),
1540
1580
  ...this.watchedSessionPatch(event.sessionId, event.session),
1541
- notice: this.locale("agent 进程已退出,会话已挂起;再次发送消息将自动恢复。", "agent process died; the session is paused and will resume on your next message.")
1581
+ ...log
1542
1582
  });
1543
1583
  return Promise.resolve();
1544
1584
  }
@@ -2921,6 +2961,17 @@ window.__ModuleLoader__.load({
2921
2961
  .dwa-deadline-flag { margin-top: 4px; font-size: var(--ntr-font-xs); font-weight: 550; color: var(--ntr-warn); }
2922
2962
  .dwa-deadline-warn { margin-top: 10px; padding: 9px 12px; border-left: 3px solid var(--dsw-alias-state-warn-primary, #d97706); background: var(--dsw-alias-state-warn-secondary, #fffbeb); border-radius: var(--ntr-radius-sm); font-size: var(--ntr-font-xs); line-height: 1.55; }
2923
2963
  .dwa-deadline-reset { display: flex; justify-content: flex-end; margin-top: 8px; }
2964
+ /* Runtime log: one bounded list, newest first. The time column never wraps;
2965
+ the message is the only cell that may break anywhere. */
2966
+ .dwa-log-list { list-style: none; margin: 0; padding: 0; border: var(--ntr-line); border-radius: var(--ntr-radius); overflow: hidden; }
2967
+ .dwa-log-row { display: flex; align-items: baseline; gap: 8px; padding: 6px 10px; font-size: var(--ntr-font-sm); border-top: var(--ntr-line); min-width: 0; }
2968
+ .dwa-log-row:first-child { border-top: 0; }
2969
+ .dwa-log-row.is-error { background: var(--dsw-alias-state-error-tertiary, #fef2f2); }
2970
+ .dwa-log-time { color: var(--ntr-muted); white-space: nowrap; }
2971
+ .dwa-log-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
2972
+ .dwa-log-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ntr-danger); display: inline-block; margin-left: 6px; }
2973
+ .dwa-log-level.is-info { background: var(--dsw-alias-state-business-tertiary, #dbeafe); color: var(--dsw-alias-state-business-primary, #2563eb); }
2974
+ .dwa-log-level.is-error { background: var(--dsw-alias-state-error-tertiary, #fef2f2); color: var(--ntr-danger); }
2924
2975
  @media (max-width: 640px) {
2925
2976
  .dwa-root { padding: 20px 16px; }
2926
2977
  }
@@ -3449,6 +3500,19 @@ window.__ModuleLoader__.load({
3449
3500
  })]
3450
3501
  });
3451
3502
  }
3503
+ /**
3504
+ * Local wall-clock `HH:MM:SS` for one runtime-log line. Hand-built on purpose:
3505
+ * `toLocaleTimeString` may drop seconds or pick a 12-hour clock depending on
3506
+ * the browser locale, which makes the column ragged and ambiguous.
3507
+ */
3508
+ function logClock(at) {
3509
+ const date = new Date(at);
3510
+ return [
3511
+ date.getHours(),
3512
+ date.getMinutes(),
3513
+ date.getSeconds()
3514
+ ].map((value) => String(value).padStart(2, "0")).join(":");
3515
+ }
3452
3516
  /** The states that carry an update badge next to the version badge. */
3453
3517
  function updateBadgeText(update, acp) {
3454
3518
  if (update.status === "available") return acp.label(`可更新 v${update.latest ?? update.current}`, `Update v${update.latest ?? update.current}`);
@@ -3594,6 +3658,13 @@ window.__ModuleLoader__.load({
3594
3658
  (0, react.useEffect)(() => {
3595
3659
  acp.checkPluginUpdate();
3596
3660
  }, [acp]);
3661
+ (0, react.useEffect)(() => {
3662
+ if (page === "logs" && ui.logUnread > 0) acp.markLogsRead();
3663
+ }, [
3664
+ acp,
3665
+ page,
3666
+ ui.logUnread
3667
+ ]);
3597
3668
  const { configured, available } = partitionAgents(ui.detectedAgents, ui.agents);
3598
3669
  const selected = addTarget ?? available[0] ?? NEW_AGENT_SEED;
3599
3670
  const openAdd = () => {
@@ -3760,6 +3831,15 @@ window.__ModuleLoader__.load({
3760
3831
  "aria-pressed": page === "appearance",
3761
3832
  onClick: () => setPage("appearance"),
3762
3833
  children: acp.label("对话外观", "Appearance")
3834
+ }),
3835
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3836
+ type: "button",
3837
+ "aria-pressed": page === "logs",
3838
+ onClick: () => setPage("logs"),
3839
+ children: [acp.label("日志", "Logs"), ui.logUnread > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3840
+ className: "dwa-log-dot",
3841
+ "aria-hidden": "true"
3842
+ })]
3763
3843
  })
3764
3844
  ]
3765
3845
  }),
@@ -4011,6 +4091,52 @@ window.__ModuleLoader__.load({
4011
4091
  })]
4012
4092
  })]
4013
4093
  }),
4094
+ page === "logs" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4095
+ className: "dwa-section",
4096
+ "aria-label": acp.label("运行日志", "Runtime log"),
4097
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4098
+ className: "dwa-section-heading",
4099
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
4100
+ className: "dwa-section-title",
4101
+ children: acp.label("运行日志", "Runtime log")
4102
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4103
+ className: "dwa-section-copy",
4104
+ children: acp.label(`agent 运行消息,最多 ${String(200)} 条。`, `Agent runtime messages, up to ${String(200)}.`)
4105
+ })] }), ui.logs.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4106
+ type: "button",
4107
+ className: "dwa-btn dwa-btn-quiet",
4108
+ onClick: () => acp.clearLogs(),
4109
+ children: acp.label("清空", "Clear")
4110
+ })]
4111
+ }), ui.logs.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4112
+ className: "dwa-empty",
4113
+ role: "status",
4114
+ children: acp.label("暂无运行日志。", "No runtime messages yet.")
4115
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
4116
+ className: "dwa-log-list",
4117
+ children: [...ui.logs].reverse().map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
4118
+ className: entry.level === "error" ? "dwa-log-row is-error" : "dwa-log-row",
4119
+ children: [
4120
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4121
+ className: "dwa-log-time dwa-mono",
4122
+ children: logClock(entry.at)
4123
+ }),
4124
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4125
+ className: `dwa-badge dwa-log-level ${entry.level === "error" ? "is-error" : "is-info"}`,
4126
+ children: entry.level === "error" ? acp.label("错误", "Error") : acp.label("信息", "Info")
4127
+ }),
4128
+ entry.agentId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4129
+ className: "dwa-badge",
4130
+ children: entry.agentId
4131
+ }),
4132
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4133
+ className: "dwa-log-text",
4134
+ children: entry.text
4135
+ })
4136
+ ]
4137
+ }, entry.id))
4138
+ })]
4139
+ }),
4014
4140
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
4015
4141
  className: "dwa-health",
4016
4142
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {