agentworkshop 0.7.3 → 0.7.5

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 (130) 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/stop.mjs +108 -0
  64. package/cli/commands/tui.mjs +19 -0
  65. package/config.yml +134 -134
  66. package/docs/tui.md +43 -0
  67. package/i18n/locales/en.ts +3 -0
  68. package/i18n/locales/zh-CN.ts +3 -0
  69. package/package.json +140 -144
  70. package/scripts/_spike-pitui.mjs +116 -0
  71. package/scripts/e2e-aw-tui-global.mjs +277 -0
  72. package/scripts/e2e-hitl-live.mjs +323 -0
  73. package/scripts/e2e-line-live.mjs +144 -0
  74. package/scripts/test-hitl-registry.ts +112 -0
  75. package/scripts/test-tui-commands.mjs +148 -0
  76. package/scripts/test-tui-reducers.mjs +72 -0
  77. package/scripts/tsconfig.json +14 -0
  78. package/scripts/tui-smoke.mjs +100 -0
  79. package/server/api/workshop/hitl/pending.get.ts +28 -0
  80. package/server/api/workshop/hitl/respond.post.ts +67 -0
  81. package/server/api/workshop/ws.ts +30 -1
  82. package/server/data/daqs.json +39 -17
  83. package/server/data/dcw-lines.json +14 -0
  84. package/server/data/dcw-products.json +35 -0
  85. package/server/data/dcw-recipes.json +140 -0
  86. package/server/data/dcw-rollback.json +111 -1
  87. package/server/data/dcw-runs.json +135 -90
  88. package/server/data/dcw-writes.json +120 -0
  89. package/server/data/dcws.json +400 -275
  90. package/server/data/line-runs.json +11 -1
  91. package/server/plugins/workshop.ts +7 -0
  92. package/server/services/workshop/agents/harness-terminal.ts +92 -25
  93. package/server/services/workshop/agents/hitl-registry.ts +181 -0
  94. package/server/services/workshop/agents/tool-approvals.ts +15 -0
  95. package/server/services/workshop/scene-events.ts +19 -9
  96. package/shared/workshop-protocol.ts +37 -0
  97. package/tui/README.md +74 -0
  98. package/tui/aw-tui.mjs +406 -0
  99. package/tui/commands/index.mjs +287 -0
  100. package/tui/components/chat-log.mjs +42 -0
  101. package/tui/components/hitl-card.mjs +36 -0
  102. package/tui/components/monitor-pane.mjs +39 -0
  103. package/tui/components/root.mjs +53 -0
  104. package/tui/components/status-bar.mjs +33 -0
  105. package/tui/lib/api.mjs +82 -0
  106. package/tui/lib/config.mjs +58 -0
  107. package/tui/lib/pickers.mjs +102 -0
  108. package/tui/lib/reducers.mjs +218 -0
  109. package/tui/lib/state.mjs +86 -0
  110. package/tui/lib/virtual-terminal.mjs +48 -0
  111. package/tui/lib/ws.mjs +124 -0
  112. package/tui/theme.mjs +38 -0
  113. package/.output/public/_nuxt/B7V9IfTL.js +0 -1
  114. package/.output/public/_nuxt/BE5QyG5J2.js +0 -1
  115. package/.output/public/_nuxt/BuKI8z-5.js +0 -1
  116. package/.output/public/_nuxt/D0C5Q0di.js +0 -1
  117. package/.output/public/_nuxt/DC-QMEk1.js +0 -2
  118. package/.output/public/_nuxt/DEkQ0qQ02.js +0 -1
  119. package/.output/public/_nuxt/builds/meta/c6c01cdd-762e-4fb8-86e7-753c5185b856.json +0 -1
  120. package/.output/public/_nuxt/dt2tKE95.js +0 -4
  121. package/.output/public/_nuxt/i_MKObHo.js +0 -1
  122. package/.output/public/_nuxt/monitor.41fCD6TB.css +0 -1
  123. package/.output/public/_nuxt/tpDMVY5w.js +0 -1
  124. package/.output/server/chunks/build/connection-Dc0fn6yd.mjs +0 -120
  125. package/.output/server/chunks/build/connection-Dc0fn6yd.mjs.map +0 -1
  126. package/.output/server/chunks/build/daq-CmeM4u5u.mjs.map +0 -1
  127. package/.output/server/chunks/build/logs-CUQt1z7D.mjs.map +0 -1
  128. package/.output/server/chunks/build/town-BN4vQPb8.mjs.map +0 -1
  129. package/.output/server/chunks/build/useWorkshopWs-xcdeiSmJ.mjs.map +0 -1
  130. package/.output/server/chunks/build/workshop-CLhwe5eo.mjs.map +0 -1
