@mobius-os/mobius 0.3.45 → 0.3.46
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/package.json +1 -1
- package/src/api.ts +8 -2
- package/src/components/Chat.tsx +41 -12
- package/src/hooks/useChat.ts +33 -2
- package/src/lib/entry-view.ts +8 -1
- package/src/lib/screen-text.ts +6 -0
- package/src/sse.ts +5 -0
- package/src/types.ts +10 -0
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
AnyEntry,
|
|
10
10
|
AuthConfig,
|
|
11
11
|
HistoryGroup,
|
|
12
|
+
HistoryPendingOpener,
|
|
12
13
|
Issue,
|
|
13
14
|
LoginResponse,
|
|
14
15
|
Memory,
|
|
@@ -163,11 +164,16 @@ export class MobiusClient {
|
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
// ── agent-history (协议 ①②: 组元数据 + 整组条目) ──────────────────────────
|
|
166
|
-
/** ① 全部组元数据,
|
|
167
|
-
async listHistoryGroups(sessionId: string): Promise<{ session_version: number; groups: HistoryGroup[] }> {
|
|
167
|
+
/** ① 全部组元数据, 一次给全; 顺带返回挂起中的开轮卡 (pending). */
|
|
168
|
+
async listHistoryGroups(sessionId: string): Promise<{ session_version: number; groups: HistoryGroup[]; pending: HistoryPendingOpener[] }> {
|
|
168
169
|
return this.request(`/api/sessions/${encodeURIComponent(sessionId)}/groups`)
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
/** 打断当前 turn 并出队下一条排队指令 (不追加新 prompt). */
|
|
173
|
+
async pauseToDequeue(sessionId: string): Promise<{ ok: boolean }> {
|
|
174
|
+
return this.request(`/api/sessions/${encodeURIComponent(sessionId)}/pause-to-dequeue`, { method: 'POST', body: '{}' })
|
|
175
|
+
}
|
|
176
|
+
|
|
171
177
|
/** ② 某组全部条目 (全量, 无分页, 条目不可变). */
|
|
172
178
|
async listHistoryGroupEntries(sessionId: string, groupId: string): Promise<{ group_id: string; version: number; entries: AnyEntry[] }> {
|
|
173
179
|
return this.request(`/api/sessions/${encodeURIComponent(sessionId)}/groups/${encodeURIComponent(groupId)}/entries`)
|
package/src/components/Chat.tsx
CHANGED
|
@@ -161,15 +161,32 @@ export function ChatScreen({ client, ready, webUserId, resumeSessionId, onClear,
|
|
|
161
161
|
// (type:user / response_item.message[user] / event_msg.user_message) 合并成 1 条,
|
|
162
162
|
// 避免在累积视图里把同一条提问显示多次.
|
|
163
163
|
const dedupedEntries = useMemo(() => dedupeUserEntries(chat.entries), [chat.entries])
|
|
164
|
-
const pendingEntry = useMemo<AnyEntry | null>(() =>
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
)
|
|
164
|
+
const pendingEntry = useMemo<AnyEntry | null>(() => {
|
|
165
|
+
// 忙时提交的指令已由排队行统一呈现, 乐观占位退役 (避免 "You:" 与 "排队" 双重显示).
|
|
166
|
+
if (chat.pending.length > 0) return null
|
|
167
|
+
return chat.pendingUser === null ? null : ({
|
|
168
|
+
type: 'user',
|
|
169
|
+
__id: '__pending-user__',
|
|
170
|
+
message: { role: 'user', content: chat.pendingUser },
|
|
171
|
+
})
|
|
172
|
+
}, [chat.pendingUser, chat.pending.length])
|
|
173
|
+
// 排队行 (合成条目): 显示最后一条挂起指令 + 总数, 紧跟对话末尾.
|
|
174
|
+
const pendingQueueEntry = useMemo<AnyEntry | null>(() => {
|
|
175
|
+
if (chat.pending.length === 0) return null
|
|
176
|
+
const last = chat.pending[chat.pending.length - 1]
|
|
177
|
+
return {
|
|
178
|
+
type: '__pending_queue__',
|
|
179
|
+
__id: '__pending-queue__',
|
|
180
|
+
text: last?.user_summary ?? '',
|
|
181
|
+
count: chat.pending.length,
|
|
182
|
+
}
|
|
183
|
+
}, [chat.pending])
|
|
184
|
+
const transcriptEntries = useMemo(() => {
|
|
185
|
+
const parts = [...dedupedEntries]
|
|
186
|
+
if (pendingEntry) parts.push(pendingEntry)
|
|
187
|
+
if (pendingQueueEntry) parts.push(pendingQueueEntry)
|
|
188
|
+
return parts
|
|
189
|
+
}, [dedupedEntries, pendingEntry, pendingQueueEntry])
|
|
173
190
|
|
|
174
191
|
// Markdown parsing and wrapping are paid once per entry/terminal width. Keep
|
|
175
192
|
// the two most recent widths so resize-back does not immediately reparse the
|
|
@@ -414,6 +431,8 @@ export function ChatScreen({ client, ready, webUserId, resumeSessionId, onClear,
|
|
|
414
431
|
commands={SLASH_COMMANDS}
|
|
415
432
|
onHeightChange={setComposerRows}
|
|
416
433
|
inputActiveRef={chatInputActiveRef}
|
|
434
|
+
onPauseToDequeue={chat.pauseToDequeue}
|
|
435
|
+
hasPending={chat.pending.length > 0}
|
|
417
436
|
/>
|
|
418
437
|
<StatusArea
|
|
419
438
|
ready={ready}
|
|
@@ -499,7 +518,8 @@ function ScreenText({ row, text }: { row: ScreenRow; text: string }) {
|
|
|
499
518
|
: tone === 'edit_header' || tone === 'reasoning' ? 'magenta'
|
|
500
519
|
: tone === 'edit_new' ? 'green'
|
|
501
520
|
: tone === 'system' ? 'yellow'
|
|
502
|
-
:
|
|
521
|
+
: tone === 'pending' ? 'yellow'
|
|
522
|
+
: undefined
|
|
503
523
|
const dimColor = tone === 'tool_result' || tone === 'tool_error' || tone === 'reasoning' || tone === 'system'
|
|
504
524
|
return <Text wrap="truncate-end" bold={tone === 'user'} dimColor={dimColor} color={color}>{text}</Text>
|
|
505
525
|
}
|
|
@@ -614,9 +634,12 @@ interface ComposerProps {
|
|
|
614
634
|
commands: { cmd: string; desc: string }[]
|
|
615
635
|
onHeightChange?: (rows: number) => void
|
|
616
636
|
inputActiveRef?: React.RefObject<boolean>
|
|
637
|
+
// 排队插队: 有挂起指令时空输入回车触发; hasPending 控制提示文案与触发开关.
|
|
638
|
+
onPauseToDequeue?: () => void
|
|
639
|
+
hasPending?: boolean
|
|
617
640
|
}
|
|
618
641
|
|
|
619
|
-
export function Composer({ onSubmit, onStop, onQuit, typing, commands, onHeightChange, inputActiveRef }: ComposerProps) {
|
|
642
|
+
export function Composer({ onSubmit, onStop, onQuit, typing, commands, onHeightChange, inputActiveRef, onPauseToDequeue, hasPending }: ComposerProps) {
|
|
620
643
|
const [value, setValue] = useState('')
|
|
621
644
|
const [cursor, setCursor] = useState(0)
|
|
622
645
|
const [popupIdx, setPopupIdx] = useState(0)
|
|
@@ -837,6 +860,9 @@ export function Composer({ onSubmit, onStop, onQuit, typing, commands, onHeightC
|
|
|
837
860
|
onSubmit(submitted)
|
|
838
861
|
edit('', 0)
|
|
839
862
|
setHistIdx(null)
|
|
863
|
+
} else if (hasPending) {
|
|
864
|
+
// 空输入回车 = 插队: 打断当前 turn 并出队下一条排队指令.
|
|
865
|
+
void onPauseToDequeue?.()
|
|
840
866
|
}
|
|
841
867
|
return
|
|
842
868
|
}
|
|
@@ -969,7 +995,10 @@ export function Composer({ onSubmit, onStop, onQuit, typing, commands, onHeightC
|
|
|
969
995
|
<Box justifyContent="space-between">
|
|
970
996
|
{confirmQuit
|
|
971
997
|
? <Text color="yellowBright" bold>请再次按下Ctrl+C退出</Text>
|
|
972
|
-
: <Text dimColor>
|
|
998
|
+
: <Text dimColor>
|
|
999
|
+
{hasPending ? <Text color="yellowBright">空回车插队 · </Text> : null}
|
|
1000
|
+
{(stdout.columns ?? 80) >= 72 ? 'Enter 发送 · Shift+Enter / Alt+Enter / Ctrl+J 换行' : 'Enter 发送 · Alt+Enter / Ctrl+J 换行'}
|
|
1001
|
+
</Text>}
|
|
973
1002
|
<Text dimColor>{wrapped.length > maxRows ? `${visualCursor + 1}/${wrapped.length} 行` : `${wrapped.length} 行`}</Text>
|
|
974
1003
|
</Box>
|
|
975
1004
|
</Box>
|
package/src/hooks/useChat.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { SseConnection } from '../sse.js'
|
|
|
19
19
|
import { updateIssuePreference } from '../config.js'
|
|
20
20
|
import { tuiAimuxIdentifier, probeAimuxBridgeConnection } from '../aimux.js'
|
|
21
21
|
import { viewsForEntry } from '../lib/entry-view.js'
|
|
22
|
-
import type { AnyEntry } from '../types.js'
|
|
22
|
+
import type { AnyEntry, HistoryPendingOpener } from '../types.js'
|
|
23
23
|
import type { ReadyState } from '../components/PrepScreen.js'
|
|
24
24
|
|
|
25
25
|
export interface ChatApi {
|
|
@@ -31,12 +31,14 @@ export interface ChatApi {
|
|
|
31
31
|
export interface ChatController {
|
|
32
32
|
entries: AnyEntry[]
|
|
33
33
|
pendingUser: string | null
|
|
34
|
+
pending: HistoryPendingOpener[]
|
|
34
35
|
typing: boolean
|
|
35
36
|
sending: boolean
|
|
36
37
|
error: string | null
|
|
37
38
|
sessionId: string | null
|
|
38
39
|
send: (text: string) => Promise<void>
|
|
39
40
|
stop: () => Promise<void>
|
|
41
|
+
pauseToDequeue: () => Promise<void>
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
let ID = 0
|
|
@@ -110,6 +112,7 @@ export function useChat({ client, ready, resumeSessionId }: ChatApi): ChatContro
|
|
|
110
112
|
const [sessionId, setSessionId] = useState<string | null>(resumeSessionId ?? null)
|
|
111
113
|
const [entries, setEntries] = useState<AnyEntry[]>([])
|
|
112
114
|
const [pendingUser, setPendingUser] = useState<string | null>(null)
|
|
115
|
+
const [pending, setPending] = useState<HistoryPendingOpener[]>([])
|
|
113
116
|
const [typing, setTyping] = useState(false)
|
|
114
117
|
const [sending, setSending] = useState(false)
|
|
115
118
|
const [error, setError] = useState<string | null>(null)
|
|
@@ -185,6 +188,8 @@ export function useChat({ client, ready, resumeSessionId }: ChatApi): ChatContro
|
|
|
185
188
|
try {
|
|
186
189
|
const data = await client.listHistoryGroups(sid)
|
|
187
190
|
const groups: any[] = Array.isArray(data?.groups) ? data.groups : []
|
|
191
|
+
// 挂起中的开轮卡 (排队指令): /groups 是权威快照, 覆盖本地增量.
|
|
192
|
+
setPending(Array.isArray(data?.pending) ? data.pending : [])
|
|
188
193
|
const local = groupSlotsRef.current
|
|
189
194
|
const targets = local.size === 0 ? groups.slice(-BOOTSTRAP_GROUP_COUNT) : groups
|
|
190
195
|
let changed = false
|
|
@@ -240,6 +245,9 @@ export function useChat({ client, ready, resumeSessionId }: ChatApi): ChatContro
|
|
|
240
245
|
version: Number(group.version) || 1,
|
|
241
246
|
entries: [],
|
|
242
247
|
})
|
|
248
|
+
// 新组开轮 = 后端已把挂起的 pending_round_openers 一次性出队 (flushPendingOpenersToSink).
|
|
249
|
+
// 排队行随之清空; 随后 entries 事件会把这一整组内容补齐.
|
|
250
|
+
setPending([])
|
|
243
251
|
},
|
|
244
252
|
onEntries: ({ group_id, group_id_version, entries }) => {
|
|
245
253
|
if (process.env.MOBIUS_TUI_DEBUG) console.error('[onEntries]', group_id, group_id_version, entries.length)
|
|
@@ -269,6 +277,16 @@ export function useChat({ client, ready, resumeSessionId }: ChatApi): ChatContro
|
|
|
269
277
|
setPendingUser(null)
|
|
270
278
|
}
|
|
271
279
|
},
|
|
280
|
+
onPendingOpener: (opener) => {
|
|
281
|
+
// 忙时提交的新指令被挂起: 追加到排队行 (uuid 去重), 乐观占位随之退役.
|
|
282
|
+
if (!opener || typeof opener !== 'object') return
|
|
283
|
+
const id = String(opener.id ?? '')
|
|
284
|
+
setPending(prev => {
|
|
285
|
+
if (id && prev.some(p => p.id === id)) return prev
|
|
286
|
+
return [...prev, { id, opener_ts: opener.opener_ts ?? null, user_summary: opener.user_summary ?? '' }]
|
|
287
|
+
})
|
|
288
|
+
setPendingUser(null)
|
|
289
|
+
},
|
|
272
290
|
onSubscribed: () => {
|
|
273
291
|
reconnectAttemptRef.current = 0
|
|
274
292
|
// 订阅即对账 (首开 = bootstrap 拉末尾几组; 重连 = stateless 补差额).
|
|
@@ -521,5 +539,18 @@ export function useChat({ client, ready, resumeSessionId }: ChatApi): ChatContro
|
|
|
521
539
|
pollNowRef.current?.()
|
|
522
540
|
}, [sessionId, client, updateTyping])
|
|
523
541
|
|
|
524
|
-
|
|
542
|
+
// 打断当前 turn 并出队下一条排队指令 (空输入回车 / 插队). 不追加新 prompt,
|
|
543
|
+
// 后端对 claude-code/codex 发一次 C-c, deepseek harness 是空实现. 排队行会在
|
|
544
|
+
// 新组开轮 (group_created) 时被清空, 这里只需触发并刷新状态轮询.
|
|
545
|
+
const pauseToDequeue = useCallback(async () => {
|
|
546
|
+
if (!sessionId) return
|
|
547
|
+
statusEpochRef.current += 1
|
|
548
|
+
try { await client.pauseToDequeue(sessionId) } catch (e: any) {
|
|
549
|
+
const msg = e instanceof ApiError ? e.message : `插队失败: ${e?.message ?? e}`
|
|
550
|
+
setError(msg)
|
|
551
|
+
}
|
|
552
|
+
pollNowRef.current?.()
|
|
553
|
+
}, [sessionId, client])
|
|
554
|
+
|
|
555
|
+
return { entries, pendingUser, pending, typing, sending, error, sessionId, send, stop, pauseToDequeue }
|
|
525
556
|
}
|
package/src/lib/entry-view.ts
CHANGED
|
@@ -31,6 +31,7 @@ export type EntryView =
|
|
|
31
31
|
| { kind: 'write_file'; filePath: string; content: string }
|
|
32
32
|
| { kind: 'system'; text: string }
|
|
33
33
|
| { kind: 'error'; text: string }
|
|
34
|
+
| { kind: 'pending'; text: string; count: number }
|
|
34
35
|
|
|
35
36
|
export interface ToolResultView {
|
|
36
37
|
text: string
|
|
@@ -113,7 +114,7 @@ function extractLocalCommandParts(entry: AnyEntry): LocalCommandPart[] {
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
/**
|
|
116
|
-
* 整卡隐藏的噪声: 对齐 web entry-classify.ts isHiddenJsonlNoiseEntry 的
|
|
117
|
+
* 整卡隐藏的噪声: 对齐 web entry-classify.ts isHiddenJsonlNoiseEntry 的 8 类
|
|
117
118
|
* - token_count : codex 每轮 token 用量统计 (event_msg)
|
|
118
119
|
* - environment_context : codex 每轮注入的 <environment_context> 纯系统 user 消息
|
|
119
120
|
* - session_meta : codex 会话首条元数据
|
|
@@ -121,10 +122,12 @@ function extractLocalCommandParts(entry: AnyEntry): LocalCommandPart[] {
|
|
|
121
122
|
* - turn_duration : Claude Code 每轮结束注入的 system 耗时统计
|
|
122
123
|
* - skill_listing : Claude Code 注入的可用 Skill 清单
|
|
123
124
|
* - agent_listing_delta : Claude Code 注入的可用 subagent 清单
|
|
125
|
+
* - mcp_tool_call_end : MCP 工具调用完成生命周期标记
|
|
124
126
|
* 注: context_compacted 不在此列 (对齐 web — 它保留为可见事件, TUI 显示成 system 行).
|
|
125
127
|
*/
|
|
126
128
|
export function isHiddenNoise(entry: AnyEntry): boolean {
|
|
127
129
|
if (entry?.type === 'event_msg' && entry?.payload?.type === 'token_count') return true
|
|
130
|
+
if (entry?.type === 'event_msg' && entry?.payload?.type === 'mcp_tool_call_end') return true
|
|
128
131
|
if (entry?.type === 'session_meta') return true
|
|
129
132
|
if (entry?.type === 'turn_context') return true
|
|
130
133
|
if (entry?.type === 'system' && entry?.subtype === 'turn_duration') return true
|
|
@@ -515,6 +518,10 @@ function parseCustomToolCall(raw: any): { name: string; input: Record<string, an
|
|
|
515
518
|
export function viewsForBlock(block: Block): EntryView[] {
|
|
516
519
|
const entry = block.entry
|
|
517
520
|
if (!entry || typeof entry !== 'object') return [{ kind: 'skip' }]
|
|
521
|
+
// 排队行 (合成条目): 显示忙时挂起的用户指令, 与 web 排队卡片同形.
|
|
522
|
+
if (entry.type === '__pending_queue__') {
|
|
523
|
+
return [{ kind: 'pending', text: String(entry.text ?? ''), count: Number(entry.count) || 1 }]
|
|
524
|
+
}
|
|
518
525
|
if (isHiddenNoise(entry)) return [{ kind: 'skip' }]
|
|
519
526
|
const type = entry.type
|
|
520
527
|
|
package/src/lib/screen-text.ts
CHANGED
|
@@ -118,6 +118,7 @@ export type ScreenRowTone =
|
|
|
118
118
|
| 'reasoning'
|
|
119
119
|
| 'system'
|
|
120
120
|
| 'error'
|
|
121
|
+
| 'pending'
|
|
121
122
|
|
|
122
123
|
export interface ScreenRow {
|
|
123
124
|
/** Visible text used for geometry, hit-testing, and clipboard extraction. */
|
|
@@ -204,6 +205,11 @@ export function viewScreenRows(view: EntryView, columns: number): ScreenRows {
|
|
|
204
205
|
const rows = view.text.split('\n').map((l, i) => `${i === 0 ? '⚠ ' : ' '}${l}`)
|
|
205
206
|
return { marginTop: true, rows: fit(rows, 'error') }
|
|
206
207
|
}
|
|
208
|
+
case 'pending': {
|
|
209
|
+
const summary = clampLines(view.text, width - 6, 1)[0] || '(无内容)'
|
|
210
|
+
const tail = view.count > 1 ? ` · 等 ${view.count} 条指令` : ''
|
|
211
|
+
return { marginTop: true, rows: fit([`⚡ 排队 · ${summary}${tail}`], 'pending') }
|
|
212
|
+
}
|
|
207
213
|
default:
|
|
208
214
|
return { marginTop: false, rows: [] }
|
|
209
215
|
}
|
package/src/sse.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface SseHandlers {
|
|
|
19
19
|
onGroupCreated?: (group: any) => void
|
|
20
20
|
/** ③ 组条目增量: version = 应用该批后的组版本 (调用方水位线判据). */
|
|
21
21
|
onEntries?: (payload: { group_id: string; group_id_version: number; entries: AnyEntry[] }) => void
|
|
22
|
+
/** 挂起开轮卡 (忙时提交、尚未出队的用户指令): { id, opener_ts, user_summary }. */
|
|
23
|
+
onPendingOpener?: (pending: { id: string; opener_ts: string | null; user_summary: string }) => void
|
|
22
24
|
onTyping?: (active: boolean) => void
|
|
23
25
|
onError?: (message: string, category?: string) => void
|
|
24
26
|
onClose?: () => void
|
|
@@ -117,6 +119,9 @@ export class SseConnection {
|
|
|
117
119
|
entries: Array.isArray(p.entries) ? p.entries : [],
|
|
118
120
|
})
|
|
119
121
|
break
|
|
122
|
+
case 'pending_opener':
|
|
123
|
+
this.handlers.onPendingOpener?.(p.entry)
|
|
124
|
+
break
|
|
120
125
|
case 'typing':
|
|
121
126
|
this.handlers.onTyping?.(!!p.active)
|
|
122
127
|
break
|
package/src/types.ts
CHANGED
|
@@ -203,6 +203,16 @@ export interface HistoryGroup {
|
|
|
203
203
|
entry_count: number
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
/**
|
|
207
|
+
* 挂起中的开轮卡 (pending_round_openers): 忙时提交、尚未出队的用户指令.
|
|
208
|
+
* /groups 端点返回的 `pending` 数组与 SSE `pending_opener` 事件的 `entry` 字段同形.
|
|
209
|
+
*/
|
|
210
|
+
export interface HistoryPendingOpener {
|
|
211
|
+
id: string
|
|
212
|
+
opener_ts: string | null
|
|
213
|
+
user_summary: string
|
|
214
|
+
}
|
|
215
|
+
|
|
206
216
|
// ── SSE envelope events (GET /api/sessions/:id/events) ───────────────────────
|
|
207
217
|
// Each SSE frame's data is a JSON object with an `event` discriminator.
|
|
208
218
|
export type SseEvent =
|