@veltra/ai 2.1.6 → 2.1.8

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 (71) hide show
  1. package/README.md +42 -0
  2. package/dist/chat/fold.d.ts +41 -0
  3. package/dist/chat/fold.js +208 -0
  4. package/dist/chat/fold.js.map +1 -0
  5. package/dist/chat/index.d.ts +4 -2
  6. package/dist/chat/session.d.ts +123 -0
  7. package/dist/chat/session.js +79 -0
  8. package/dist/chat/session.js.map +1 -0
  9. package/dist/chat/types.d.ts +38 -1
  10. package/dist/chat/use-chat-helpers.js +39 -0
  11. package/dist/chat/use-chat-helpers.js.map +1 -0
  12. package/dist/chat/use-chat-session.js +68 -0
  13. package/dist/chat/use-chat-session.js.map +1 -0
  14. package/dist/chat/use-chat.d.ts +50 -2
  15. package/dist/chat/use-chat.js +73 -39
  16. package/dist/chat/use-chat.js.map +1 -1
  17. package/dist/components/ai-chat/ai-chat.js +49 -14
  18. package/dist/components/ai-chat/ai-chat.js.map +1 -1
  19. package/dist/components/ai-chat/approval-banner.js +54 -0
  20. package/dist/components/ai-chat/approval-banner.js.map +1 -0
  21. package/dist/components/ai-chat/ask-question.js +32 -51
  22. package/dist/components/ai-chat/ask-question.js.map +1 -1
  23. package/dist/components/ai-chat/di.js.map +1 -1
  24. package/dist/components/ai-chat/job-bar.js +48 -0
  25. package/dist/components/ai-chat/job-bar.js.map +1 -0
  26. package/dist/components/ai-chat/message-list.js +5 -2
  27. package/dist/components/ai-chat/message-list.js.map +1 -1
  28. package/dist/components/ai-chat/queue-list.js +9 -3
  29. package/dist/components/ai-chat/queue-list.js.map +1 -1
  30. package/dist/components/ai-chat/side-panel.js +12 -30
  31. package/dist/components/ai-chat/side-panel.js.map +1 -1
  32. package/dist/components/ai-chat/style2.css +146 -8
  33. package/dist/components/ai-chat/tool-call.js +51 -40
  34. package/dist/components/ai-chat/tool-call.js.map +1 -1
  35. package/dist/components/ai-chat/tool-icons.js +31 -0
  36. package/dist/components/ai-chat/tool-icons.js.map +1 -0
  37. package/dist/components/ai-chat/tool-json.js +40 -0
  38. package/dist/components/ai-chat/tool-json.js.map +1 -0
  39. package/dist/components/ai-orb/style2.css +1 -1
  40. package/dist/index.d.ts +6 -4
  41. package/dist/index.js +3 -1
  42. package/dist/tools/ask-question/ask-question.js +48 -2
  43. package/dist/tools/ask-question/ask-question.js.map +1 -1
  44. package/dist/types/ai-chat.d.ts +17 -6
  45. package/package.json +7 -7
  46. package/src/chat/__test__/fold.test.ts +136 -0
  47. package/src/chat/__test__/session.test.ts +191 -0
  48. package/src/chat/__test__/use-chat.test.ts +268 -1
  49. package/src/chat/fold.ts +260 -0
  50. package/src/chat/index.ts +2 -0
  51. package/src/chat/session.ts +168 -0
  52. package/src/chat/types.ts +40 -0
  53. package/src/chat/use-chat-helpers.ts +39 -0
  54. package/src/chat/use-chat-session.ts +87 -0
  55. package/src/chat/use-chat.ts +90 -44
  56. package/src/components/ai-chat/__test__/ai-chat.test.ts +405 -7
  57. package/src/components/ai-chat/__test__/tool-icons.test.ts +33 -0
  58. package/src/components/ai-chat/ai-chat.vue +39 -4
  59. package/src/components/ai-chat/approval-banner.vue +37 -0
  60. package/src/components/ai-chat/ask-question.vue +38 -54
  61. package/src/components/ai-chat/di.ts +6 -4
  62. package/src/components/ai-chat/job-bar.vue +41 -0
  63. package/src/components/ai-chat/message-list.vue +4 -1
  64. package/src/components/ai-chat/queue-list.vue +3 -1
  65. package/src/components/ai-chat/side-panel.vue +15 -13
  66. package/src/components/ai-chat/style.scss +158 -6
  67. package/src/components/ai-chat/tool-call.vue +46 -34
  68. package/src/components/ai-chat/tool-icons.ts +33 -0
  69. package/src/components/ai-chat/tool-json.vue +52 -0
  70. package/src/tools/ask-question/ask-question.ts +53 -4
  71. package/src/types/ai-chat.ts +17 -5
