Skip to content

Commit 8db1dc3

Browse files
committed
store chatid in tab, reload from memory. new backend functions to convert native messages to UIMessage
1 parent 00faf4a commit 8db1dc3

16 files changed

Lines changed: 396 additions & 17 deletions

File tree

cmd/generatego/main-generatego.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ func GenerateWshClient() error {
3232
"github.com/wavetermdev/waveterm/pkg/wps",
3333
"github.com/wavetermdev/waveterm/pkg/vdom",
3434
"github.com/wavetermdev/waveterm/pkg/util/iochan/iochantypes",
35+
"github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes",
3536
})
3637
wshDeclMap := wshrpc.GenerateWshCommandDeclMap()
3738
for _, key := range utilfn.GetOrderedMapKeys(wshDeclMap) {

frontend/app/aipanel/aipanel.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ interface AIPanelProps {
2828
const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => {
2929
const [input, setInput] = useState("");
3030
const [isDragOver, setIsDragOver] = useState(false);
31+
const [isLoadingChat, setIsLoadingChat] = useState(true);
3132
const model = WaveAIModel.getInstance();
3233
const errorMessage = jotai.useAtomValue(model.errorMessage);
3334
const realMessageRef = useRef<AIMessage>(null);
@@ -92,9 +93,18 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => {
9293
model.registerInputRef(inputRef);
9394
}, [model]);
9495

96+
useEffect(() => {
97+
const loadMessages = async () => {
98+
const messages = await model.loadChat();
99+
setMessages(messages as any);
100+
setIsLoadingChat(false);
101+
};
102+
loadMessages();
103+
}, [model, setMessages]);
104+
95105
const handleSubmit = async (e: React.FormEvent) => {
96106
e.preventDefault();
97-
if (!input.trim() || status !== "ready") return;
107+
if (!input.trim() || status !== "ready" || isLoadingChat) return;
98108

99109
if (input.trim() === "/clear" || input.trim() === "/new") {
100110
clearChat();
@@ -306,7 +316,7 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => {
306316
<TelemetryRequiredMessage />
307317
) : (
308318
<>
309-
<AIPanelMessages messages={messages} status={status} />
319+
<AIPanelMessages messages={messages} status={status} isLoadingChat={isLoadingChat} />
310320
{errorMessage && (
311321
<div className="px-4 py-2 text-red-400 bg-red-900/20 border-l-4 border-red-500 mx-2 mb-2 relative">
312322
<button

frontend/app/aipanel/aipanelmessages.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ AIWelcomeMessage.displayName = "AIWelcomeMessage";
2121
interface AIPanelMessagesProps {
2222
messages: any[];
2323
status: string;
24+
isLoadingChat?: boolean;
2425
}
2526

26-
export const AIPanelMessages = memo(({ messages, status }: AIPanelMessagesProps) => {
27+
export const AIPanelMessages = memo(({ messages, status, isLoadingChat }: AIPanelMessagesProps) => {
2728
const isPanelOpen = useAtomValue(workspaceLayoutModel.panelVisibleAtom);
2829
const messagesEndRef = useRef<HTMLDivElement>(null);
2930
const messagesContainerRef = useRef<HTMLDivElement>(null);
@@ -48,7 +49,7 @@ export const AIPanelMessages = memo(({ messages, status }: AIPanelMessagesProps)
4849
if (messages.length == 0) {
4950
return (
5051
<div ref={messagesContainerRef} className="flex-1 overflow-y-auto p-2 space-y-4">
51-
<AIWelcomeMessage />
52+
{!isLoadingChat && <AIWelcomeMessage />}
5253
</div>
5354
);
5455
}

frontend/app/aipanel/waveai-model.tsx

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,25 @@ export class WaveAIModel {
2828

2929
widgetAccess: jotai.PrimitiveAtom<boolean> = jotai.atom(true);
3030
droppedFiles: jotai.PrimitiveAtom<DroppedFile[]> = jotai.atom([]);
31-
chatId: jotai.PrimitiveAtom<string> = jotai.atom(crypto.randomUUID());
31+
chatId!: jotai.PrimitiveAtom<string>;
3232
errorMessage: jotai.PrimitiveAtom<string> = jotai.atom(null) as jotai.PrimitiveAtom<string>;
3333
modelAtom!: jotai.Atom<string>;
3434

3535
private constructor() {
36+
const tabId = globalStore.get(atoms.staticTabId);
37+
const chatIdMetaAtom = getTabMetaKeyAtom(tabId, "waveai:chatid");
38+
let chatIdValue = globalStore.get(chatIdMetaAtom);
39+
40+
if (chatIdValue == null) {
41+
chatIdValue = crypto.randomUUID();
42+
RpcApi.SetMetaCommand(TabRpcClient, {
43+
oref: WOS.makeORef("tab", tabId),
44+
meta: { "waveai:chatid": chatIdValue },
45+
});
46+
}
47+
48+
this.chatId = jotai.atom(chatIdValue);
49+
3650
this.modelAtom = jotai.atom((get) => {
3751
const tabId = get(atoms.staticTabId);
3852
const modelMetaAtom = getTabMetaKeyAtom(tabId, "waveai:model");
@@ -98,7 +112,14 @@ export class WaveAIModel {
98112

99113
clearChat() {
100114
this.clearFiles();
101-
globalStore.set(this.chatId, crypto.randomUUID());
115+
const newChatId = crypto.randomUUID();
116+
globalStore.set(this.chatId, newChatId);
117+
118+
const tabId = globalStore.get(atoms.staticTabId);
119+
RpcApi.SetMetaCommand(TabRpcClient, {
120+
oref: WOS.makeORef("tab", tabId),
121+
meta: { "waveai:chatid": newChatId },
122+
});
102123
}
103124

104125
setError(message: string) {
@@ -129,7 +150,26 @@ export class WaveAIModel {
129150
meta: { "waveai:model": model },
130151
});
131152
}
132-
}
133153

134-
// Export singleton instance for easy access
135-
export const waveAIModel = WaveAIModel.getInstance();
154+
async loadChat(): Promise<UIMessage[]> {
155+
const chatId = globalStore.get(this.chatId);
156+
try {
157+
const chatData = await RpcApi.GetWaveAIChatCommand(TabRpcClient, { chatid: chatId });
158+
return chatData?.messages ?? [];
159+
} catch (error) {
160+
console.error("Failed to load chat:", error);
161+
this.setError("Failed to load chat. Starting new chat...");
162+
163+
const newChatId = crypto.randomUUID();
164+
globalStore.set(this.chatId, newChatId);
165+
166+
const tabId = globalStore.get(atoms.staticTabId);
167+
RpcApi.SetMetaCommand(TabRpcClient, {
168+
oref: WOS.makeORef("tab", tabId),
169+
meta: { "waveai:chatid": newChatId },
170+
});
171+
172+
return [];
173+
}
174+
}
175+
}

frontend/app/store/keymodel.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2025, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { waveAIModel } from "@/app/aipanel/waveai-model";
4+
import { WaveAIModel } from "@/app/aipanel/waveai-model";
55
import {
66
atoms,
77
createBlock,
@@ -148,7 +148,7 @@ function switchBlockInDirection(tabId: string, direction: NavigateDirection) {
148148
const inWaveAI = globalStore.get(atoms.waveAIFocusedAtom);
149149
const navResult = layoutModel.switchNodeFocusInDirection(direction, inWaveAI);
150150
if (navResult.atLeft) {
151-
waveAIModel.focusInput();
151+
WaveAIModel.getInstance().focusInput();
152152
return;
153153
}
154154
setTimeout(() => {
@@ -498,11 +498,11 @@ function registerGlobalKeys() {
498498
});
499499
}
500500
globalKeyMap.set("Ctrl:Shift:c{Digit0}", () => {
501-
waveAIModel.focusInput();
501+
WaveAIModel.getInstance().focusInput();
502502
return true;
503503
});
504504
globalKeyMap.set("Ctrl:Shift:c{Numpad0}", () => {
505-
waveAIModel.focusInput();
505+
WaveAIModel.getInstance().focusInput();
506506
return true;
507507
});
508508
function activateSearch(event: WaveKeyboardEvent): boolean {
@@ -539,7 +539,7 @@ function registerGlobalKeys() {
539539
globalKeyMap.set("Cmd:Shift:a", () => {
540540
const currentVisible = workspaceLayoutModel.getAIPanelVisible();
541541
if (!currentVisible) {
542-
waveAIModel.focusInput();
542+
WaveAIModel.getInstance().focusInput();
543543
} else {
544544
workspaceLayoutModel.setAIPanelVisible(false);
545545
globalRefocus();

frontend/app/store/wshclientapi.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,11 @@ class RpcApiType {
282282
return client.wshRpcCall("getvar", data, opts);
283283
}
284284

285+
// command "getwaveaichat" [call]
286+
GetWaveAIChatCommand(client: WshClient, data: CommandGetWaveAIChatData, opts?: RpcOpts): Promise<UIChat> {
287+
return client.wshRpcCall("getwaveaichat", data, opts);
288+
}
289+
285290
// command "message" [call]
286291
MessageCommand(client: WshClient, data: CommandMessageData, opts?: RpcOpts): Promise<void> {
287292
return client.wshRpcCall("message", data, opts);

frontend/types/gotypes.d.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,11 @@ declare global {
209209
oref: ORef;
210210
};
211211

212+
// wshrpc.CommandGetWaveAIChatData
213+
type CommandGetWaveAIChatData = {
214+
chatid: string;
215+
};
216+
212217
// wshrpc.CommandMessageData
213218
type CommandMessageData = {
214219
oref: ORef;
@@ -591,6 +596,7 @@ declare global {
591596
"waveai:panelopen"?: boolean;
592597
"waveai:panelwidth"?: number;
593598
"waveai:model"?: string;
599+
"waveai:chatid"?: string;
594600
"term:*"?: boolean;
595601
"term:fontsize"?: number;
596602
"term:fontfamily"?: string;
@@ -954,12 +960,49 @@ declare global {
954960
values: {[key: string]: number};
955961
};
956962

963+
// uctypes.UIChat
964+
type UIChat = {
965+
chatid: string;
966+
apitype: string;
967+
model: string;
968+
apiversion: string;
969+
messages: UIMessage[];
970+
};
971+
957972
// waveobj.UIContext
958973
type UIContext = {
959974
windowid: string;
960975
activetabid: string;
961976
};
962977

978+
// uctypes.UIMessage
979+
type UIMessage = {
980+
id: string;
981+
role: string;
982+
metadata?: any;
983+
parts?: UIMessagePart[];
984+
};
985+
986+
// uctypes.UIMessagePart
987+
type UIMessagePart = {
988+
type: string;
989+
text?: string;
990+
state?: string;
991+
toolCallId?: string;
992+
input?: any;
993+
output?: any;
994+
errorText?: string;
995+
providerExecuted?: boolean;
996+
sourceId?: string;
997+
url?: string;
998+
title?: string;
999+
filename?: string;
1000+
mediaType?: string;
1001+
id?: string;
1002+
data?: any;
1003+
providerMetadata?: {[key: string]: any};
1004+
};
1005+
9631006
// userinput.UserInputRequest
9641007
type UserInputRequest = {
9651008
requestid: string;

pkg/aiusechat/anthropic/anthropic-convertmessage.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,7 @@ func convertFileAIMessagePart(part uctypes.AIMessagePart) (*anthropicMessageCont
617617
}
618618

619619
// ConvertToUIMessage converts an anthropicChatMessage to a UIMessage
620-
func (m *anthropicChatMessage) ConvertToUIMessage() uctypes.UIMessage {
620+
func (m *anthropicChatMessage) ConvertToUIMessage() *uctypes.UIMessage {
621621
var parts []uctypes.UIMessagePart
622622

623623
// Iterate over all content blocks
@@ -671,7 +671,11 @@ func (m *anthropicChatMessage) ConvertToUIMessage() uctypes.UIMessage {
671671
}
672672
}
673673

674-
return uctypes.UIMessage{
674+
if len(parts) == 0 {
675+
return nil
676+
}
677+
678+
return &uctypes.UIMessage{
675679
ID: m.MessageId,
676680
Role: m.Role,
677681
Parts: parts,
@@ -760,3 +764,32 @@ func ConvertToolResultsToAnthropicChatMessage(toolResults []uctypes.AIToolResult
760764
Content: contentBlocks,
761765
}, nil
762766
}
767+
768+
// ConvertAIChatToUIChat converts an AIChat to a UIChat for Anthropic
769+
func ConvertAIChatToUIChat(aiChat uctypes.AIChat) (*uctypes.UIChat, error) {
770+
if aiChat.APIType != "anthropic" {
771+
return nil, fmt.Errorf("APIType must be 'anthropic', got '%s'", aiChat.APIType)
772+
}
773+
774+
uiMessages := make([]uctypes.UIMessage, 0, len(aiChat.NativeMessages))
775+
776+
for i, nativeMsg := range aiChat.NativeMessages {
777+
anthropicMsg, ok := nativeMsg.(*anthropicChatMessage)
778+
if !ok {
779+
return nil, fmt.Errorf("message %d: expected *anthropicChatMessage, got %T", i, nativeMsg)
780+
}
781+
782+
uiMsg := anthropicMsg.ConvertToUIMessage()
783+
if uiMsg != nil {
784+
uiMessages = append(uiMessages, *uiMsg)
785+
}
786+
}
787+
788+
return &uctypes.UIChat{
789+
ChatId: aiChat.ChatId,
790+
APIType: aiChat.APIType,
791+
Model: aiChat.Model,
792+
APIVersion: aiChat.APIVersion,
793+
Messages: uiMessages,
794+
}, nil
795+
}

0 commit comments

Comments
 (0)