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
package/tui/aw-tui.mjs ADDED
@@ -0,0 +1,356 @@
1
+ // ============================================================
2
+ // AgentWorkShop TUI 入口 —— 终端工作台(与 AgentTeam 交互作业)。
3
+ //
4
+ // node tui/aw-tui.mjs [--url http://127.0.0.1:3000] [--token ut-*]
5
+ // [--channel <名>] [--headless]
6
+ // (或 `aw tui …` / `pnpm tui`)
7
+ //
8
+ // 装配:认证(配置根 tui-auth.json / 交互登录)→ AEP 事件流(频道 sub)+
9
+ // 命令系统(/help)+ 监控面板(终端 WS)+ HITL 统一作答(/hitl)。
10
+ // --headless:VirtualTerminal 驱动(无 TTY;scripts/tui-smoke.mjs 用)。
11
+ // ============================================================
12
+ import { TuiMainScreen, Editor, CombinedAutocompleteProvider, ProcessTerminal } from '@earendil-works/pi-tui'
13
+ import { theme } from './theme.mjs'
14
+ import { loadAuth, saveAuth, configRoot } from './lib/config.mjs'
15
+ import { createApi } from './lib/api.mjs'
16
+ import { connectAep, connectTerm } from './lib/ws.mjs'
17
+ import { createState, pushLog, withLog } from './lib/state.mjs'
18
+ import { reduceEnvelope, reduceTermFrame } from './lib/reducers.mjs'
19
+ import { dispatchCommand, slashCommandCompletions } from './commands/index.mjs'
20
+ import { ChatLog } from './components/chat-log.mjs'
21
+ import { StatusBar } from './components/status-bar.mjs'
22
+ import { MonitorPane } from './components/monitor-pane.mjs'
23
+ import { HitlCard } from './components/hitl-card.mjs'
24
+ import { buildTree } from './components/root.mjs'
25
+ import { VirtualTerminal } from './lib/virtual-terminal.mjs'
26
+
27
+ function parseArgs(argv) {
28
+ const opts = { headless: false }
29
+ for (let i = 0; i < argv.length; i++) {
30
+ const a = argv[i]
31
+ if (a === '--headless') opts.headless = true
32
+ else if (a === '--url') opts.url = argv[++i]
33
+ else if (a === '--token') opts.token = argv[++i]
34
+ else if (a === '--channel') opts.channel = argv[++i]
35
+ }
36
+ return opts
37
+ }
38
+
39
+ /** 认证:已存凭据 → --token → 环境变量 → 交互登录 */
40
+ async function ensureAuth(opts) {
41
+ const saved = loadAuth()
42
+ const baseUrl = opts.url ?? process.env.AW_TUI_URL ?? saved?.baseUrl ?? 'http://127.0.0.1:3000'
43
+ if (opts.token) return { baseUrl, token: opts.token }
44
+ if (saved?.token && saved.baseUrl === baseUrl) return { baseUrl, token: saved.token }
45
+
46
+ const email = process.env.AW_TUI_EMAIL
47
+ const password = process.env.AW_TUI_PASSWORD
48
+ if (email && password) {
49
+ const api = createApi({ baseUrl })
50
+ const res = await api.login(email, password)
51
+ const auth = { baseUrl, token: res.token, email }
52
+ saveAuth(auth)
53
+ return auth
54
+ }
55
+ if (opts.headless) throw new Error('无凭据:headless 模式请传 --token 或设 AW_TUI_EMAIL/AW_TUI_PASSWORD')
56
+
57
+ const rl = (await import('node:readline/promises')).createInterface({ input: process.stdin, output: process.stdout })
58
+ try {
59
+ console.log(`AgentWorkShop TUI 首次使用(凭据落 ${configRoot()}/tui-auth.json)`)
60
+ const mail = await rl.question('邮箱: ')
61
+ const pass = await rl.question('密码: ')
62
+ const api = createApi({ baseUrl })
63
+ const res = await api.login(mail.trim(), pass)
64
+ const auth = { baseUrl, token: res.token, email: mail.trim() }
65
+ saveAuth(auth)
66
+ console.log('✔ 登录成功,凭据已保存')
67
+ return auth
68
+ }
69
+ finally {
70
+ rl.close()
71
+ }
72
+ }
73
+
74
+ export async function main(argv = process.argv.slice(2)) {
75
+ const opts = parseArgs(argv)
76
+ const auth = await ensureAuth(opts)
77
+
78
+ const store = createState()
79
+ const state = withLog(store.state)
80
+ state.baseUrl = auth.baseUrl
81
+ state.token = auth.token
82
+ const api = createApi(auth)
83
+
84
+ // ── 终端 + 组件树 ──
85
+ const terminal = opts.headless ? new VirtualTerminal() : new ProcessTerminal()
86
+ if (opts.headless) (globalThis).__tuiSmokeTerminal = terminal // 无头 e2e 注入句柄
87
+ const tui = new TuiMainScreen(terminal)
88
+ const chatLog = new ChatLog(state)
89
+ const monitorPane = new MonitorPane(state)
90
+ const hitlCard = new HitlCard(state)
91
+ const statusBar = new StatusBar(state)
92
+ const editor = new Editor(tui, theme.editor)
93
+ const { stack } = buildTree({ state, chatLog, monitorPane, hitlCard, statusBar, editor })
94
+
95
+ const push = (kind, text) => {
96
+ pushLog(state, kind, text)
97
+ store.notify()
98
+ }
99
+ const echo = (kind, text) => push(kind === 'warn' ? 'hitl' : kind, text)
100
+
101
+ // ── 装配动作(命令系统/提交路由的副作用入口) ──
102
+ let termConn = null
103
+ const actions = {
104
+ async switchChannel(channelId) {
105
+ if (state.activeChannelId && state.activeChannelId !== channelId) aep.unsubscribe(state.activeChannelId)
106
+ state.activeChannelId = channelId
107
+ aep.subscribe(channelId, seqCursor.get(channelId))
108
+ await actions.refreshAgents()
109
+ // 时间线回填:最近 20 条(人类/Agent 消息)
110
+ try {
111
+ const msgs = await api.listMessages(channelId, 20)
112
+ for (const m of msgs) {
113
+ const text = (m.parts ?? []).map(p => p?.text ?? '').join('').trim()
114
+ if (!text) continue
115
+ if (m.role === 'USER' || m.fromAgentId == null) push('user', text)
116
+ else push('agent', text)
117
+ }
118
+ }
119
+ catch { /* 回填失败不阻塞 */ }
120
+ const ch = state.channels.find(c => c.id === channelId)
121
+ push('system', `已切换到频道「${ch?.name ?? channelId.slice(0, 8)}」—— 普通文本发送任务,/help 查看命令。`)
122
+ store.notify()
123
+ },
124
+ async refreshAgents() {
125
+ if (!state.activeChannelId) return
126
+ const list = await api.listAgents(state.activeChannelId)
127
+ state.agents = list.map(a => ({ id: a.id, name: a.name, role: a.role, harness: a.harness, enabled: a.enabled }))
128
+ store.notify()
129
+ },
130
+ openMonitor(agent) {
131
+ termConn?.close()
132
+ termConn = null
133
+ state.monitor = { agentId: agent.id, name: agent.name, connected: false, waiting: false, streaming: false, lines: [], streamText: '' }
134
+ const mon = state.monitor
135
+ const addLine = (text, tone = 'dim') => {
136
+ mon.lines.push({ text, tone })
137
+ if (mon.lines.length > 120) mon.lines.splice(0, mon.lines.length - 120)
138
+ }
139
+ const applyFrame = (frame) => {
140
+ if (frame.type === 'message_update') {
141
+ const ev = frame.assistantMessageEvent
142
+ const delta = typeof ev?.delta === 'string' ? ev.delta : typeof frame.text === 'string' ? frame.text : ''
143
+ if (delta) mon.streamText = (mon.streamText + delta).slice(-2000)
144
+ mon.streaming = true
145
+ return
146
+ }
147
+ if (frame.type === 'message_end' || frame.type === 'turn_end') mon.streaming = false
148
+ const { lines } = reduceTermFrameSafe(frame)
149
+ for (const l of lines) addLine(l.text, l.tone)
150
+ }
151
+ termConn = connectTerm({
152
+ baseUrl: state.baseUrl,
153
+ token: state.token,
154
+ agentId: agent.id,
155
+ channelId: state.activeChannelId,
156
+ onMessage(msg) {
157
+ if (msg.type === 'term.init') {
158
+ mon.connected = true
159
+ mon.waiting = false
160
+ addLine(`── 已接入 ${msg.meta?.name ?? agent.name} pid ${msg.meta?.pid ?? '?'}(重放 ${msg.lastSeq ?? 0} 帧)──`, 'accent')
161
+ if (msg.hitl) {
162
+ upsertHitlLocal(state, { kind: 'omp-dialog', id: msg.hitl.id, channelId: state.activeChannelId, agentId: agent.id, agentName: agent.name, method: msg.hitl.method, title: msg.hitl.title, options: msg.hitl.options, message: msg.hitl.message })
163
+ addLine(`⏸ 待答对话框:${msg.hitl.title}(/hitl 作答)`, 'warn')
164
+ }
165
+ // 重放尾部 40 帧给上下文
166
+ for (const f of (msg.replay ?? []).slice(-40)) applyFrame(f.frame)
167
+ }
168
+ else if (msg.type === 'term.frames') {
169
+ for (const f of msg.frames ?? []) applyFrame(f.frame)
170
+ }
171
+ else if (msg.type === 'term.state') {
172
+ mon.streaming = msg.streaming
173
+ }
174
+ else if (msg.type === 'term.notice') {
175
+ addLine(msg.message, msg.level === 'error' ? 'error' : 'dim')
176
+ }
177
+ store.notify()
178
+ },
179
+ onClose(code) {
180
+ if (mon.agentId !== agent.id) return
181
+ mon.connected = false
182
+ if (code === 4404) {
183
+ mon.waiting = true
184
+ addLine('omp 进程未启动(随首个任务 spawn),等待接入…', 'warn')
185
+ }
186
+ store.notify()
187
+ },
188
+ })
189
+ push('system', `监控已开启:${agent.name}(右侧面板;/monitor off 关闭)`)
190
+ },
191
+ closeMonitor() {
192
+ termConn?.close()
193
+ termConn = null
194
+ state.monitor = { agentId: null, name: null, connected: false, waiting: false, streaming: false, lines: [], streamText: '' }
195
+ store.notify()
196
+ },
197
+ async submitHitlAnswer(text) {
198
+ const item = state.hitlAnswering
199
+ if (!item) return false
200
+ const body = { kind: item.kind, id: item.id }
201
+ if (item.kind === 'omp-dialog') {
202
+ if (item.method === 'confirm') {
203
+ const norm = text.trim().toLowerCase()
204
+ if (!['y', 'n', 'yes', 'no'].includes(norm)) {
205
+ push('error', 'confirm 对话框请输入 y(批准)或 n(拒绝)')
206
+ return true
207
+ }
208
+ body.confirmed = norm.startsWith('y')
209
+ }
210
+ else if (item.method === 'select') {
211
+ const idx = Number.parseInt(text.trim(), 10)
212
+ if (!Number.isInteger(idx) || idx < 1 || idx > (item.options?.length ?? 0)) {
213
+ push('error', `select 请输入 1-${item.options?.length ?? 0} 的序号`)
214
+ return true
215
+ }
216
+ body.value = item.options[idx - 1]
217
+ }
218
+ else {
219
+ body.value = text
220
+ }
221
+ }
222
+ else {
223
+ // dcw-approval:confirm 语义(y=批准/n=拒绝)+ 备注
224
+ const norm = text.trim().toLowerCase()
225
+ body.confirmed = norm.startsWith('y')
226
+ if (norm.length > 1) body.comment = text
227
+ }
228
+ try {
229
+ await api.hitlRespond(body)
230
+ push('system', `✔ 应答已提交(${item.title})`)
231
+ state.hitl = state.hitl.filter(i => !(i.kind === item.kind && i.id === item.id))
232
+ state.hitlAnswering = null
233
+ }
234
+ catch (err) {
235
+ push('error', `应答失败:${err.message}`)
236
+ if (String(err.code) === 'ALREADY_RESOLVED') state.hitlAnswering = null
237
+ }
238
+ store.notify()
239
+ return true
240
+ },
241
+ quit() {
242
+ aep.close()
243
+ termConn?.close()
244
+ tui.stop()
245
+ process.exit(0)
246
+ },
247
+ }
248
+ const ctx = { state, store, api, echo, push, actions }
249
+
250
+ // ── 编辑器提交路由:HITL 作答 → 命令 → 普通文本发任务 ──
251
+ editor.onSubmit = async (text) => {
252
+ const trimmed = text.trim()
253
+ editor.setText('')
254
+ editor.addToHistory?.(text)
255
+ if (!trimmed) return
256
+ if (state.hitlAnswering) {
257
+ await actions.submitHitlAnswer(trimmed)
258
+ return
259
+ }
260
+ if (trimmed.startsWith('/')) {
261
+ await dispatchCommand(ctx, trimmed)
262
+ store.notify()
263
+ return
264
+ }
265
+ if (!state.activeChannelId) {
266
+ push('error', '未选择频道:先 /channels + /channel use <名|序号>')
267
+ return
268
+ }
269
+ // messages 端点要求 toAgentId:普通文本缺省路由 lead
270
+ const ch = state.channels.find(c => c.id === state.activeChannelId)
271
+ const leadId = ch?.leadAgentId ?? state.agents.find(a => a.role === 'lead')?.id
272
+ if (!leadId) {
273
+ push('error', '当前频道没有 lead:用 /channel new --lead <名> 重建,或 /send <成员> 指定收件人。')
274
+ return
275
+ }
276
+ try {
277
+ await api.sendMessage(state.activeChannelId, { toAgentId: leadId, text: trimmed, priority: 'task', fromLabel: state.userName || '用户' })
278
+ }
279
+ catch (err) {
280
+ push('error', `发送失败:${err.message}`)
281
+ }
282
+ store.notify()
283
+ }
284
+
285
+ // ── AEP 事件流 ──
286
+ const seqCursor = new Map()
287
+ const aep = connectAep({
288
+ baseUrl: state.baseUrl,
289
+ token: state.token,
290
+ onEvent(e) {
291
+ if (e.channelId && typeof e.seq === 'number') {
292
+ aep.cursor(e.channelId, e.seq)
293
+ seqCursor.set(e.channelId, e.seq)
294
+ }
295
+ reduceEnvelope(state, e)
296
+ store.notify()
297
+ },
298
+ onState(s) {
299
+ state.connState = s
300
+ store.notify()
301
+ },
302
+ })
303
+
304
+ store.onChange(() => tui.requestRender())
305
+
306
+ // ── 组装视图 ──
307
+ tui.addChild({ render: width => [...stack.render(width), '', ...editor.render(width)] })
308
+ editor.setAutocompleteProvider?.(new CombinedAutocompleteProvider(slashCommandCompletions(), process.cwd()))
309
+ tui.setFocus(editor)
310
+
311
+ push('system', `AgentWorkShop TUI 已就绪 ${state.baseUrl}`)
312
+ push('system', '普通文本 = 发任务到当前频道;/help 命令列表;/hitl 处理待办。')
313
+
314
+ // ── 启动:频道自动选择 → 订阅 ──
315
+ try {
316
+ const channels = await api.listChannels()
317
+ state.channels = channels
318
+ // 空字符串 --channel 视为未指定('' 非 nullish,?? 不会走右侧)
319
+ const target = (opts.channel ? channels.find(c => c.name === opts.channel || c.id === opts.channel) : undefined)
320
+ ?? channels[0]
321
+ if (target) await actions.switchChannel(target.id)
322
+ else push('warn', '尚无频道:/channel new <名称> 创建一个。')
323
+ const me = await api.get('/api/workshop/users/me').catch(() => null)
324
+ state.userName = me?.user?.name ?? me?.name ?? '用户'
325
+ }
326
+ catch (err) {
327
+ push('error', `初始化失败:${err.message}`)
328
+ }
329
+
330
+ tui.start()
331
+ return tui
332
+ }
333
+
334
+ function upsertHitlLocal(state, item) {
335
+ const i = state.hitl.findIndex(x => x.kind === item.kind && x.id === item.id)
336
+ if (i >= 0) state.hitl[i] = item
337
+ else state.hitl.push(item)
338
+ }
339
+
340
+ /** reduceTermFrame 的防御包装(未知帧形状不炸渲染) */
341
+ function reduceTermFrameSafe(frame) {
342
+ try {
343
+ return reduceTermFrame(frame)
344
+ }
345
+ catch {
346
+ return { lines: [] }
347
+ }
348
+ }
349
+
350
+ // 直接运行入口(aw tui / pnpm tui 均落到这里)
351
+ if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replaceAll('\\', '/').split('/').pop())) {
352
+ main().catch((err) => {
353
+ console.error('[tui] 启动失败:', err.message)
354
+ process.exit(1)
355
+ })
356
+ }
@@ -0,0 +1,250 @@
1
+ // ============================================================
2
+ // TUI 命令系统 —— SlashCommand 集中注册 + 解析(仿 openclaw tui/commands.ts)。
3
+ // handler(ctx, args):ctx = { state, notify, api, ws, actions, echo }。
4
+ // actions 由 aw-tui 注入(switchChannel/openMonitor/submitHitl 等带副作用的装配)。
5
+ // ============================================================
6
+
7
+ /** 解析 '/cmd arg1 arg2 --flag val --bool' → { name, args, flags } */
8
+ export function parseCommand(input) {
9
+ const text = String(input ?? '').trim()
10
+ if (!text.startsWith('/')) return null
11
+ const tokens = text.slice(1).split(/\s+/).filter(Boolean)
12
+ const name = tokens.shift() ?? ''
13
+ const args = []
14
+ const flags = {}
15
+ for (let i = 0; i < tokens.length; i++) {
16
+ const tok = tokens[i]
17
+ if (tok.startsWith('--')) {
18
+ const key = tok.slice(2)
19
+ const next = tokens[i + 1]
20
+ if (next !== undefined && !next.startsWith('--')) {
21
+ flags[key] = next
22
+ i++
23
+ }
24
+ else flags[key] = true
25
+ }
26
+ else args.push(tok)
27
+ }
28
+ return { name, args, flags, rest: args.join(' ') }
29
+ }
30
+
31
+ export const commands = [
32
+ {
33
+ name: 'help',
34
+ description: '命令列表',
35
+ handler(ctx) {
36
+ const rows = commands.map(c => ` /${c.name}${c.usage ? ` ${c.usage}` : ''} — ${c.description}`)
37
+ ctx.echo('system', `可用命令:\n${rows.join('\n')}\n普通文本 = 发送任务到当前频道(lead);以 Agent 身份发信走 /send。`)
38
+ },
39
+ },
40
+ {
41
+ name: 'channels',
42
+ description: '列出我的频道',
43
+ async handler(ctx) {
44
+ const list = await ctx.api.listChannels()
45
+ ctx.state.channels = list
46
+ ctx.echo('system', `频道 ${list.length} 个:`)
47
+ for (const [i, c] of list.entries()) {
48
+ ctx.echo('system', ` ${i + 1}. ${c.name}${c.id === ctx.state.activeChannelId ? ' ◀ 当前' : ''}(id ${c.id.slice(0, 8)})`)
49
+ }
50
+ },
51
+ },
52
+ {
53
+ name: 'channel',
54
+ usage: 'new <name> [--desc …] [--lead <名>] | use <名|序号> | add <模板|名> [--role lead] [--harness omp] [--config JSON]',
55
+ description: '创建/切换频道、向频道放置 Agent',
56
+ async handler(ctx, args, flags) {
57
+ const sub = args.shift()
58
+ if (sub === 'new') {
59
+ const name = args.shift()
60
+ if (!name) return ctx.echo('error', '用法:/channel new <name> [--desc …] [--lead <名>]')
61
+ const desc = args.join(' ')
62
+ const body = { name, description: desc }
63
+ if (flags.lead) body.leadAgent = { name: String(flags.lead), harness: String(flags.harness ?? 'omp') }
64
+ // 响应为 { channelId, leadAgentId }(manager.createChannel 收敛)
65
+ const result = await ctx.api.createChannel(body)
66
+ ctx.state.channels.push({ id: result.channelId, name, description: desc, leadAgentId: result.leadAgentId ?? null })
67
+ ctx.echo('system', `✔ 频道已创建:${name}(id ${result.channelId.slice(0, 8)})`)
68
+ await ctx.actions.switchChannel(result.channelId)
69
+ }
70
+ else if (sub === 'use') {
71
+ const key = args.shift()
72
+ if (!key) return ctx.echo('error', '用法:/channel use <名|序号>(/channels 查看列表)')
73
+ const ch = resolveChannel(ctx.state, key)
74
+ if (!ch) return ctx.echo('error', `频道不存在:${key}`)
75
+ await ctx.actions.switchChannel(ch.id)
76
+ }
77
+ else if (sub === 'add') {
78
+ const name = args.shift()
79
+ if (!name) return ctx.echo('error', '用法:/channel add <模板名|名称> [--role lead|worker] [--harness omp] [--config JSON]')
80
+ const role = flags.role === 'lead' ? 'lead' : 'worker'
81
+ const harness = String(flags.harness ?? 'omp')
82
+ let body
83
+ const templates = await ctx.api.listTemplates().catch(() => [])
84
+ const tpl = templates.find(t => t.name === name)
85
+ if (tpl) {
86
+ body = { agentId: tpl.id, role, configOverride: parseConfigFlag(flags.config) }
87
+ }
88
+ else {
89
+ body = { name, harness, role, config: parseConfigFlag(flags.config) }
90
+ ctx.echo('system', `未找到模板「${name}」,按内联定义创建(harness=${harness})。`)
91
+ }
92
+ const inst = await ctx.api.addAgent(ctx.state.activeChannelId, body)
93
+ await ctx.actions.refreshAgents()
94
+ ctx.echo('system', `✔ Agent 已放入频道:${inst?.name ?? name}(${role})`)
95
+ }
96
+ else {
97
+ ctx.echo('error', '用法:/channel new|use|add …(/help)')
98
+ }
99
+ },
100
+ },
101
+ {
102
+ name: 'agents',
103
+ description: '当前频道成员与实时状态',
104
+ async handler(ctx) {
105
+ await ctx.actions.refreshAgents()
106
+ const rows = ctx.state.agents.map((a, i) => {
107
+ const st = ctx.state.agentStates[a.id]
108
+ const state = st?.state ?? 'idle'
109
+ const queued = st?.queued ?? 0
110
+ return ` ${i + 1}. ${a.name}${a.role === 'lead' ? '(lead)' : ''} ${state === 'busy' ? '● busy' : state === 'stopped' ? '○ stopped' : '○ idle'}${queued ? ` 队列${queued}` : ''} [${a.id.slice(0, 8)}]`
111
+ })
112
+ ctx.echo('system', `成员 ${rows.length}:\n${rows.join('\n') || ' (空 —— /channel add <名>)'}`)
113
+ },
114
+ },
115
+ {
116
+ name: 'send',
117
+ usage: '<agent名|序号> <文本…>',
118
+ description: '向指定 Agent 直发消息(忙碌时 steer 注入当前回合)',
119
+ async handler(ctx, args) {
120
+ const key = args.shift()
121
+ const text = args.join(' ')
122
+ if (!key || !text) return ctx.echo('error', '用法:/send <agent名|序号> <文本…>')
123
+ const agent = resolveAgent(ctx.state, key)
124
+ if (!agent) return ctx.echo('error', `成员不存在:${key}(/agents 查看)`)
125
+ await ctx.api.sendMessage(ctx.state.activeChannelId, { toAgentId: agent.id, text, priority: 'immediate', requireReply: true, fromLabel: ctx.state.userName || '用户' })
126
+ ctx.echo('system', `✔ 已直发 ${agent.name}`)
127
+ },
128
+ },
129
+ {
130
+ name: 'task',
131
+ usage: '<标题…> [--mode goal|loop|pipeline] [--assignee <agent>]',
132
+ description: '提交正式任务(缺省路由 lead,状态经 /tasks 跟踪)',
133
+ async handler(ctx, args, flags) {
134
+ const title = args.join(' ')
135
+ if (!title) return ctx.echo('error', '用法:/task <标题…> [--mode goal|loop|pipeline] [--assignee <agent>]')
136
+ const body = { title, mode: flags.mode ?? 'goal' }
137
+ if (flags.assignee) {
138
+ const a = resolveAgent(ctx.state, String(flags.assignee))
139
+ if (!a) return ctx.echo('error', `assignee 不存在:${flags.assignee}`)
140
+ body.assigneeId = a.id
141
+ }
142
+ const task = await ctx.api.submitTask(ctx.state.activeChannelId, body)
143
+ ctx.echo('system', `✔ 任务已提交:${task?.id?.slice(0, 8) ?? ''}「${title}」`)
144
+ },
145
+ },
146
+ {
147
+ name: 'tasks',
148
+ description: '任务列表与状态',
149
+ async handler(ctx) {
150
+ const list = await ctx.api.listTasks(ctx.state.activeChannelId)
151
+ ctx.state.tasks = list
152
+ const rows = list.slice(0, 20).map((t, i) => ` ${i + 1}. [${t.state}] ${t.title ?? t.taskId?.slice(0, 8)}${t.progress ? ` ${t.progress}%` : ''}`)
153
+ ctx.echo('system', `任务 ${list.length}:\n${rows.join('\n') || ' (无)'}`)
154
+ },
155
+ },
156
+ {
157
+ name: 'monitor',
158
+ usage: '<agent名|序号|off>',
159
+ description: '开/关右侧终端镜像面板(实时查看该 Agent 执行过程)',
160
+ async handler(ctx, args) {
161
+ const key = args.shift()
162
+ if (!key || key === 'off') {
163
+ ctx.actions.closeMonitor()
164
+ ctx.echo('system', '监控面板已关闭')
165
+ return
166
+ }
167
+ await ctx.actions.refreshAgents()
168
+ const agent = resolveAgent(ctx.state, key)
169
+ if (!agent) return ctx.echo('error', `成员不存在:${key}(/agents 查看)`)
170
+ ctx.actions.openMonitor(agent)
171
+ },
172
+ },
173
+ {
174
+ name: 'hitl',
175
+ usage: '[序号|off]',
176
+ description: '列出待人工处理事项;带序号进入作答;off 放弃作答',
177
+ async handler(ctx, args) {
178
+ const key = args.shift()
179
+ if (key === 'off') {
180
+ ctx.state.hitlAnswering = null
181
+ ctx.echo('system', '已放弃当前作答(对话框仍在待办中,可 /hitl 重进)')
182
+ return
183
+ }
184
+ if (key === undefined) {
185
+ const list = ctx.state.hitl.length > 0 ? ctx.state.hitl : await ctx.api.hitlPending(ctx.state.activeChannelId)
186
+ ctx.state.hitl = list
187
+ if (list.length === 0) return ctx.echo('system', '当前没有待人工处理的事项。')
188
+ const rows = list.map((it, i) => ` ${i + 1}. [${it.kind}] ${it.title} —— ${it.agentName}${it.expiresAt ? `(park 截止 ${it.expiresAt.slice(11, 19)})` : ''}`)
189
+ ctx.echo('warn', `待人工处理 ${list.length} 项:\n${rows.join('\n')}\n/hitl <序号> 进入作答。`)
190
+ return
191
+ }
192
+ const idx = Number.parseInt(key, 10) - 1
193
+ const item = ctx.state.hitl[idx]
194
+ if (!item) return ctx.echo('error', `序号无效:${key}(/hitl 查看列表)`)
195
+ ctx.state.hitlAnswering = item
196
+ ctx.echo('warn', `进入作答:${item.title} —— 完成输入回车提交,/hitl off 放弃。`)
197
+ },
198
+ },
199
+ {
200
+ name: 'quit',
201
+ description: '退出 TUI',
202
+ handler(ctx) {
203
+ ctx.actions.quit()
204
+ },
205
+ },
206
+ ]
207
+
208
+ function resolveChannel(state, key) {
209
+ const byIdx = state.channels[Number.parseInt(key, 10) - 1]
210
+ return state.channels.find(c => c.name === key || c.id === key) ?? (Number.isInteger(Number.parseInt(key, 10)) ? byIdx : undefined)
211
+ }
212
+
213
+ function resolveAgent(state, key) {
214
+ const byIdx = state.agents[Number.parseInt(key, 10) - 1]
215
+ return state.agents.find(a => a.name === key || a.id === key) ?? (Number.isInteger(Number.parseInt(key, 10)) ? byIdx : undefined)
216
+ }
217
+
218
+ function parseConfigFlag(raw) {
219
+ if (!raw || raw === true) return undefined
220
+ try {
221
+ return JSON.parse(String(raw))
222
+ }
223
+ catch {
224
+ throw new Error(`--config 不是合法 JSON:${raw}`)
225
+ }
226
+ }
227
+
228
+ /** 命令分发(编辑器 onSubmit 入口;返回 true = 已作为命令消费) */
229
+ export async function dispatchCommand(ctx, input) {
230
+ const parsed = parseCommand(input)
231
+ if (!parsed) return false
232
+ const cmd = commands.find(c => c.name === parsed.name)
233
+ if (!cmd) {
234
+ ctx.echo('error', `未知命令:/${parsed.name}(/help 查看命令列表)`)
235
+ return true
236
+ }
237
+ try {
238
+ await cmd.handler(ctx, parsed.args, parsed.flags)
239
+ }
240
+ catch (err) {
241
+ ctx.echo('error', `/${cmd.name} 失败:${err.message}`)
242
+ }
243
+ return true
244
+ }
245
+
246
+ /** pi-tui CombinedAutocompleteProvider 的命令表(斜杠补全;name 不带 '/'——
247
+ * provider 把 '/' 当触发符自带,带了会在应用补全时拼出 '//cmd') */
248
+ export function slashCommandCompletions() {
249
+ return commands.map(c => ({ name: c.name, description: c.description }))
250
+ }
@@ -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
+ }