agentworkshop 0.7.3 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/B9Yie5HN.js +2 -0
  3. package/.output/public/_nuxt/CN7Zh8GQ.js +1 -0
  4. package/.output/public/_nuxt/CdSgfcYR.js +1 -0
  5. package/.output/public/_nuxt/CdfAP_IH2.js +1 -0
  6. package/.output/public/_nuxt/ChnfyQHz.js +1 -0
  7. package/.output/public/_nuxt/{BkKYf8xY.js → CyygA0q4.js} +4 -4
  8. package/.output/public/_nuxt/{Bz40vyZS.js → DFnGFEav.js} +7 -7
  9. package/.output/public/_nuxt/Xaf_HIR3.js +1 -0
  10. package/.output/public/_nuxt/ZugRk0CM2.js +4 -0
  11. package/.output/public/_nuxt/builds/latest.json +1 -1
  12. package/.output/public/_nuxt/builds/meta/55c10854-ee0e-4be4-924b-a1b0e100fe11.json +1 -0
  13. package/.output/public/_nuxt/{default.iGJy9dP-.css → default.D17kXlD6.css} +1 -1
  14. package/.output/public/_nuxt/monitor.CmwpEUCI.css +1 -0
  15. package/.output/public/_nuxt/yIKY_V1P.js +1 -0
  16. package/.output/server/chunks/_/nitro.mjs +1131 -920
  17. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  18. package/.output/server/chunks/build/{TownView-BkHxMiwZ.mjs → TownView-Bl8Gkcxi.mjs} +3 -3
  19. package/.output/server/chunks/build/{TownView-BkHxMiwZ.mjs.map → TownView-Bl8Gkcxi.mjs.map} +1 -1
  20. package/.output/server/chunks/build/{_id_-Bna1_JuB.mjs → _id_-CWLFGZjV.mjs} +2 -2
  21. package/.output/server/chunks/build/{_id_-Bna1_JuB.mjs.map → _id_-CWLFGZjV.mjs.map} +1 -1
  22. package/.output/server/chunks/build/_id_-GnRJPykX.mjs +1 -1
  23. package/.output/server/chunks/build/{_wsId_-CzLDGT_V.mjs → _wsId_-BhQMjyHG.mjs} +3 -4
  24. package/.output/server/chunks/build/{_wsId_-CzLDGT_V.mjs.map → _wsId_-BhQMjyHG.mjs.map} +1 -1
  25. package/.output/server/chunks/build/{daq-CmeM4u5u.mjs → daq-BOgpn9sl.mjs} +4 -5
  26. package/.output/server/chunks/build/daq-BOgpn9sl.mjs.map +1 -0
  27. package/.output/server/chunks/build/dcw-BTM5YU-B.mjs +1 -1
  28. package/.output/server/chunks/build/{default-Dt6xLdpd.mjs → default-CKJWhkot.mjs} +60 -17
  29. package/.output/server/chunks/build/{default-Dt6xLdpd.mjs.map → default-CKJWhkot.mjs.map} +1 -1
  30. package/.output/server/chunks/build/{logs-CUQt1z7D.mjs → logs-Dyk0I2I9.mjs} +3 -4
  31. package/.output/server/chunks/build/logs-Dyk0I2I9.mjs.map +1 -0
  32. package/.output/server/chunks/build/{monitor-yNoV98rf.mjs → monitor-KW8WLOJd.mjs} +34 -33
  33. package/.output/server/chunks/build/{monitor-yNoV98rf.mjs.map → monitor-KW8WLOJd.mjs.map} +1 -1
  34. package/.output/server/chunks/build/{town-BN4vQPb8.mjs → town-_4tEEEVW.mjs} +3 -4
  35. package/.output/server/chunks/build/town-_4tEEEVW.mjs.map +1 -0
  36. package/.output/server/chunks/build/useDaqStream-hwkhJHFU.mjs +1 -1
  37. package/.output/server/chunks/build/useDcwStream-Bdew_yrr.mjs +1 -1
  38. package/.output/server/chunks/build/{useWorkshopWs-xcdeiSmJ.mjs → useWorkshopWs-B3j5wIAK.mjs} +177 -3
  39. package/.output/server/chunks/build/useWorkshopWs-B3j5wIAK.mjs.map +1 -0
  40. package/.output/server/chunks/build/{workshop-CLhwe5eo.mjs → workshop-k0XZTFKg.mjs} +2 -3
  41. package/.output/server/chunks/build/workshop-k0XZTFKg.mjs.map +1 -0
  42. package/.output/server/chunks/routes/api/workshop/channel-templates/_id/instantiate.post.mjs +1 -1
  43. package/.output/server/chunks/routes/api/workshop/channels.post.mjs +1 -1
  44. package/.output/server/chunks/routes/api/workshop/hitl/pending.get.mjs +32 -0
  45. package/.output/server/chunks/routes/api/workshop/hitl/pending.get.mjs.map +1 -0
  46. package/.output/server/chunks/routes/api/workshop/hitl/respond.post.mjs +56 -0
  47. package/.output/server/chunks/routes/api/workshop/hitl/respond.post.mjs.map +1 -0
  48. package/.output/server/chunks/routes/api/workshop/plugins/_name/disable.post.mjs +1 -1
  49. package/.output/server/chunks/routes/api/workshop/plugins/_name/enable.post.mjs +1 -1
  50. package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.delete.mjs +1 -1
  51. package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.put.mjs +1 -1
  52. package/.output/server/chunks/routes/api/workshop/workspaces/_id/channel-templates/_tplId_.post.mjs +1 -1
  53. package/.output/server/chunks/routes/renderer.mjs +2 -2
  54. package/.output/server/chunks/virtual/entry.mjs +17 -11
  55. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  56. package/.output/server/package.json +1 -1
  57. package/README.md +435 -434
  58. package/app/components/AppHeader.vue +138 -0
  59. package/app/composables/workshop/useWorkshopWs.ts +4 -0
  60. package/app/pages/monitor.vue +17 -1
  61. package/app/stores/workshop/hitl.ts +80 -0
  62. package/bin/aw.mjs +0 -0
  63. package/cli/commands/tui.mjs +19 -0
  64. package/config.yml +134 -134
  65. package/docs/tui.md +43 -0
  66. package/i18n/locales/en.ts +3 -0
  67. package/i18n/locales/zh-CN.ts +3 -0
  68. package/package.json +140 -144
  69. package/scripts/_spike-pitui.mjs +116 -0
  70. package/scripts/e2e-hitl-live.mjs +323 -0
  71. package/scripts/e2e-line-live.mjs +144 -0
  72. package/scripts/test-hitl-registry.ts +112 -0
  73. package/scripts/test-tui-commands.mjs +131 -0
  74. package/scripts/test-tui-reducers.mjs +72 -0
  75. package/scripts/tsconfig.json +14 -0
  76. package/scripts/tui-smoke.mjs +91 -0
  77. package/server/api/workshop/hitl/pending.get.ts +28 -0
  78. package/server/api/workshop/hitl/respond.post.ts +67 -0
  79. package/server/api/workshop/ws.ts +30 -1
  80. package/server/data/daqs.json +39 -17
  81. package/server/data/dcw-lines.json +14 -0
  82. package/server/data/dcw-products.json +35 -0
  83. package/server/data/dcw-recipes.json +140 -0
  84. package/server/data/dcw-rollback.json +111 -1
  85. package/server/data/dcw-runs.json +135 -90
  86. package/server/data/dcw-writes.json +120 -0
  87. package/server/data/dcws.json +400 -275
  88. package/server/data/line-runs.json +11 -1
  89. package/server/plugins/workshop.ts +7 -0
  90. package/server/services/workshop/agents/harness-terminal.ts +92 -25
  91. package/server/services/workshop/agents/hitl-registry.ts +181 -0
  92. package/server/services/workshop/agents/tool-approvals.ts +15 -0
  93. package/server/services/workshop/scene-events.ts +19 -9
  94. package/shared/workshop-protocol.ts +37 -0
  95. package/tui/README.md +65 -0
  96. package/tui/aw-tui.mjs +356 -0
  97. package/tui/commands/index.mjs +250 -0
  98. package/tui/components/chat-log.mjs +42 -0
  99. package/tui/components/hitl-card.mjs +36 -0
  100. package/tui/components/monitor-pane.mjs +39 -0
  101. package/tui/components/root.mjs +53 -0
  102. package/tui/components/status-bar.mjs +29 -0
  103. package/tui/lib/api.mjs +82 -0
  104. package/tui/lib/config.mjs +58 -0
  105. package/tui/lib/reducers.mjs +218 -0
  106. package/tui/lib/state.mjs +80 -0
  107. package/tui/lib/virtual-terminal.mjs +48 -0
  108. package/tui/lib/ws.mjs +124 -0
  109. package/tui/theme.mjs +38 -0
  110. package/.output/public/_nuxt/B7V9IfTL.js +0 -1
  111. package/.output/public/_nuxt/BE5QyG5J2.js +0 -1
  112. package/.output/public/_nuxt/BuKI8z-5.js +0 -1
  113. package/.output/public/_nuxt/D0C5Q0di.js +0 -1
  114. package/.output/public/_nuxt/DC-QMEk1.js +0 -2
  115. package/.output/public/_nuxt/DEkQ0qQ02.js +0 -1
  116. package/.output/public/_nuxt/builds/meta/c6c01cdd-762e-4fb8-86e7-753c5185b856.json +0 -1
  117. package/.output/public/_nuxt/dt2tKE95.js +0 -4
  118. package/.output/public/_nuxt/i_MKObHo.js +0 -1
  119. package/.output/public/_nuxt/monitor.41fCD6TB.css +0 -1
  120. package/.output/public/_nuxt/tpDMVY5w.js +0 -1
  121. package/.output/server/chunks/build/connection-Dc0fn6yd.mjs +0 -120
  122. package/.output/server/chunks/build/connection-Dc0fn6yd.mjs.map +0 -1
  123. package/.output/server/chunks/build/daq-CmeM4u5u.mjs.map +0 -1
  124. package/.output/server/chunks/build/logs-CUQt1z7D.mjs.map +0 -1
  125. package/.output/server/chunks/build/town-BN4vQPb8.mjs.map +0 -1
  126. package/.output/server/chunks/build/useWorkshopWs-xcdeiSmJ.mjs.map +0 -1
  127. package/.output/server/chunks/build/workshop-CLhwe5eo.mjs.map +0 -1