@@ -0,0 +1,42 @@
1
+ // ============================================================
2
+ // ChatLog 组件 —— 会话时间线(AEP 归约后的 log 行渲染)。
3
+ // 每次 render 全量输出(有界 400 行),增量绘制由 TuiMainScreen 差分完成。
4
+ // 输出经 truncateToWidth 兜底(CJK 宽字符行宽;pi-tui 对超宽行直接 crash)。
5
+ // ============================================================
6
+ import { theme } from '../theme.mjs'
7
+ import { wrapTextWithAnsi, truncateToWidth } from '@earendil-works/pi-tui'
8
+
9
+ const TONE_OF = {
10
+ 'system': s => theme.faint(s),
11
+ 'user': s => `${theme.info('◆ 你')} ${s}`,
12
+ 'agent': s => `${theme.accent('◆ Agent')} ${s}`,
13
+ 'stream': s => theme.faint(s),
14
+ 'task': s => theme.info(s),
15
+ 'hitl': s => theme.warn(s),
16
+ 'hitl-resolved': s => theme.accent(s),
17
+ 'error': s => theme.error(`✗ ${s}`),
18
+ 'notice': s => theme.faint(`· ${s}`),
19
+ 'monitor': s => s,
20
+ }
21
+
22
+ export class ChatLog {
23
+ constructor(state) {
24
+ this.state = state
25
+ }
26
+
27
+ render(width) {
28
+ const w = Math.max(width, 20)
29
+ const lines = []
30
+ for (const row of this.state.log) {
31
+ const paint = TONE_OF[row.kind] ?? (s => s)
32
+ const prefix = row.kind === 'agent' && row.agentName ? `${theme.faint(`[${row.agentName}]`)} ` : ''
33
+ const wrapped = wrapTextWithAnsi(paint(`${prefix}${row.text}`), w)
34
+ lines.push(...wrapped)
35
+ }
36
+ if (this.state.statusMsg && Date.now() - this.state.statusAt < 15_000) {
37
+ lines.push('')
38
+ lines.push(theme.faint(this.state.statusMsg))
39
+ }
40
+ return lines.map(l => truncateToWidth(l, w))
41
+ }
42
+ }
@@ -0,0 +1,36 @@
1
+ // ============================================================
2
+ // HitlCard —— 作答模式卡片(state.hitlAnswering 非空时渲染于输入框上方)。
3
+ // 作答约定:confirm → y/n;select → 选项序号;input/editor → 自由文本;
4
+ // /hitl off 放弃作答(等价 Esc)。提交经 POST /api/workshop/hitl/respond。
5
+ // ============================================================
6
+ import { theme } from '../theme.mjs'
7
+ import { wrapTextWithAnsi, truncateToWidth } from '@earendil-works/pi-tui'
8
+
9
+ const HINT = {
10
+ confirm: '输入 y 批准 / n 拒绝',
11
+ select: '输入选项序号(如 1)',
12
+ input: '输入回答文本',
13
+ editor: '输入回答文本',
14
+ }
15
+
16
+ export class HitlCard {
17
+ constructor(state) {
18
+ this.state = state
19
+ }
20
+
21
+ render(width) {
22
+ const item = this.state.hitlAnswering
23
+ if (!item) return []
24
+ const w = Math.max(width - 4, 20)
25
+ const lines = []
26
+ lines.push(theme.warn(`⏸ HITL 作答 · ${item.agentName} · ${item.kind === 'omp-dialog' ? 'omp 对话框' : '下发审批'}`))
27
+ lines.push(truncateToWidth(theme.bold(String(item.title ?? '')), w))
28
+ if (item.detail) lines.push(...wrapTextWithAnsi(theme.faint(item.detail), w))
29
+ if (item.message) lines.push(...wrapTextWithAnsi(String(item.message), w))
30
+ if (item.method === 'select' && Array.isArray(item.options)) {
31
+ item.options.forEach((opt, i) => lines.push(` ${theme.accent(`${i + 1}.`)} ${opt}`))
32
+ }
33
+ lines.push(theme.faint(`${HINT[item.method] ?? '输入回答'} · /hitl off 放弃`))
34
+ return lines.map(l => truncateToWidth(l, Math.max(width, 20)))
35
+ }
36
+ }
@@ -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,33 @@
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 target = s.target
20
+ ? theme.info(`@${s.target.name}(通信)`)
21
+ : theme.accent('频道(任务)')
22
+ const parts = [
23
+ theme.accent(`▣ ${channel?.name ?? '未选择频道'}`),
24
+ `${theme.faint('输入→')} ${target}`,
25
+ members > 0 ? theme.faint(`成员 ${members}(忙 ${busy})`) : theme.faint('成员 0'),
26
+ s.hitl.length > 0 ? theme.warn(`⏸ HITL 待处理 ${s.hitl.length}(/hitl)`) : theme.faint('HITL 0'),
27
+ s.monitor.agentId ? theme.info(`监控:${s.monitor.waiting ? '等待进程' : s.monitor.name}`) : null,
28
+ s.connState === 'open' ? theme.faint(`● ${CONN_LABEL[s.connState]}`) : theme.warn(`● ${CONN_LABEL[s.connState] ?? s.connState}`),
29
+ ].filter(Boolean)
30
+ const line = parts.join(theme.faint(' · '))
31
+ return [truncateToWidth(line, Math.max(width, 20))]
32
+ }
33
+ }
@@ -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,102 @@
1
+ // ============================================================
2
+ // 交互选择器(浮层)—— 频道选择 / Agent 选择。
3
+ // 基于 pi-tui showOverlay + SelectList:方向键移动、Enter 选择、Esc 取消。
4
+ // ============================================================
5
+ import { SelectList } from '@earendil-works/pi-tui'
6
+ import { theme } from '../theme.mjs'
7
+
8
+ /** 组合"标题 + SelectList"的浮层组件。返回 handle { hide, list, component, __bindHandle } */
9
+ function pickerComponent(title, items, maxVisible, onSelect, onCancel) {
10
+ const list = new SelectList(items, maxVisible, theme.editor.selectList)
11
+ const wrap = {
12
+ render(width) {
13
+ return [theme.accent(`┌─ ${title}`), ...list.render(Math.max(width - 2, 24))]
14
+ },
15
+ }
16
+ const handle = { hide() {}, list, component: wrap }
17
+ list.onSelect = (item) => {
18
+ handle.hide()
19
+ onSelect(item)
20
+ }
21
+ list.onCancel = () => {
22
+ handle.hide()
23
+ onCancel?.()
24
+ }
25
+ // 二次装配:showOverlay 返回真实 handle 后回填 hide(调用方调 picker.__bindHandle(real))
26
+ handle.__bindHandle = (h) => {
27
+ handle.hide = () => h.hide()
28
+ }
29
+ return handle
30
+ }
31
+
32
+ /**
33
+ * 频道选择浮层。Enter 选择并切换;Esc 取消(fallback 频道由调用方决定)。
34
+ * @returns handle { close }
35
+ */
36
+ export function openChannelPicker(tui, state, { onSelect, onCancel } = {}) {
37
+ const items = state.channels.map(c => ({
38
+ value: c.id,
39
+ label: c.name,
40
+ description: c.leadAgentId ? 'lead 已配置' : '无 lead(仅收发消息)',
41
+ }))
42
+ const picker = pickerComponent('选择要进入的频道(↑↓ 移动 · Enter 进入 · Esc 取消)', items, Math.min(10, items.length),
43
+ (item) => {
44
+ state.channelPickerOpen = false
45
+ onSelect?.(item.value)
46
+ },
47
+ () => {
48
+ state.channelPickerOpen = false
49
+ onCancel?.()
50
+ })
51
+ state.channelPickerOpen = true
52
+ const handle = tui.showOverlay(picker.component, { anchor: 'center' })
53
+ picker.__bindHandle(handle)
54
+ tui.setFocus(picker.list)
55
+ return handle
56
+ }
57
+
58
+ /**
59
+ * Agent/对话目标选择浮层(Tab 触发)。首项 = 频道(发布任务模式);
60
+ * 选具体成员 = 切换通信目标并自动打开其独立监控。
61
+ */
62
+ export function openTargetPicker(tui, state, { onPick, onCancel } = {}) {
63
+ const s = state.agents
64
+ .filter(a => a.enabled !== 0)
65
+ .map(a => ({
66
+ value: a.id,
67
+ label: `${a.name}${a.role === 'lead' ? '(lead)' : ''}`,
68
+ description: `通信对话 + 独立监控 · ${state.agentStates[a.id]?.state ?? 'idle'}`,
69
+ }))
70
+ const items = [{ value: '__channel__', label: '频道(发布任务)', description: '普通文本 = 向频道发布正式任务' }, ...s]
71
+ if (items.length === 1 && s.length === 0) {
72
+ onCancel?.()
73
+ return null
74
+ }
75
+ const picker = pickerComponent('选择对话目标(Tab 呼出 · ↑↓ 移动 · Enter 确认 · Esc 取消)', items, Math.min(10, items.length),
76
+ (item) => {
77
+ if (process.env.AW_TUI_DEBUG) console.error('[picker-debug] item=', JSON.stringify(item), 'agentIds=', JSON.stringify(s.map(x => x.value)))
78
+ state.targetPickerOpen = false
79
+ if (item.value === '__channel__') {
80
+ state.target = null
81
+ onPick?.(null)
82
+ }
83
+ else {
84
+ const a = state.agents.find(x => x.id === item.value)
85
+ if (!a) {
86
+ onCancel?.()
87
+ return
88
+ }
89
+ state.target = { agentId: a.id, name: a.name }
90
+ onPick?.(a)
91
+ }
92
+ },
93
+ () => {
94
+ state.targetPickerOpen = false
95
+ onCancel?.()
96
+ })
97
+ state.targetPickerOpen = true
98
+ const handle = tui.showOverlay(picker.component, { anchor: 'center' })
99
+ picker.__bindHandle(handle)
100
+ tui.setFocus(picker.list)
101
+ return handle
102
+ }
@@ -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
+ }