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