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.
- package/.output/nitro.json +1 -1
- package/.output/public/_nuxt/B9Yie5HN.js +2 -0
- package/.output/public/_nuxt/CN7Zh8GQ.js +1 -0
- package/.output/public/_nuxt/CdSgfcYR.js +1 -0
- package/.output/public/_nuxt/CdfAP_IH2.js +1 -0
- package/.output/public/_nuxt/ChnfyQHz.js +1 -0
- package/.output/public/_nuxt/{BkKYf8xY.js → CyygA0q4.js} +4 -4
- package/.output/public/_nuxt/{Bz40vyZS.js → DFnGFEav.js} +7 -7
- package/.output/public/_nuxt/Xaf_HIR3.js +1 -0
- package/.output/public/_nuxt/ZugRk0CM2.js +4 -0
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/55c10854-ee0e-4be4-924b-a1b0e100fe11.json +1 -0
- package/.output/public/_nuxt/{default.iGJy9dP-.css → default.D17kXlD6.css} +1 -1
- package/.output/public/_nuxt/monitor.CmwpEUCI.css +1 -0
- package/.output/public/_nuxt/yIKY_V1P.js +1 -0
- package/.output/server/chunks/_/nitro.mjs +1131 -920
- package/.output/server/chunks/_/nitro.mjs.map +1 -1
- package/.output/server/chunks/build/{TownView-BkHxMiwZ.mjs → TownView-Bl8Gkcxi.mjs} +3 -3
- package/.output/server/chunks/build/{TownView-BkHxMiwZ.mjs.map → TownView-Bl8Gkcxi.mjs.map} +1 -1
- package/.output/server/chunks/build/{_id_-Bna1_JuB.mjs → _id_-CWLFGZjV.mjs} +2 -2
- package/.output/server/chunks/build/{_id_-Bna1_JuB.mjs.map → _id_-CWLFGZjV.mjs.map} +1 -1
- package/.output/server/chunks/build/_id_-GnRJPykX.mjs +1 -1
- package/.output/server/chunks/build/{_wsId_-CzLDGT_V.mjs → _wsId_-BhQMjyHG.mjs} +3 -4
- package/.output/server/chunks/build/{_wsId_-CzLDGT_V.mjs.map → _wsId_-BhQMjyHG.mjs.map} +1 -1
- package/.output/server/chunks/build/{daq-CmeM4u5u.mjs → daq-BOgpn9sl.mjs} +4 -5
- package/.output/server/chunks/build/daq-BOgpn9sl.mjs.map +1 -0
- package/.output/server/chunks/build/dcw-BTM5YU-B.mjs +1 -1
- package/.output/server/chunks/build/{default-Dt6xLdpd.mjs → default-CKJWhkot.mjs} +60 -17
- package/.output/server/chunks/build/{default-Dt6xLdpd.mjs.map → default-CKJWhkot.mjs.map} +1 -1
- package/.output/server/chunks/build/{logs-CUQt1z7D.mjs → logs-Dyk0I2I9.mjs} +3 -4
- package/.output/server/chunks/build/logs-Dyk0I2I9.mjs.map +1 -0
- package/.output/server/chunks/build/{monitor-yNoV98rf.mjs → monitor-KW8WLOJd.mjs} +34 -33
- package/.output/server/chunks/build/{monitor-yNoV98rf.mjs.map → monitor-KW8WLOJd.mjs.map} +1 -1
- package/.output/server/chunks/build/{town-BN4vQPb8.mjs → town-_4tEEEVW.mjs} +3 -4
- package/.output/server/chunks/build/town-_4tEEEVW.mjs.map +1 -0
- package/.output/server/chunks/build/useDaqStream-hwkhJHFU.mjs +1 -1
- package/.output/server/chunks/build/useDcwStream-Bdew_yrr.mjs +1 -1
- package/.output/server/chunks/build/{useWorkshopWs-xcdeiSmJ.mjs → useWorkshopWs-B3j5wIAK.mjs} +177 -3
- package/.output/server/chunks/build/useWorkshopWs-B3j5wIAK.mjs.map +1 -0
- package/.output/server/chunks/build/{workshop-CLhwe5eo.mjs → workshop-k0XZTFKg.mjs} +2 -3
- package/.output/server/chunks/build/workshop-k0XZTFKg.mjs.map +1 -0
- package/.output/server/chunks/routes/api/workshop/channel-templates/_id/instantiate.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/channels.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/hitl/pending.get.mjs +32 -0
- package/.output/server/chunks/routes/api/workshop/hitl/pending.get.mjs.map +1 -0
- package/.output/server/chunks/routes/api/workshop/hitl/respond.post.mjs +56 -0
- package/.output/server/chunks/routes/api/workshop/hitl/respond.post.mjs.map +1 -0
- package/.output/server/chunks/routes/api/workshop/plugins/_name/disable.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/plugins/_name/enable.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.put.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/workspaces/_id/channel-templates/_tplId_.post.mjs +1 -1
- package/.output/server/chunks/routes/renderer.mjs +2 -2
- package/.output/server/chunks/virtual/entry.mjs +17 -11
- package/.output/server/chunks/virtual/precomputed.mjs +1 -1
- package/.output/server/package.json +1 -1
- package/README.md +435 -434
- package/app/components/AppHeader.vue +138 -0
- package/app/composables/workshop/useWorkshopWs.ts +4 -0
- package/app/pages/monitor.vue +17 -1
- package/app/stores/workshop/hitl.ts +80 -0
- package/bin/aw.mjs +0 -0
- package/cli/commands/tui.mjs +19 -0
- package/config.yml +134 -134
- package/docs/tui.md +43 -0
- package/i18n/locales/en.ts +3 -0
- package/i18n/locales/zh-CN.ts +3 -0
- package/package.json +140 -144
- package/scripts/_spike-pitui.mjs +116 -0
- package/scripts/e2e-hitl-live.mjs +323 -0
- package/scripts/e2e-line-live.mjs +144 -0
- package/scripts/test-hitl-registry.ts +112 -0
- package/scripts/test-tui-commands.mjs +131 -0
- package/scripts/test-tui-reducers.mjs +72 -0
- package/scripts/tsconfig.json +14 -0
- package/scripts/tui-smoke.mjs +91 -0
- package/server/api/workshop/hitl/pending.get.ts +28 -0
- package/server/api/workshop/hitl/respond.post.ts +67 -0
- package/server/api/workshop/ws.ts +30 -1
- package/server/data/daqs.json +39 -17
- package/server/data/dcw-lines.json +14 -0
- package/server/data/dcw-products.json +35 -0
- package/server/data/dcw-recipes.json +140 -0
- package/server/data/dcw-rollback.json +111 -1
- package/server/data/dcw-runs.json +135 -90
- package/server/data/dcw-writes.json +120 -0
- package/server/data/dcws.json +400 -275
- package/server/data/line-runs.json +11 -1
- package/server/plugins/workshop.ts +7 -0
- package/server/services/workshop/agents/harness-terminal.ts +92 -25
- package/server/services/workshop/agents/hitl-registry.ts +181 -0
- package/server/services/workshop/agents/tool-approvals.ts +15 -0
- package/server/services/workshop/scene-events.ts +19 -9
- package/shared/workshop-protocol.ts +37 -0
- package/tui/README.md +65 -0
- package/tui/aw-tui.mjs +356 -0
- package/tui/commands/index.mjs +250 -0
- package/tui/components/chat-log.mjs +42 -0
- package/tui/components/hitl-card.mjs +36 -0
- package/tui/components/monitor-pane.mjs +39 -0
- package/tui/components/root.mjs +53 -0
- package/tui/components/status-bar.mjs +29 -0
- package/tui/lib/api.mjs +82 -0
- package/tui/lib/config.mjs +58 -0
- package/tui/lib/reducers.mjs +218 -0
- package/tui/lib/state.mjs +80 -0
- package/tui/lib/virtual-terminal.mjs +48 -0
- package/tui/lib/ws.mjs +124 -0
- package/tui/theme.mjs +38 -0
- package/.output/public/_nuxt/B7V9IfTL.js +0 -1
- package/.output/public/_nuxt/BE5QyG5J2.js +0 -1
- package/.output/public/_nuxt/BuKI8z-5.js +0 -1
- package/.output/public/_nuxt/D0C5Q0di.js +0 -1
- package/.output/public/_nuxt/DC-QMEk1.js +0 -2
- package/.output/public/_nuxt/DEkQ0qQ02.js +0 -1
- package/.output/public/_nuxt/builds/meta/c6c01cdd-762e-4fb8-86e7-753c5185b856.json +0 -1
- package/.output/public/_nuxt/dt2tKE95.js +0 -4
- package/.output/public/_nuxt/i_MKObHo.js +0 -1
- package/.output/public/_nuxt/monitor.41fCD6TB.css +0 -1
- package/.output/public/_nuxt/tpDMVY5w.js +0 -1
- package/.output/server/chunks/build/connection-Dc0fn6yd.mjs +0 -120
- package/.output/server/chunks/build/connection-Dc0fn6yd.mjs.map +0 -1
- package/.output/server/chunks/build/daq-CmeM4u5u.mjs.map +0 -1
- package/.output/server/chunks/build/logs-CUQt1z7D.mjs.map +0 -1
- package/.output/server/chunks/build/town-BN4vQPb8.mjs.map +0 -1
- package/.output/server/chunks/build/useWorkshopWs-xcdeiSmJ.mjs.map +0 -1
- 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
|
+
}
|