@@ -1,7 +1,14 @@
1
1
  import { describe, expect, it, vi } from 'vitest'
2
- import { nextTick, reactive } from 'vue'
2
+ import { effectScope, nextTick, reactive } from 'vue'
3
3
 
4
+ import * as toolsModule from '../../tools'
4
5
  import type { AiChatEmits, AiChatProps } from '../../types'
6
+ import {
7
+ createServerTransport,
8
+ isServerTransport,
9
+ type ChatSessionAdapter,
10
+ type ChatSessionEvent
11
+ } from '../session'
5
12
  import type {
6
13
  ChatMessage,
7
14
  ChatTool,
@@ -635,3 +642,263 @@ describe('useChat', () => {
635
642
  expect(chat.lastTurnUsage.value).toBeNull()
636
643
  })
637
644
  })
645
+
646
+ function createFakeAdapter(options?: {
647
+ history?: ChatSessionEvent[]
648
+ }): ChatSessionAdapter & {
649
+ emit: (event: ChatSessionEvent) => void
650
+ disconnect: () => void
651
+ unsubscribed: () => boolean
652
+ } {
653
+ let listener: { onEvent(event: ChatSessionEvent): void; onDisconnect?(): void } | undefined
654
+ let unsubscribed = false
655
+
656
+ const adapter: ChatSessionAdapter = {
657
+ subscribe(handlers) {
658
+ listener = handlers
659
+ return () => {
660
+ unsubscribed = true
661
+ listener = undefined
662
+ }
663
+ },
664
+ send: vi.fn(async () => {}),
665
+ cancel: vi.fn(async () => {}),
666
+ respond: vi.fn(async () => {}),
667
+ fetchHistory: vi.fn(async () => ({ events: options?.history ?? [], hasMore: false })),
668
+ selectModel: vi.fn(async () => {})
669
+ }
670
+
671
+ return Object.assign(adapter, {
672
+ emit(event: ChatSessionEvent) {
673
+ listener?.onEvent(event)
674
+ },
675
+ disconnect() {
676
+ listener?.onDisconnect?.()
677
+ },
678
+ unsubscribed: () => unsubscribed
679
+ })
680
+ }
681
+
682
+ async function setupSession(options?: {
683
+ history?: ChatSessionEvent[]
684
+ tools?: AiChatProps['tools']
685
+ models?: AiChatProps['models']
686
+ model?: string
687
+ }) {
688
+ const adapter = createFakeAdapter({ history: options?.history })
689
+ const props = reactive<AiChatProps>({
690
+ transport: createServerTransport(adapter),
691
+ tools: options?.tools,
692
+ models: options?.models,
693
+ model: options?.model
694
+ })
695
+ const emit = createEmit()
696
+ const chat = useChat({ props, emit })
697
+ await vi.waitFor(() => {
698
+ expect(adapter.fetchHistory).toHaveBeenCalled()
699
+ })
700
+ return { adapter, props, emit, chat }
701
+ }
702
+
703
+ describe('useChat session', () => {
704
+ it('函数 transport 时 isServerTransport 为 false', () => {
705
+ const emit = createEmit()
706
+ const transport = textTransport('x')
707
+ useChat({ props: { transport }, emit })
708
+ expect(isServerTransport(transport)).toBe(false)
709
+ })
710
+
711
+ it('open + fetchHistory 播种 messages;函数 transport 不调用 open/fetchHistory', async () => {
712
+ const { chat, adapter } = await setupSession({
713
+ history: [{ type: 'user/message', messageId: 'u1', seq: 1, content: '历史' }]
714
+ })
715
+ expect(isServerTransport(createServerTransport(adapter))).toBe(true)
716
+ expect(chat.messages.value).toEqual([{ id: 'u1', role: 'user', content: '历史' }])
717
+
718
+ const fnTransport = textTransport('ok')
719
+ const fetchHistory = vi.fn()
720
+ const open = vi.fn()
721
+ useChat({
722
+ props: { transport: Object.assign(fnTransport, { open, fetchHistory }) },
723
+ emit: createEmit()
724
+ })
725
+ await Promise.resolve()
726
+ expect(open).not.toHaveBeenCalled()
727
+ expect(fetchHistory).not.toHaveBeenCalled()
728
+ })
729
+
730
+ it('transport 更换或卸载时调用 disposer', async () => {
731
+ const adapter1 = createFakeAdapter()
732
+ const adapter2 = createFakeAdapter()
733
+ const props = reactive<AiChatProps>({ transport: createServerTransport(adapter1) })
734
+ const scope = effectScope()
735
+ scope.run(() => {
736
+ useChat({ props, emit: createEmit() })
737
+ })
738
+ await vi.waitFor(() => expect(adapter1.fetchHistory).toHaveBeenCalled())
739
+ expect(adapter1.unsubscribed()).toBe(false)
740
+
741
+ props.transport = createServerTransport(adapter2)
742
+ await vi.waitFor(() => expect(adapter2.fetchHistory).toHaveBeenCalled())
743
+ expect(adapter1.unsubscribed()).toBe(true)
744
+
745
+ scope.stop()
746
+ expect(adapter2.unsubscribed()).toBe(true)
747
+ })
748
+
749
+ it('send 只调 session.send,不带全量历史、不执行 execute、不注入内置工具', async () => {
750
+ const spy = vi.spyOn(toolsModule, 'createBuiltinTools')
751
+ const execute = vi.fn()
752
+ const { chat, adapter } = await setupSession({
753
+ tools: [{ name: 'add', description: '加法', parameters: {}, execute }]
754
+ })
755
+ spy.mockClear()
756
+
757
+ chat.send('hello')
758
+ expect(adapter.send).toHaveBeenCalledWith('hello', undefined)
759
+ expect(chat.messages.value).toHaveLength(0)
760
+
761
+ adapter.emit({ type: 'user/message', messageId: 'u1', seq: 1, content: 'hello' })
762
+ adapter.emit({ type: 'tool/call', callId: 'c1', name: 'add', arguments: '{"a":1}', seq: 2 })
763
+ expect(execute).not.toHaveBeenCalled()
764
+ expect(spy).not.toHaveBeenCalled()
765
+ spy.mockRestore()
766
+ })
767
+
768
+ it('abort 调 session.cancel;running 由事件与 onDisconnect 驱动', async () => {
769
+ const { chat, adapter } = await setupSession()
770
+ expect(chat.running.value).toBe(false)
771
+
772
+ adapter.emit({ type: 'running', running: true })
773
+ expect(chat.running.value).toBe(true)
774
+
775
+ chat.abort()
776
+ expect(adapter.cancel).toHaveBeenCalledTimes(1)
777
+
778
+ adapter.emit({ type: 'finish' })
779
+ expect(chat.running.value).toBe(false)
780
+
781
+ adapter.emit({ type: 'running', running: true })
782
+ adapter.emit({ type: 'error', code: 'x', message: '失败' })
783
+ expect(chat.running.value).toBe(false)
784
+
785
+ adapter.emit({ type: 'running', running: true })
786
+ adapter.disconnect()
787
+ await vi.waitFor(() => {
788
+ expect(chat.running.value).toBe(false)
789
+ })
790
+ })
791
+
792
+ it('队列只认 snapshot;enqueue / startQueued / removeQueued / regenerate 为 no-op', async () => {
793
+ const { chat, adapter } = await setupSession()
794
+ adapter.emit({ type: 'queue/snapshot', items: [{ id: 'q1', content: '排队' }] })
795
+ expect(chat.queue.value).toEqual([{ id: 'q1', content: '排队' }])
796
+
797
+ chat.enqueue('本地')
798
+ chat.startQueued('q1')
799
+ expect(chat.removeQueued('q1')).toBeUndefined()
800
+ expect(chat.queue.value).toEqual([{ id: 'q1', content: '排队' }])
801
+ expect(adapter.send).not.toHaveBeenCalled()
802
+
803
+ adapter.emit({ type: 'user/message', messageId: 'u1', seq: 1, content: '问' })
804
+ adapter.emit({ type: 'assistant/message', messageId: 'a1', seq: 2, content: '答' })
805
+ chat.regenerate()
806
+ expect(chat.messages.value).toHaveLength(2)
807
+ expect(adapter.send).not.toHaveBeenCalled()
808
+ })
809
+
810
+ it('clear 只清本地渲染面,不调 cancel', async () => {
811
+ const { chat, adapter } = await setupSession()
812
+ adapter.emit({ type: 'user/message', messageId: 'u1', seq: 1, content: 'hi' })
813
+ adapter.emit({
814
+ type: 'jobs/snapshot',
815
+ jobs: [{ id: 'j1', kind: 'bash', label: '跑', status: 'running' }]
816
+ })
817
+ adapter.emit({
818
+ type: 'projection',
819
+ key: 'tokenUsage',
820
+ seq: 2,
821
+ value: { promptTokens: 1, completionTokens: 2, totalTokens: 3 }
822
+ })
823
+ adapter.emit({ type: 'projection', key: 'title', seq: 3, value: '标题' })
824
+ adapter.emit({ type: 'projection', key: 'other', seq: 4, value: 1 })
825
+
826
+ expect(chat.jobs.value).toHaveLength(1)
827
+ expect(chat.title.value).toBe('标题')
828
+ expect(chat.projections.value.other).toBe(1)
829
+ expect(chat.tokenUsage.value?.totalTokens).toBe(3)
830
+
831
+ chat.clear()
832
+ expect(adapter.cancel).not.toHaveBeenCalled()
833
+ expect(chat.messages.value).toHaveLength(0)
834
+ expect(chat.queue.value).toHaveLength(0)
835
+ expect(chat.jobs.value).toHaveLength(0)
836
+ expect(chat.tokenUsage.value).toBeNull()
837
+ expect(chat.projections.value).toEqual({})
838
+ expect(chat.title.value).toBeNull()
839
+ })
840
+
841
+ it('approval 与 question 走 session.respond,不执行客户端工具', async () => {
842
+ const execute = vi.fn()
843
+ const { chat, adapter } = await setupSession({
844
+ tools: [{ name: 'del', description: '删', parameters: {}, execute }]
845
+ })
846
+
847
+ adapter.emit({ type: 'tool/call', callId: 'c1', name: 'del', arguments: '{}', seq: 1 })
848
+ adapter.emit({
849
+ type: 'approval/requested',
850
+ approvalId: 'ap1',
851
+ toolName: 'del',
852
+ callId: 'c1',
853
+ rpcId: 'rpc-call'
854
+ })
855
+ expect(chat.messages.value[0]?.toolCalls?.[0]?.status).toBe('awaiting-confirm')
856
+ chat.respondToolCall('c1', true)
857
+ await vi.waitFor(() => {
858
+ expect(adapter.respond).toHaveBeenCalledWith('rpc-call', true, undefined)
859
+ })
860
+ expect(execute).not.toHaveBeenCalled()
861
+
862
+ adapter.emit({
863
+ type: 'approval/requested',
864
+ approvalId: 'ap2',
865
+ toolName: 'del',
866
+ rpcId: 'rpc-banner'
867
+ })
868
+ expect(chat.pendingApprovals.value.some((a) => a.rpcId === 'rpc-banner' && !a.callId)).toBe(
869
+ true
870
+ )
871
+ chat.respondSession('rpc-banner', false)
872
+ await vi.waitFor(() => {
873
+ expect(adapter.respond).toHaveBeenCalledWith('rpc-banner', false, undefined)
874
+ })
875
+
876
+ adapter.emit({
877
+ type: 'question/requested',
878
+ rpcId: 'rpc-q',
879
+ questions: [{ question: '选一个', options: ['A', 'B'] }]
880
+ })
881
+ expect(chat.pendingQuestion.value?.rpcId).toBe('rpc-q')
882
+ chat.respondSession('rpc-q', true, [{ question: '选一个', answer: 'A' }])
883
+ await vi.waitFor(() => {
884
+ expect(adapter.respond).toHaveBeenCalledWith('rpc-q', true, [
885
+ { question: '选一个', answer: 'A' }
886
+ ])
887
+ })
888
+ })
889
+
890
+ it('已 open 时模型切换走 selectModel(providerId, modelId)', async () => {
891
+ const { chat, adapter } = await setupSession({
892
+ models: [
893
+ { id: 'm1', providerId: 'p1' },
894
+ { id: 'm2', providerId: 'p1' }
895
+ ],
896
+ model: 'm1'
897
+ })
898
+ expect(adapter.selectModel).not.toHaveBeenCalled()
899
+
900
+ chat.model.value = 'm2'
901
+ await nextTick()
902
+ expect(adapter.selectModel).toHaveBeenCalledWith('p1', 'm2')
903
+ })
904
+ })
@@ -0,0 +1,260 @@
1
+ import type { AskQuestionItem } from '../tools'
2
+ import type { ChatSessionEvent } from './session'
3
+ import type { ChatJob, ChatMessage, ChatQueuedMessage, ChatTokenUsage, ChatToolCall } from './types'
4
+
5
+ function eventSeq(event: ChatSessionEvent): number | undefined {
6
+ return 'seq' in event ? event.seq : undefined
7
+ }
8
+
9
+ export interface ChatPendingApproval {
10
+ approvalId: string
11
+ toolName: string
12
+ callId?: string
13
+ reason?: string
14
+ rpcId: string
15
+ }
16
+
17
+ /** 历史回放与实时 onEvent 共用的折叠状态 */
18
+ export interface ChatFoldState {
19
+ lastSeq: number | null
20
+ messages: ChatMessage[]
21
+ queue: ChatQueuedMessage[]
22
+ jobs: ChatJob[]
23
+ tokenUsage: ChatTokenUsage | null
24
+ projections: Record<string, unknown>
25
+ title: string | null
26
+ running: boolean
27
+ pendingApprovals: ChatPendingApproval[]
28
+ pendingQuestion: { questions: AskQuestionItem[]; rpcId: string } | null
29
+ error: { code: string; message: string } | null
30
+ }
31
+
32
+ export function createFoldState(): ChatFoldState {
33
+ return {
34
+ lastSeq: null,
35
+ messages: [],
36
+ queue: [],
37
+ jobs: [],
38
+ tokenUsage: null,
39
+ projections: {},
40
+ title: null,
41
+ running: false,
42
+ pendingApprovals: [],
43
+ pendingQuestion: null,
44
+ error: null
45
+ }
46
+ }
47
+
48
+ function mergeOptionalCount(a?: number, b?: number): number | undefined {
49
+ if (a == null && b == null) return undefined
50
+ return (a ?? 0) + (b ?? 0)
51
+ }
52
+
53
+ function addTokenUsage(a: ChatTokenUsage, b: ChatTokenUsage): ChatTokenUsage {
54
+ const usage: ChatTokenUsage = {
55
+ promptTokens: a.promptTokens + b.promptTokens,
56
+ completionTokens: a.completionTokens + b.completionTokens,
57
+ totalTokens: a.totalTokens + b.totalTokens
58
+ }
59
+ const cacheHitTokens = mergeOptionalCount(a.cacheHitTokens, b.cacheHitTokens)
60
+ if (cacheHitTokens != null) usage.cacheHitTokens = cacheHitTokens
61
+ const cacheMissTokens = mergeOptionalCount(a.cacheMissTokens, b.cacheMissTokens)
62
+ if (cacheMissTokens != null) usage.cacheMissTokens = cacheMissTokens
63
+ const cacheCreationTokens = mergeOptionalCount(a.cacheCreationTokens, b.cacheCreationTokens)
64
+ if (cacheCreationTokens != null) usage.cacheCreationTokens = cacheCreationTokens
65
+ return usage
66
+ }
67
+
68
+ function isTokenUsage(value: unknown): value is ChatTokenUsage {
69
+ if (value == null || typeof value !== 'object') return false
70
+ const usage = value as ChatTokenUsage
71
+ return (
72
+ typeof usage.promptTokens === 'number' &&
73
+ typeof usage.completionTokens === 'number' &&
74
+ typeof usage.totalTokens === 'number'
75
+ )
76
+ }
77
+
78
+ function replaceAt<T>(list: T[], idx: number, item: T): T[] {
79
+ const next = list.slice()
80
+ next[idx] = item
81
+ return next
82
+ }
83
+
84
+ function patchMessage(
85
+ messages: ChatMessage[],
86
+ id: string,
87
+ patch: Partial<ChatMessage>
88
+ ): ChatMessage[] {
89
+ const idx = messages.findIndex((m) => m.id === id)
90
+ if (idx < 0) {
91
+ return [...messages, { id, role: 'assistant', content: '', status: 'streaming', ...patch }]
92
+ }
93
+ return replaceAt(messages, idx, { ...messages[idx]!, ...patch })
94
+ }
95
+
96
+ function appendToolCall(messages: ChatMessage[], call: ChatToolCall): ChatMessage[] {
97
+ const idx = messages.findLastIndex((m) => m.role === 'assistant')
98
+ if (idx < 0) {
99
+ return [
100
+ ...messages,
101
+ {
102
+ id: `tool-host:${call.id}`,
103
+ role: 'assistant',
104
+ content: '',
105
+ status: 'streaming',
106
+ toolCalls: [call]
107
+ }
108
+ ]
109
+ }
110
+ const target = messages[idx]!
111
+ return replaceAt(messages, idx, { ...target, toolCalls: [...(target.toolCalls ?? []), call] })
112
+ }
113
+
114
+ function updateToolCall(
115
+ messages: ChatMessage[],
116
+ callId: string,
117
+ patch: Partial<ChatToolCall>
118
+ ): ChatMessage[] {
119
+ const idx = messages.findIndex((m) => m.toolCalls?.some((c) => c.id === callId))
120
+ if (idx < 0) return messages
121
+ const msg = messages[idx]!
122
+ const calls = (msg.toolCalls ?? []).slice()
123
+ const callIdx = calls.findIndex((c) => c.id === callId)
124
+ if (callIdx < 0) return messages
125
+ calls[callIdx] = { ...calls[callIdx]!, ...patch }
126
+ return replaceAt(messages, idx, { ...msg, toolCalls: calls })
127
+ }
128
+
129
+ /**
130
+ * 纯函数 fold:历史回放与实时 onEvent 走同一套。
131
+ * 后到的更小/重复 seq 不覆盖已应用事件。
132
+ */
133
+ export function foldSessionEvent(state: ChatFoldState, event: ChatSessionEvent): ChatFoldState {
134
+ const seq = eventSeq(event)
135
+ if (seq != null && state.lastSeq != null && seq <= state.lastSeq) return state
136
+
137
+ const next: ChatFoldState = {
138
+ ...state,
139
+ lastSeq: seq ?? state.lastSeq,
140
+ projections: { ...state.projections }
141
+ }
142
+
143
+ switch (event.type) {
144
+ case 'user/message': {
145
+ if (next.messages.some((m) => m.id === event.messageId)) break
146
+ const userMessage: ChatMessage = { id: event.messageId, role: 'user', content: event.content }
147
+ if (event.attachments) userMessage.attachments = event.attachments
148
+ next.messages = [...next.messages, userMessage]
149
+ break
150
+ }
151
+ case 'assistant/chunk': {
152
+ const current = next.messages.find((m) => m.id === event.messageId)
153
+ next.messages = patchMessage(next.messages, event.messageId, {
154
+ role: 'assistant',
155
+ status: 'streaming',
156
+ content: (current?.content ?? '') + event.delta,
157
+ reasoning: event.reasoningDelta
158
+ ? (current?.reasoning ?? '') + event.reasoningDelta
159
+ : current?.reasoning
160
+ })
161
+ break
162
+ }
163
+ case 'assistant/message': {
164
+ next.messages = patchMessage(next.messages, event.messageId, {
165
+ role: 'assistant',
166
+ status: 'done',
167
+ content: event.content,
168
+ reasoning: event.reasoning,
169
+ toolCalls: event.toolCalls ?? next.messages.find((m) => m.id === event.messageId)?.toolCalls
170
+ })
171
+ break
172
+ }
173
+ case 'tool/call': {
174
+ const call: ChatToolCall = {
175
+ id: event.callId,
176
+ name: event.name,
177
+ arguments: event.arguments,
178
+ status: 'pending'
179
+ }
180
+ if (event.view !== undefined) call.view = event.view
181
+ next.messages = appendToolCall(next.messages, call)
182
+ break
183
+ }
184
+ case 'tool/result': {
185
+ const patch: Partial<ChatToolCall> = { status: event.status }
186
+ if (event.result !== undefined) patch.result = event.result
187
+ if (event.error !== undefined) patch.error = event.error
188
+ if (event.view !== undefined) patch.view = event.view
189
+ next.messages = updateToolCall(next.messages, event.callId, patch)
190
+ break
191
+ }
192
+ case 'queue/snapshot': {
193
+ next.queue = event.items.slice()
194
+ break
195
+ }
196
+ case 'jobs/snapshot': {
197
+ next.jobs = event.jobs.slice()
198
+ break
199
+ }
200
+ case 'projection': {
201
+ if (event.key === 'tokenUsage' && isTokenUsage(event.value)) {
202
+ next.tokenUsage = next.tokenUsage
203
+ ? addTokenUsage(next.tokenUsage, event.value)
204
+ : event.value
205
+ } else if (event.key === 'title' && typeof event.value === 'string') {
206
+ next.title = event.value
207
+ } else {
208
+ next.projections[event.key] = event.value
209
+ }
210
+ break
211
+ }
212
+ case 'running': {
213
+ next.running = event.running
214
+ break
215
+ }
216
+ case 'finish': {
217
+ next.running = false
218
+ break
219
+ }
220
+ case 'error': {
221
+ next.running = false
222
+ next.error = { code: event.code, message: event.message }
223
+ break
224
+ }
225
+ case 'approval/requested': {
226
+ if (event.callId) {
227
+ next.messages = updateToolCall(next.messages, event.callId, { status: 'awaiting-confirm' })
228
+ }
229
+ next.pendingApprovals = [
230
+ ...next.pendingApprovals,
231
+ {
232
+ approvalId: event.approvalId,
233
+ toolName: event.toolName,
234
+ callId: event.callId,
235
+ reason: event.reason,
236
+ rpcId: event.rpcId
237
+ }
238
+ ]
239
+ break
240
+ }
241
+ case 'approval/resolved': {
242
+ const hit = next.pendingApprovals.find((a) => a.approvalId === event.approvalId)
243
+ next.pendingApprovals = next.pendingApprovals.filter((a) => a.approvalId !== event.approvalId)
244
+ if (hit?.callId && event.outcome === 'rejected') {
245
+ next.messages = updateToolCall(next.messages, hit.callId, { status: 'rejected' })
246
+ }
247
+ break
248
+ }
249
+ case 'question/requested': {
250
+ next.pendingQuestion = { questions: event.questions, rpcId: event.rpcId }
251
+ break
252
+ }
253
+ case 'question/resolved': {
254
+ if (next.pendingQuestion?.rpcId === event.questionRpcId) next.pendingQuestion = null
255
+ break
256
+ }
257
+ }
258
+
259
+ return next
260
+ }
package/src/chat/index.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export * from './types'
2
2
  export * from './use-chat'
3
3
  export * from './transports'
4
+ export * from './session'
5
+ export * from './fold'