@@ -0,0 +1,39 @@
1
+ // ============================================================
2
+ // MonitorPane —— 右侧 agent 终端镜像面板(term 帧 → 行)。
3
+ // 行来源:aw-tui 的终端 WS 接线把 term.* 帧写入 state.monitor。
4
+ // ============================================================
5
+ import { theme } from '../theme.mjs'
6
+ import { wrapTextWithAnsi, truncateToWidth } from '@earendil-works/pi-tui'
7
+
8
+ const TONE = {
9
+ dim: s => theme.faint(s),
10
+ info: s => theme.info(s),
11
+ warn: s => theme.warn(s),
12
+ error: s => theme.error(s),
13
+ accent: s => theme.accent(s),
14
+ monitor: s => s,
15
+ }
16
+
17
+ export class MonitorPane {
18
+ constructor(state, width = 44) {
19
+ this.state = state
20
+ this.width = width
21
+ }
22
+
23
+ render() {
24
+ const m = this.state.monitor
25
+ const w = Math.max(this.width - 2, 16)
26
+ const head = theme.info(`┌─ 监控 ${m.name ?? m.agentId?.slice(0, 8) ?? ''} ${m.streaming ? '· 流式中' : ''}`)
27
+ const lines = [head]
28
+ if (m.waiting) lines.push(theme.warn('omp 进程未启动,等待首个任务…'))
29
+ if (!m.agentId) lines.push(theme.faint('(未开启 —— /monitor <agent名|序号>)'))
30
+ for (const l of m.lines) {
31
+ const paint = TONE[l.tone] ?? (s => s)
32
+ lines.push(...wrapTextWithAnsi(paint(l.text), w))
33
+ }
34
+ if (m.streamText) {
35
+ lines.push(...wrapTextWithAnsi(theme.faint(m.streamText.slice(-600)), w))
36
+ }
37
+ return lines.slice(0, 200).map(l => truncateToWidth(l, w))
38
+ }
39
+ }
@@ -0,0 +1,53 @@
1
+ // ============================================================
2
+ // Root —— 组件树组装:双栏(会话流 | 监控面板) + HITL 卡 + 状态条 + 编辑器。
3
+ // 双栏为手工列合成(pi-tui Container 只做纵向;列宽恒定防抖动)。
4
+ // 列宽计算必须用 CJK 感知的 visibleWidth(中文占 2 列;stripAnsi().length 会低估)。
5
+ // ============================================================
6
+ import { visibleWidth, truncateToWidth } from '@earendil-works/pi-tui'
7
+ import { theme } from '../theme.mjs'
8
+
9
+ /**
10
+ * 双栏容器:左栏自适应,右栏固定宽;任一为空退化为单栏。
11
+ * left/right 需实现 render(width) → string[](Component duck-typing)。
12
+ */
13
+ export class Columns {
14
+ constructor(left, right = null, rightWidth = 46) {
15
+ this.left = left
16
+ this.right = right
17
+ this.rightWidth = rightWidth
18
+ }
19
+
20
+ render(width) {
21
+ const gutter = 3
22
+ const leftWidth = this.right ? width - this.rightWidth - gutter : width
23
+ const leftLines = this.left.render(leftWidth).map(l => truncateToWidth(l, leftWidth))
24
+ if (!this.right || this.right.render === undefined) return leftLines
25
+ const rightLines = this.right.render(this.rightWidth).map(l => truncateToWidth(l, this.rightWidth - 1))
26
+ const height = Math.max(leftLines.length, rightLines.length)
27
+ const out = []
28
+ for (let i = 0; i < height; i++) {
29
+ const l = leftLines[i] ?? ''
30
+ const r = rightLines[i] ?? ''
31
+ const padL = ' '.repeat(Math.max(leftWidth - visibleWidth(l), 0))
32
+ out.push(`${l}${padL}${theme.faint(' │ ')}${r}`)
33
+ }
34
+ return out
35
+ }
36
+ }
37
+
38
+ /** 组装整棵组件树(aw-tui 调用;返回可挂到 TUI 的顶层组件) */
39
+ export function buildTree({ state, chatLog, monitorPane, hitlCard, statusBar, editor }) {
40
+ const columns = new Columns(chatLog, monitorPane)
41
+ const stack = {
42
+ render(width) {
43
+ return [
44
+ ...columns.render(width),
45
+ ...hitlCard.render(width),
46
+ ...statusBar.render(width),
47
+ ]
48
+ },
49
+ }
50
+ void state
51
+ void editor
52
+ return { stack, editor }
53
+ }
@@ -0,0 +1,29 @@
1
+ // ============================================================
2
+ // StatusBar —— 底部状态条:频道/成员忙碌/HITL 待办/连接态。
3
+ // ============================================================
4
+ import { theme } from '../theme.mjs'
5
+ import { truncateToWidth } from '@earendil-works/pi-tui'
6
+
7
+ const CONN_LABEL = { open: '实时', connecting: '连接中', closed: '离线' }
8
+
9
+ export class StatusBar {
10
+ constructor(state) {
11
+ this.state = state
12
+ }
13
+
14
+ render(width) {
15
+ const s = this.state
16
+ const channel = s.channels.find(c => c.id === s.activeChannelId)
17
+ const members = s.agents.length
18
+ const busy = Object.values(s.agentStates).filter(a => a.state === 'busy').length
19
+ const parts = [
20
+ theme.accent(`▣ ${channel?.name ?? '未选择频道'}`),
21
+ members > 0 ? theme.faint(`成员 ${members}(忙 ${busy})`) : theme.faint('成员 0'),
22
+ s.hitl.length > 0 ? theme.warn(`⏸ HITL 待处理 ${s.hitl.length}(/hitl)`) : theme.faint('HITL 0'),
23
+ s.monitor.agentId ? theme.info(`监控:${s.monitor.waiting ? '等待进程' : s.monitor.name}`) : null,
24
+ s.connState === 'open' ? theme.faint(`● ${CONN_LABEL[s.connState]}`) : theme.warn(`● ${CONN_LABEL[s.connState] ?? s.connState}`),
25
+ ].filter(Boolean)
26
+ const line = parts.join(theme.faint(' · '))
27
+ return [truncateToWidth(line, Math.max(width, 20))]
28
+ }
29
+ }
@@ -0,0 +1,82 @@
1
+ // ============================================================
2
+ // TUI REST 客户端 —— 统一信封 { code, message, data } 解包 + Bearer 注入。
3
+ // 与 sdk/api.mjs 同语义(轻量自包含:TUI 不依赖构建产物,保持 .mjs 直跑)。
4
+ // ============================================================
5
+
6
+ /**
7
+ * 创建 API 客户端
8
+ * @param {object} opts
9
+ * @param {string} opts.baseUrl 形如 http://127.0.0.1:3000
10
+ * @param {string} opts.token 用户 token(ut-*)
11
+ */
12
+ export function createApi({ baseUrl, token, timeoutMs = 15_000 }) {
13
+ const base = String(baseUrl ?? '').replace(/\/+$/, '')
14
+ // 动态 token:登录/运行中换凭据即时生效(避免实例化时闭包捕获 undefined)
15
+ let currentToken = token ?? ''
16
+
17
+ async function request(path, { method = 'GET', body, query } = {}) {
18
+ const url = new URL(base + path)
19
+ if (query) for (const [k, v] of Object.entries(query)) url.searchParams.set(k, String(v))
20
+ const ctrl = new AbortController()
21
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs)
22
+ try {
23
+ const res = await fetch(url, {
24
+ method,
25
+ headers: {
26
+ ...(currentToken ? { authorization: `Bearer ${currentToken}` } : {}),
27
+ ...(body !== undefined ? { 'content-type': 'application/json' } : {}),
28
+ },
29
+ body: body !== undefined ? JSON.stringify(body) : undefined,
30
+ signal: ctrl.signal,
31
+ })
32
+ const json = await res.json().catch(() => null)
33
+ if (!json || json.code === undefined) {
34
+ throw new Error(`HTTP ${res.status}(响应非标准信封)`)
35
+ }
36
+ if (json.code !== 0) {
37
+ const err = new Error(json.message ?? String(json.code))
38
+ err.code = json.code
39
+ throw err
40
+ }
41
+ return json.data
42
+ }
43
+ finally {
44
+ clearTimeout(timer)
45
+ }
46
+ }
47
+
48
+ return {
49
+ request,
50
+ /** 运行中更新凭据(登录后/换号) */
51
+ setToken(t) {
52
+ currentToken = t ?? ''
53
+ },
54
+
55
+ // ── 登录(无 token 引导;成功自动记住 token) ──
56
+ async login(email, password) {
57
+ const res = await request('/api/users/login', { method: 'POST', body: { email, password } })
58
+ currentToken = res.token
59
+ return res
60
+ },
61
+ get: (path, query) => request(path, { query }),
62
+ post: (path, body) => request(path, { method: 'POST', body }),
63
+
64
+ // ── channel/agent/task 管理面 ──
65
+ listChannels: () => request('/api/workshop/channels'),
66
+ createChannel: body => request('/api/workshop/channels', { method: 'POST', body }),
67
+ listAgents: channelId => request(`/api/workshop/channels/${channelId}/agents`),
68
+ addAgent: (channelId, body) => request(`/api/workshop/channels/${channelId}/agents`, { method: 'POST', body }),
69
+ listTemplates: () => request('/api/workshop/agents'),
70
+ listMessages: (channelId, limit = 50) => request(`/api/workshop/channels/${channelId}/messages`, { query: { limit } }),
71
+ sendMessage: (channelId, body) => request(`/api/workshop/channels/${channelId}/messages`, { method: 'POST', body }),
72
+ listTasks: channelId => request(`/api/workshop/channels/${channelId}/tasks`),
73
+ submitTask: (channelId, body) => request(`/api/workshop/channels/${channelId}/tasks`, { method: 'POST', body }),
74
+
75
+ // ── HITL 统一面(pending 响应为 { items: [...] },此处解包为数组) ──
76
+ hitlPending: async (channelId) => {
77
+ const data = await request('/api/workshop/hitl/pending', { query: channelId ? { channelId } : {} })
78
+ return data?.items ?? []
79
+ },
80
+ hitlRespond: body => request('/api/workshop/hitl/respond', { method: 'POST', body }),
81
+ }
82
+ }
@@ -0,0 +1,58 @@
1
+ // ============================================================
2
+ // TUI 认证/连接配置 —— 落配置根 .AgentWorkShop/tui-auth.json(0600)。
3
+ // 解析口径与服务端一致:cwd 向上找 config.yml → <repo>/.AgentWorkShop,
4
+ // 找不到(全局安装任意 cwd)→ ~/.AgentWorkShop。
5
+ // ============================================================
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'node:fs'
7
+ import { homedir } from 'node:os'
8
+ import { join, resolve, dirname } from 'node:path'
9
+
10
+ /** 向上查找文件(repo 模式判定) */
11
+ function findUp(startDir, filename) {
12
+ let dir = resolve(startDir)
13
+ for (;;) {
14
+ const candidate = join(dir, filename)
15
+ if (existsSync(candidate)) return candidate
16
+ const parent = dirname(dir)
17
+ if (parent === dir) return null
18
+ dir = parent
19
+ }
20
+ }
21
+
22
+ /** 配置根(repo 内 cwd → <repo>/.AgentWorkShop;否则 ~/.AgentWorkShop) */
23
+ export function configRoot(cwd = process.cwd()) {
24
+ const marker = findUp(cwd, 'config.yml')
25
+ if (marker) return join(dirname(marker), '.AgentWorkShop')
26
+ return join(homedir(), '.AgentWorkShop')
27
+ }
28
+
29
+ const authPath = cwd => join(configRoot(cwd), 'tui-auth.json')
30
+
31
+ /** 读取已存凭据({ baseUrl, token, email? } 或 null) */
32
+ export function loadAuth(cwd = process.cwd()) {
33
+ try {
34
+ const raw = JSON.parse(readFileSync(authPath(cwd), 'utf8'))
35
+ if (raw && typeof raw.token === 'string' && typeof raw.baseUrl === 'string') return raw
36
+ }
37
+ catch { /* 无凭据/损坏 → 重新登录 */ }
38
+ return null
39
+ }
40
+
41
+ /** 持久化凭据(0600;路径加入 .gitignore) */
42
+ export function saveAuth(auth, cwd = process.cwd()) {
43
+ const file = authPath(cwd)
44
+ mkdirSync(dirname(file), { recursive: true })
45
+ writeFileSync(file, `${JSON.stringify(auth, null, 2)}\n`, 'utf8')
46
+ try {
47
+ chmodSync(file, 0o600)
48
+ }
49
+ catch { /* Windows FAT 卷 chmod 无效,忽略 */ }
50
+ }
51
+
52
+ /** 清除凭据(登出) */
53
+ export function clearAuth(cwd = process.cwd()) {
54
+ try {
55
+ existsSync(authPath(cwd)) && (writeFileSync(authPath(cwd), '{}\n', 'utf8'), true)
56
+ }
57
+ catch { /* 尽力而为 */ }
58
+ }
@@ -0,0 +1,218 @@
1
+ // ============================================================
2
+ // AEP 帧归约(纯函数式:输入 state+信封 → 原地更新 state 与 log 行)。
3
+ // 与 WebUI 的 events.store 同一套语义:agent.delta 聚合进流式块,落定
4
+ // agent.message 携带全文定稿(去重);hitl 增删幂等(kind+id)。
5
+ // 可被 tsx/单测与 scripts/tui-smoke.mjs 无头驱动。
6
+ // ============================================================
7
+ import { pushLog } from './state.mjs'
8
+
9
+ const STREAM_TAIL = 4000
10
+
11
+ /** 从信封/消息提取展示名 */
12
+ function agentNameOf(state, agentId, fallback = '') {
13
+ if (!agentId) return fallback
14
+ const a = state.agents.find(x => x.id === agentId)
15
+ return a?.name ?? fallback ?? agentId.slice(0, 8)
16
+ }
17
+
18
+ /** 消息 parts → 纯文本 */
19
+ function partsText(message) {
20
+ const parts = message?.parts ?? []
21
+ return parts.map((p) => {
22
+ if (typeof p === 'string') return p
23
+ if (p && typeof p === 'object' && typeof p.text === 'string') return p.text
24
+ return ''
25
+ }).join('').trim()
26
+ }
27
+
28
+ /**
29
+ * 归约一帧 AEP 信封。返回 'handled' | 'ignored'(测试断言用)。
30
+ * 流式块键 = agentId+taskId(agent.message 到达即定稿替换全文)。
31
+ */
32
+ export function reduceEnvelope(state, e) {
33
+ switch (e.type) {
34
+ case 'agent.status': {
35
+ const p = e.payload
36
+ state.agentStates[p.agentId] = {
37
+ state: p.state,
38
+ currentTaskId: p.currentTaskId ?? null,
39
+ queued: p.queued ?? 0,
40
+ completed: p.completed ?? 0,
41
+ }
42
+ return 'handled'
43
+ }
44
+ case 'agent.delta': {
45
+ const text = e.payload?.delta ?? ''
46
+ if (!text) return 'ignored'
47
+ const row = findStreamRow(state, e.agentId, e.taskId)
48
+ if (row) {
49
+ row.text = (row.text + text).slice(-STREAM_TAIL)
50
+ }
51
+ else {
52
+ pushLog(state, 'stream', text, { agentName: agentNameOf(state, e.agentId), agentId: e.agentId, taskId: e.taskId ?? null })
53
+ }
54
+ return 'handled'
55
+ }
56
+ case 'agent.message': {
57
+ const msg = e.payload ?? {}
58
+ const text = partsText(msg)
59
+ if (!text) return 'ignored'
60
+ const agentId = e.agentId ?? msg.metadata?.['x-aw-from-agent'] ?? ''
61
+ const name = agentNameOf(state, agentId, msg.metadata?.['x-aw-from-label'] ?? '')
62
+ const row = findStreamRow(state, agentId, e.taskId)
63
+ if (row) {
64
+ // 定稿:流式增量替换为全文,并升级为正式 agent 行(渲染带名高亮)
65
+ row.text = text
66
+ row.kind = 'agent'
67
+ row.agentName = name
68
+ }
69
+ else {
70
+ pushLog(state, 'agent', text, { agentName: name, agentId, taskId: e.taskId ?? null })
71
+ }
72
+ return 'handled'
73
+ }
74
+ case 'a2a.message': {
75
+ const msg = e.payload ?? {}
76
+ const meta = msg.metadata ?? {}
77
+ const fromLabel = meta['x-aw-from-label'] ?? ''
78
+ const fromAgent = meta['x-aw-from-agent'] ?? e.agentId ?? ''
79
+ const text = partsText(msg)
80
+ if (!text) return 'ignored'
81
+ const cross = meta['x-aw-cross-channel'] === 'true'
82
+ const prefix = cross ? '[跨频道] ' : ''
83
+ if (fromAgent) {
84
+ pushLog(state, 'agent', prefix + text, { agentName: agentNameOf(state, fromAgent, fromLabel), agentId: fromAgent, taskId: msg.taskId ?? null })
85
+ }
86
+ else {
87
+ // 人类消息(服务端盖章 from-label;本端发出的回显也走这里)
88
+ pushLog(state, 'user', prefix + text, { agentName: fromLabel || '用户' })
89
+ }
90
+ return 'handled'
91
+ }
92
+ case 'agent.status.message': {
93
+ const text = (e.payload?.text ?? '').trim()
94
+ if (text) pushLog(state, 'notice', text, { agentName: agentNameOf(state, e.agentId) })
95
+ return 'handled'
96
+ }
97
+ case 'task.status': {
98
+ const p = e.payload ?? {}
99
+ upsertTask(state, p)
100
+ const title = p.title ?? state.tasks.find(t2 => t2.taskId === p.taskId)?.title ?? ''
101
+ pushLog(state, 'task', `任务 ${p.taskId?.slice(0, 8)}${title ? `「${title}」` : ''} → ${p.state}${p.progress != null ? ` ${p.progress}%` : ''}`)
102
+ return 'handled'
103
+ }
104
+ case 'task.progress': {
105
+ const p = e.payload ?? {}
106
+ upsertTask(state, p)
107
+ return 'handled'
108
+ }
109
+ case 'a2a.artifact': {
110
+ const art = e.payload?.artifact
111
+ const name = art?.name ?? art?.kind ?? 'artifact'
112
+ pushLog(state, 'notice', `交付物:${name}`, { agentName: agentNameOf(state, e.agentId) })
113
+ return 'handled'
114
+ }
115
+ case 'error': {
116
+ pushLog(state, 'error', e.payload?.message ?? '未知错误', { agentName: agentNameOf(state, e.agentId) })
117
+ return 'handled'
118
+ }
119
+ case 'hitl.request': {
120
+ upsertHitl(state, e.payload)
121
+ const it = e.payload
122
+ pushLog(state, 'hitl', `⏸ 需要人工处理:${it.title}(${it.agentName})—— /hitl 查看并作答`)
123
+ return 'handled'
124
+ }
125
+ case 'hitl.resolved': {
126
+ const r = e.payload
127
+ state.hitl = state.hitl.filter(i => !(i.kind === r.kind && i.id === r.id))
128
+ if (state.hitlAnswering && state.hitlAnswering.kind === r.kind && state.hitlAnswering.id === r.id) state.hitlAnswering = null
129
+ pushLog(state, 'hitl-resolved', `✔ 待办已落定:${r.id}(outcome=${r.outcome}${r.by ? `, by ${r.by}` : ''})`)
130
+ return 'handled'
131
+ }
132
+ default:
133
+ return 'ignored'
134
+ }
135
+ }
136
+
137
+ function findStreamRow(state, agentId, taskId) {
138
+ for (let i = state.log.length - 1; i >= 0; i--) {
139
+ const row = state.log[i]
140
+ if (row.kind === 'stream' && row.agentId === agentId && (row.taskId ?? null) === (taskId ?? null)) return row
141
+ }
142
+ return null
143
+ }
144
+
145
+ function upsertTask(state, p) {
146
+ if (!p.taskId) return
147
+ const idx = state.tasks.findIndex(t => t.taskId === p.taskId)
148
+ if (idx >= 0) state.tasks[idx] = { ...state.tasks[idx], ...p }
149
+ else state.tasks.unshift(p)
150
+ state.tasks = state.tasks.slice(0, 100)
151
+ }
152
+
153
+ function upsertHitl(state, item) {
154
+ if (!item?.id) return
155
+ const idx = state.hitl.findIndex(i => i.kind === item.kind && i.id === item.id)
156
+ if (idx >= 0) state.hitl[idx] = item
157
+ else state.hitl.push(item)
158
+ }
159
+
160
+ // ===== 终端镜像帧 → monitor 行(纯部分;ws 接线在 aw-tui) =====
161
+
162
+ /** term 帧 → monitor 行描述;返回 { lines:[{text,tone}], streamAppend?, streamEnd?, statePatch? } */
163
+ export function reduceTermFrame(frame) {
164
+ const out = { lines: [], streamAppend: '', streamEnd: null, statePatch: null }
165
+ const type = frame?.type
166
+ switch (type) {
167
+ case 'agent_start':
168
+ out.lines.push({ text: '── 回合开始 ──', tone: 'dim' })
169
+ break
170
+ case 'agent_end':
171
+ out.lines.push({ text: '── 回合结束 ──', tone: 'dim' })
172
+ break
173
+ case 'message_end': {
174
+ const text = String(frame.text ?? '').trim()
175
+ if (text) out.lines.push({ text, tone: 'monitor' })
176
+ break
177
+ }
178
+ case 'tool_execution_start':
179
+ out.lines.push({ text: `🔧 ${frame.toolName ?? ''}${brief(frame.args, 80)}`, tone: 'info' })
180
+ break
181
+ case 'tool_execution_end':
182
+ out.lines.push({ text: `${frame.isError ? '✗' : '✔'} ${frame.toolName ?? ''}${brief(frame.result, 120)}`, tone: frame.isError ? 'error' : 'dim' })
183
+ break
184
+ case 'host_tool_call':
185
+ out.lines.push({ text: `🛠 host ${frame.toolName ?? ''}${brief(frame.args, 80)}`, tone: 'info' })
186
+ break
187
+ case 'extension_ui_request':
188
+ if (frame.method && frame.method !== 'cancel') {
189
+ out.lines.push({ text: `⏸ HITL ${frame.method}「${String(frame.title ?? '').slice(0, 60)}」(/hitl 作答)`, tone: 'warn' })
190
+ }
191
+ break
192
+ case '__human_input':
193
+ out.lines.push({ text: `⌨ ${String(frame.text ?? '').slice(0, 120)}`, tone: 'info' })
194
+ break
195
+ case '__terminal_notice':
196
+ out.lines.push({ text: String(frame.message ?? ''), tone: frame.level === 'error' ? 'error' : frame.level === 'warning' ? 'warn' : 'dim' })
197
+ break
198
+ default:
199
+ break
200
+ }
201
+ return out
202
+ }
203
+
204
+ function brief(value, max) {
205
+ if (value == null) return ''
206
+ let s = typeof value === 'string'
207
+ ? value
208
+ : (() => {
209
+ try {
210
+ return JSON.stringify(value) ?? ''
211
+ }
212
+ catch {
213
+ return String(value)
214
+ }
215
+ })()
216
+ s = s.replace(/\s+/g, ' ').trim()
217
+ return s ? ` ${s.slice(0, max)}${s.length > max ? '…' : ''}` : ''
218
+ }
@@ -0,0 +1,80 @@
1
+ // ============================================================
2
+ // TuiState —— 全局共享状态(单一对象 + 订阅通知;组件渲染时读取)。
3
+ // 纯数据,不含行为;帧归约逻辑在 reducers.mjs(可单测)。
4
+ // ============================================================
5
+ import { EventEmitter } from 'node:events'
6
+
7
+ export const LOG_CAP = 400
8
+
9
+ /** 日志行:{ id, kind, agentName?, text, at }
10
+ * kind: system|user|agent|stream|task|hitl|hitl-resolved|error|monitor|notice */
11
+ export function createState() {
12
+ const emitter = new EventEmitter()
13
+ emitter.setMaxListeners(20)
14
+
15
+ const state = {
16
+ baseUrl: '',
17
+ token: '',
18
+ userName: '',
19
+
20
+ connState: 'closed', // closed|connecting|open
21
+ channels: [],
22
+ activeChannelId: null,
23
+ agents: [], // channel 成员实例:{ id, name, role, harness, enabled }
24
+ agentStates: {}, // agentId → { state, currentTaskId, queued, completed }
25
+ tasks: [],
26
+
27
+ hitl: [], // AepHitlItem[]
28
+ /** 正在作答的条目(index → /hitl <n>;null = 常规输入) */
29
+ hitlAnswering: null,
30
+
31
+ monitor: {
32
+ agentId: null,
33
+ name: null,
34
+ connected: false,
35
+ waiting: false, // omp 未 spawn(NO_SESSION 重试等待)
36
+ streaming: false,
37
+ lines: [], // { text, tone: dim|info|warn|error|accent }
38
+ streamText: '', // message_update 聚合行(尾部)
39
+ },
40
+
41
+ /** 瞬时状态条消息(命令回执;几行内自然被后续输出顶走) */
42
+ statusMsg: '',
43
+ statusAt: 0,
44
+ }
45
+
46
+ return {
47
+ state,
48
+ /** 组件注册重渲染回调(requestRender) */
49
+ onChange(fn) {
50
+ emitter.on('change', fn)
51
+ return () => emitter.off('change', fn)
52
+ },
53
+ /** 归约器/命令改状态后调用 → 触发一帧渲染 */
54
+ notify() {
55
+ emitter.emit('change')
56
+ },
57
+ }
58
+ }
59
+
60
+ /** 追加日志行(封顶裁剪);返回该行 id */
61
+ export function pushLog(state, kind, text, extra = {}) {
62
+ const row = {
63
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
64
+ kind,
65
+ text,
66
+ agentName: extra.agentName ?? '',
67
+ agentId: extra.agentId ?? null,
68
+ taskId: extra.taskId ?? null,
69
+ at: new Date(),
70
+ }
71
+ state.log.push(row)
72
+ if (state.log.length > LOG_CAP) state.log.splice(0, state.log.length - LOG_CAP)
73
+ return row
74
+ }
75
+
76
+ /** TUI 挂载 log 数组(state.log 由宿主初始化;此处类型聚合,便于单测构造) */
77
+ export function withLog(state) {
78
+ state.log ??= []
79
+ return state
80
+ }
@@ -0,0 +1,48 @@
1
+ // ============================================================
2
+ // 虚拟终端(Terminal 接口最小实现)—— 无 TTY 环境驱动 TUI:
3
+ // 捕获写入帧、注入按键流;scripts/tui-smoke.mjs 无头 e2e 用。
4
+ // ============================================================
5
+ import { stripAnsi } from '../theme.mjs'
6
+
7
+ export class VirtualTerminal {
8
+ constructor(cols = 120, rows = 34) {
9
+ this.columns = cols
10
+ this.rows = rows
11
+ this.frames = []
12
+ this.inputHandler = null
13
+ this.kittyProtocolActive = false
14
+ }
15
+
16
+ start(onInput) {
17
+ this.inputHandler = onInput
18
+ }
19
+
20
+ stop() {}
21
+ async drainInput() {}
22
+ write(data) {
23
+ this.frames.push(data)
24
+ }
25
+
26
+ moveBy() {}
27
+ hideCursor() {}
28
+ showCursor() {}
29
+ clearLine() {}
30
+ clearFromCursor() {}
31
+ clearScreen() {}
32
+ setTitle() {}
33
+ setProgress() {}
34
+
35
+ /** 测试注入按键(模拟 raw-mode 字节流) */
36
+ emitInput(data) {
37
+ this.inputHandler?.(data)
38
+ }
39
+
40
+ output() {
41
+ return this.frames.join('')
42
+ }
43
+
44
+ /** 剥 ANSI 后的纯文本(断言用) */
45
+ text() {
46
+ return stripAnsi(this.output())
47
+ }
48
+ }