refactor(apps): adopt conversation-first IPC and API surfaces
This commit is contained in:
parent
5af7aa7840
commit
218e6da544
19 changed files with 202 additions and 349 deletions
22
apps/desktop/src/main/electron-env.d.ts
vendored
22
apps/desktop/src/main/electron-env.d.ts
vendored
|
|
@ -100,8 +100,7 @@ interface ProfileData {
|
|||
|
||||
interface LocalChatEvent {
|
||||
agentId: string
|
||||
conversationId?: string
|
||||
sessionId?: string
|
||||
conversationId: string
|
||||
streamId?: string
|
||||
type?: 'error'
|
||||
content?: string
|
||||
|
|
@ -119,8 +118,7 @@ interface LocalChatEvent {
|
|||
interface LocalChatApproval {
|
||||
approvalId: string
|
||||
agentId: string
|
||||
conversationId?: string
|
||||
sessionId?: string
|
||||
conversationId: string
|
||||
command: string
|
||||
cwd?: string
|
||||
riskLevel: 'safe' | 'needs-review' | 'dangerous'
|
||||
|
|
@ -183,15 +181,11 @@ interface ElectronAPI {
|
|||
getAgentInfo: () => Promise<AgentInfo | null>
|
||||
info: () => Promise<unknown>
|
||||
reconnect: (url: string) => Promise<unknown>
|
||||
listAgents: () => Promise<unknown>
|
||||
listConversations: () => Promise<unknown>
|
||||
createAgent: (id?: string) => Promise<unknown>
|
||||
createConversation: (id?: string) => Promise<unknown>
|
||||
getAgent: (id: string) => Promise<unknown>
|
||||
getConversation: (id: string) => Promise<unknown>
|
||||
closeAgent: (id: string) => Promise<unknown>
|
||||
closeConversation: (id: string) => Promise<unknown>
|
||||
sendMessage: (agentId: string, content: string, conversationId?: string) => Promise<unknown>
|
||||
sendMessage: (agentId: string, content: string, conversationId: string) => Promise<unknown>
|
||||
registerToken: (token: string, agentId: string, conversationId: string, expiresAt: number) => Promise<unknown>
|
||||
onDeviceConfirmRequest: (callback: (deviceId: string, agentId: string, conversationId: string, meta?: DeviceMeta) => void) => void
|
||||
offDeviceConfirmRequest: () => void
|
||||
|
|
@ -259,11 +253,11 @@ interface ElectronAPI {
|
|||
wake: (reason?: string) => Promise<{ ok: boolean; result?: unknown; error?: string }>
|
||||
}
|
||||
localChat: {
|
||||
subscribe: (agentId: string) => Promise<{ ok?: boolean; error?: string; alreadySubscribed?: boolean }>
|
||||
unsubscribe: (agentId: string) => Promise<{ ok: boolean }>
|
||||
getHistory: (agentId: string, options?: { offset?: number; limit?: number; conversationId?: string }) => Promise<{ messages: unknown[]; total: number; offset: number; limit: number; contextWindowTokens?: number }>
|
||||
send: (agentId: string, content: string, conversationId?: string) => Promise<{ ok?: boolean; error?: string }>
|
||||
abort: (agentId: string, conversationId?: string) => Promise<{ ok?: boolean; error?: string }>
|
||||
subscribe: (conversationId: string) => Promise<{ ok?: boolean; error?: string; alreadySubscribed?: boolean }>
|
||||
unsubscribe: (conversationId: string) => Promise<{ ok: boolean }>
|
||||
getHistory: (conversationId: string, options?: { offset?: number; limit?: number }) => Promise<{ messages: unknown[]; total: number; offset: number; limit: number; contextWindowTokens?: number }>
|
||||
send: (conversationId: string, content: string) => Promise<{ ok?: boolean; error?: string }>
|
||||
abort: (conversationId: string) => Promise<{ ok?: boolean; error?: string }>
|
||||
resolveExecApproval: (approvalId: string, decision: string) => Promise<{ ok: boolean }>
|
||||
onEvent: (callback: (event: LocalChatEvent) => void) => void
|
||||
offEvent: () => void
|
||||
|
|
|
|||
|
|
@ -47,10 +47,10 @@ function getDefaultAgent() {
|
|||
const hub = getCurrentHub()
|
||||
if (!hub) return null
|
||||
|
||||
const agentIds = hub.listAgents()
|
||||
if (agentIds.length === 0) return null
|
||||
const conversationIds = hub.listConversations()
|
||||
if (conversationIds.length === 0) return null
|
||||
|
||||
return hub.getAgent(agentIds[0]) ?? null
|
||||
return hub.getConversation(conversationIds[0]) ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import type { ConnectionState } from '@multica/sdk'
|
|||
|
||||
// Singleton Hub instance
|
||||
let hub: Hub | null = null
|
||||
let defaultAgentId: string | null = null
|
||||
let defaultConversationId: string | null = null
|
||||
let mainWindowRef: BrowserWindow | null = null
|
||||
|
||||
// Track which agents have active IPC subscriptions (for local direct chat)
|
||||
|
|
@ -35,7 +35,7 @@ function safeLog(...args: unknown[]): void {
|
|||
|
||||
/**
|
||||
* Initialize Hub on app startup.
|
||||
* Creates Hub and a default Agent automatically.
|
||||
* Creates Hub and a default conversation automatically.
|
||||
*/
|
||||
export async function initializeHub(): Promise<void> {
|
||||
if (hub) {
|
||||
|
|
@ -47,16 +47,16 @@ export async function initializeHub(): Promise<void> {
|
|||
|
||||
hub = new Hub(gatewayUrl)
|
||||
|
||||
// Create default agent if none exists
|
||||
const agents = hub.listAgents()
|
||||
if (agents.length === 0) {
|
||||
safeLog('[Desktop] Creating default agent...')
|
||||
const agent = hub.createAgent()
|
||||
defaultAgentId = agent.sessionId
|
||||
safeLog(`[Desktop] Default agent created: ${defaultAgentId}`)
|
||||
// Create default conversation if none exists
|
||||
const conversations = hub.listConversations()
|
||||
if (conversations.length === 0) {
|
||||
safeLog('[Desktop] Creating default conversation...')
|
||||
const conversation = hub.createConversation()
|
||||
defaultConversationId = conversation.sessionId
|
||||
safeLog(`[Desktop] Default conversation created: ${defaultConversationId}`)
|
||||
} else {
|
||||
defaultAgentId = agents[0]
|
||||
safeLog(`[Desktop] Using existing agent: ${defaultAgentId}`)
|
||||
defaultConversationId = conversations[0]
|
||||
safeLog(`[Desktop] Using existing conversation: ${defaultConversationId}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,8 +75,8 @@ function getHub(): Hub {
|
|||
* Get the default agent.
|
||||
*/
|
||||
export function getDefaultAgent(): AsyncAgent | null {
|
||||
if (!hub || !defaultAgentId) return null
|
||||
return hub.getAgent(defaultAgentId) ?? null
|
||||
if (!hub || !defaultConversationId) return null
|
||||
return hub.getConversation(defaultConversationId) ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -107,14 +107,10 @@ export function registerHubIpcHandlers(): void {
|
|||
ipcMain.handle('hub:init', async () => {
|
||||
await initializeHub()
|
||||
const h = getHub()
|
||||
const defaultConversationId = defaultAgentId
|
||||
? (h.getAgentMainConversationId(defaultAgentId) ?? defaultAgentId)
|
||||
: null
|
||||
return {
|
||||
hubId: h.hubId,
|
||||
url: h.url,
|
||||
connectionState: h.connectionState,
|
||||
defaultAgentId,
|
||||
defaultConversationId,
|
||||
}
|
||||
})
|
||||
|
|
@ -128,7 +124,7 @@ export function registerHubIpcHandlers(): void {
|
|||
hubId: h.hubId,
|
||||
url: h.url,
|
||||
connectionState: h.connectionState,
|
||||
agentCount: h.listAgents().length,
|
||||
agentCount: h.listConversations().length,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -142,12 +138,12 @@ export function registerHubIpcHandlers(): void {
|
|||
return {
|
||||
hubId: h.hubId,
|
||||
status: h.connectionState === 'connected' ? 'ready' : h.connectionState,
|
||||
agentCount: h.listAgents().length,
|
||||
agentCount: h.listConversations().length,
|
||||
gatewayConnected: h.connectionState === 'connected',
|
||||
gatewayUrl: h.url,
|
||||
defaultAgent: agent
|
||||
? {
|
||||
agentId: defaultAgentId ?? agent.sessionId,
|
||||
agentId: defaultConversationId ?? agent.sessionId,
|
||||
status: agent.closed ? 'closed' : 'idle',
|
||||
}
|
||||
: null,
|
||||
|
|
@ -163,7 +159,7 @@ export function registerHubIpcHandlers(): void {
|
|||
return null
|
||||
}
|
||||
return {
|
||||
agentId: defaultAgentId ?? agent.sessionId,
|
||||
agentId: defaultConversationId ?? agent.sessionId,
|
||||
status: agent.closed ? 'closed' : 'idle',
|
||||
}
|
||||
})
|
||||
|
|
@ -178,22 +174,7 @@ export function registerHubIpcHandlers(): void {
|
|||
})
|
||||
|
||||
/**
|
||||
* List all agents.
|
||||
*/
|
||||
ipcMain.handle('hub:listAgents', async (): Promise<AgentInfo[]> => {
|
||||
const h = getHub()
|
||||
const agentIds = h.listAgents()
|
||||
return agentIds.map((id) => {
|
||||
const agent = h.getAgent(id)
|
||||
return {
|
||||
id,
|
||||
closed: agent?.closed ?? true,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* List all conversations (alias of listAgents for clearer semantics).
|
||||
* List all conversations.
|
||||
*/
|
||||
ipcMain.handle('hub:listConversations', async (): Promise<AgentInfo[]> => {
|
||||
const h = getHub()
|
||||
|
|
@ -208,19 +189,7 @@ export function registerHubIpcHandlers(): void {
|
|||
})
|
||||
|
||||
/**
|
||||
* Create a new agent.
|
||||
*/
|
||||
ipcMain.handle('hub:createAgent', async (_event, id?: string) => {
|
||||
const h = getHub()
|
||||
const agent = h.createAgent(id)
|
||||
return {
|
||||
id: agent.sessionId,
|
||||
closed: agent.closed,
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Create a new conversation (alias of createAgent).
|
||||
* Create a new conversation.
|
||||
*/
|
||||
ipcMain.handle('hub:createConversation', async (_event, id?: string) => {
|
||||
const h = getHub()
|
||||
|
|
@ -232,22 +201,7 @@ export function registerHubIpcHandlers(): void {
|
|||
})
|
||||
|
||||
/**
|
||||
* Get a specific agent.
|
||||
*/
|
||||
ipcMain.handle('hub:getAgent', async (_event, id: string) => {
|
||||
const h = getHub()
|
||||
const agent = h.getAgent(id)
|
||||
if (!agent) {
|
||||
return { error: `Agent not found: ${id}` }
|
||||
}
|
||||
return {
|
||||
id: agent.sessionId,
|
||||
closed: agent.closed,
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Get a specific conversation (alias of getAgent).
|
||||
* Get a specific conversation.
|
||||
*/
|
||||
ipcMain.handle('hub:getConversation', async (_event, id: string) => {
|
||||
const h = getHub()
|
||||
|
|
@ -262,16 +216,7 @@ export function registerHubIpcHandlers(): void {
|
|||
})
|
||||
|
||||
/**
|
||||
* Close/delete an agent.
|
||||
*/
|
||||
ipcMain.handle('hub:closeAgent', async (_event, id: string) => {
|
||||
const h = getHub()
|
||||
const result = h.closeAgent(id)
|
||||
return { ok: result }
|
||||
})
|
||||
|
||||
/**
|
||||
* Close/delete a conversation (alias of closeAgent).
|
||||
* Close/delete a conversation.
|
||||
*/
|
||||
ipcMain.handle('hub:closeConversation', async (_event, id: string) => {
|
||||
const h = getHub()
|
||||
|
|
@ -283,10 +228,10 @@ export function registerHubIpcHandlers(): void {
|
|||
* Send a message to an agent (for remote clients via Gateway).
|
||||
* Note: For local direct chat, use 'localChat:send' instead.
|
||||
*/
|
||||
ipcMain.handle('hub:sendMessage', async (_event, agentId: string, content: string, conversationId?: string) => {
|
||||
ipcMain.handle('hub:sendMessage', async (_event, agentId: string, content: string, conversationId: string) => {
|
||||
const h = getHub()
|
||||
const resolvedConversationId = conversationId ?? agentId
|
||||
const agent = h.getAgent(resolvedConversationId)
|
||||
const resolvedConversationId = conversationId
|
||||
const agent = h.getConversation(resolvedConversationId)
|
||||
if (!agent) {
|
||||
return { error: `Conversation not found: ${resolvedConversationId}` }
|
||||
}
|
||||
|
|
@ -302,15 +247,14 @@ export function registerHubIpcHandlers(): void {
|
|||
* Subscribe to local agent events (for direct IPC chat without Gateway).
|
||||
* Uses agent.subscribe() which supports multiple subscribers.
|
||||
*/
|
||||
ipcMain.handle('localChat:subscribe', async (_event, agentId: string) => {
|
||||
ipcMain.handle('localChat:subscribe', async (_event, conversationId: string) => {
|
||||
const h = getHub()
|
||||
const conversationId = agentId
|
||||
const conversation = h.getConversation(conversationId)
|
||||
if (!conversation) {
|
||||
return { error: `Agent not found: ${conversationId}` }
|
||||
return { error: `Conversation not found: ${conversationId}` }
|
||||
}
|
||||
if (conversation.closed) {
|
||||
return { error: `Agent is closed: ${conversationId}` }
|
||||
return { error: `Conversation is closed: ${conversationId}` }
|
||||
}
|
||||
const logicalAgentId = h.getConversationAgentId(conversationId) ?? conversationId
|
||||
|
||||
|
|
@ -336,7 +280,6 @@ export function registerHubIpcHandlers(): void {
|
|||
mainWindowRef.webContents.send('localChat:event', {
|
||||
agentId: logicalAgentId,
|
||||
conversationId,
|
||||
streamId: null,
|
||||
event,
|
||||
})
|
||||
return
|
||||
|
|
@ -392,14 +335,14 @@ export function registerHubIpcHandlers(): void {
|
|||
/**
|
||||
* Unsubscribe from local agent events.
|
||||
*/
|
||||
ipcMain.handle('localChat:unsubscribe', async (_event, agentId: string) => {
|
||||
const unsubscribe = ipcAgentSubscriptions.get(agentId)
|
||||
ipcMain.handle('localChat:unsubscribe', async (_event, conversationId: string) => {
|
||||
const unsubscribe = ipcAgentSubscriptions.get(conversationId)
|
||||
if (unsubscribe) {
|
||||
unsubscribe()
|
||||
}
|
||||
ipcAgentSubscriptions.delete(agentId)
|
||||
getHub().removeLocalApprovalHandler(agentId)
|
||||
safeLog(`[IPC] Local chat unsubscribed from agent: ${agentId}`)
|
||||
ipcAgentSubscriptions.delete(conversationId)
|
||||
getHub().removeLocalApprovalHandler(conversationId)
|
||||
safeLog(`[IPC] Local chat unsubscribed from conversation: ${conversationId}`)
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
|
|
@ -413,12 +356,11 @@ export function registerHubIpcHandlers(): void {
|
|||
*/
|
||||
ipcMain.handle('localChat:getHistory', async (
|
||||
_event,
|
||||
agentId: string,
|
||||
options?: { offset?: number; limit?: number; conversationId?: string },
|
||||
conversationId: string,
|
||||
options?: { offset?: number; limit?: number },
|
||||
) => {
|
||||
const h = getHub()
|
||||
const conversationId = options?.conversationId ?? agentId
|
||||
const agent = h.getAgent(conversationId)
|
||||
const agent = h.getConversation(conversationId)
|
||||
if (!agent) {
|
||||
return { messages: [], total: 0, offset: 0, limit: 0, contextWindowTokens: undefined }
|
||||
}
|
||||
|
|
@ -442,10 +384,10 @@ export function registerHubIpcHandlers(): void {
|
|||
* Send a message via local direct IPC (no Gateway).
|
||||
* Events will be pushed to renderer via 'localChat:event' channel.
|
||||
*/
|
||||
ipcMain.handle('localChat:send', async (_event, agentId: string, content: string, conversationId?: string) => {
|
||||
ipcMain.handle('localChat:send', async (_event, conversationId: string, content: string) => {
|
||||
const h = getHub()
|
||||
const resolvedConversationId = conversationId ?? agentId
|
||||
const agent = h.getAgent(resolvedConversationId)
|
||||
const resolvedConversationId = conversationId
|
||||
const agent = h.getConversation(resolvedConversationId)
|
||||
if (!agent) {
|
||||
return { error: `Conversation not found: ${resolvedConversationId}` }
|
||||
}
|
||||
|
|
@ -455,14 +397,14 @@ export function registerHubIpcHandlers(): void {
|
|||
|
||||
// Must be subscribed first to receive events
|
||||
if (!ipcAgentSubscriptions.has(resolvedConversationId)) {
|
||||
return { error: 'Not subscribed to agent events. Call subscribe first.' }
|
||||
return { error: 'Not subscribed to conversation events. Call subscribe first.' }
|
||||
}
|
||||
|
||||
h.channelManager.clearLastRoute()
|
||||
const source = { type: 'local' as const }
|
||||
// Broadcast as local source (for consistency, though UI already knows)
|
||||
h.broadcastInbound({
|
||||
agentId: h.getConversationAgentId(resolvedConversationId) ?? agentId,
|
||||
agentId: h.getConversationAgentId(resolvedConversationId) ?? resolvedConversationId,
|
||||
conversationId: resolvedConversationId,
|
||||
content,
|
||||
source,
|
||||
|
|
@ -476,10 +418,10 @@ export function registerHubIpcHandlers(): void {
|
|||
/**
|
||||
* Abort the current agent run for local chat.
|
||||
*/
|
||||
ipcMain.handle('localChat:abort', async (_event, agentId: string, conversationId?: string) => {
|
||||
ipcMain.handle('localChat:abort', async (_event, conversationId: string) => {
|
||||
const h = getHub()
|
||||
const resolvedConversationId = conversationId ?? agentId
|
||||
const agent = h.getAgent(resolvedConversationId)
|
||||
const resolvedConversationId = conversationId
|
||||
const agent = h.getConversation(resolvedConversationId)
|
||||
if (!agent) {
|
||||
return { error: `Conversation not found: ${resolvedConversationId}` }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ function getDefaultAgent() {
|
|||
const hub = getCurrentHub()
|
||||
if (!hub) return null
|
||||
|
||||
const agentIds = hub.listAgents()
|
||||
if (agentIds.length === 0) return null
|
||||
const conversationIds = hub.listConversations()
|
||||
if (conversationIds.length === 0) return null
|
||||
|
||||
return hub.getAgent(agentIds[0]) ?? null
|
||||
return hub.getConversation(conversationIds[0]) ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -53,10 +53,10 @@ function getDefaultAgent() {
|
|||
const hub = getCurrentHub()
|
||||
if (!hub) return null
|
||||
|
||||
const agentIds = hub.listAgents()
|
||||
if (agentIds.length === 0) return null
|
||||
const conversationIds = hub.listConversations()
|
||||
if (conversationIds.length === 0) return null
|
||||
|
||||
return hub.getAgent(agentIds[0]) ?? null
|
||||
return hub.getConversation(conversationIds[0]) ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@ function getDefaultAgent() {
|
|||
const hub = getCurrentHub()
|
||||
if (!hub) return null
|
||||
|
||||
const agentIds = hub.listAgents()
|
||||
if (agentIds.length === 0) return null
|
||||
const conversationIds = hub.listConversations()
|
||||
if (conversationIds.length === 0) return null
|
||||
|
||||
return hub.getAgent(agentIds[0]) ?? null
|
||||
return hub.getConversation(conversationIds[0]) ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -67,8 +67,7 @@ export interface CurrentProviderInfo {
|
|||
// Local chat event types (for direct IPC communication without Gateway)
|
||||
export interface LocalChatEvent {
|
||||
agentId: string
|
||||
conversationId?: string
|
||||
sessionId?: string
|
||||
conversationId: string
|
||||
streamId?: string
|
||||
type?: 'error'
|
||||
content?: string
|
||||
|
|
@ -101,8 +100,7 @@ export interface InboundMessageEvent {
|
|||
export interface LocalChatApproval {
|
||||
approvalId: string
|
||||
agentId: string
|
||||
conversationId?: string
|
||||
sessionId?: string
|
||||
conversationId: string
|
||||
command: string
|
||||
cwd?: string
|
||||
riskLevel: 'safe' | 'needs-review' | 'dangerous'
|
||||
|
|
@ -162,15 +160,11 @@ const electronAPI = {
|
|||
getAgentInfo: (): Promise<AgentInfo | null> => ipcRenderer.invoke('hub:getAgentInfo'),
|
||||
info: () => ipcRenderer.invoke('hub:info'),
|
||||
reconnect: (url: string) => ipcRenderer.invoke('hub:reconnect', url),
|
||||
listAgents: () => ipcRenderer.invoke('hub:listAgents'),
|
||||
listConversations: () => ipcRenderer.invoke('hub:listConversations'),
|
||||
createAgent: (id?: string) => ipcRenderer.invoke('hub:createAgent', id),
|
||||
createConversation: (id?: string) => ipcRenderer.invoke('hub:createConversation', id),
|
||||
getAgent: (id: string) => ipcRenderer.invoke('hub:getAgent', id),
|
||||
getConversation: (id: string) => ipcRenderer.invoke('hub:getConversation', id),
|
||||
closeAgent: (id: string) => ipcRenderer.invoke('hub:closeAgent', id),
|
||||
closeConversation: (id: string) => ipcRenderer.invoke('hub:closeConversation', id),
|
||||
sendMessage: (agentId: string, content: string, conversationId?: string) =>
|
||||
sendMessage: (agentId: string, content: string, conversationId: string) =>
|
||||
ipcRenderer.invoke('hub:sendMessage', agentId, content, conversationId),
|
||||
registerToken: (token: string, agentId: string, conversationId: string, expiresAt: number) =>
|
||||
ipcRenderer.invoke('hub:registerToken', token, agentId, conversationId, expiresAt),
|
||||
|
|
@ -337,19 +331,19 @@ const electronAPI = {
|
|||
|
||||
// Local chat (direct IPC, no Gateway required)
|
||||
localChat: {
|
||||
/** Subscribe to agent events for local direct chat */
|
||||
subscribe: (agentId: string) => ipcRenderer.invoke('localChat:subscribe', agentId),
|
||||
/** Unsubscribe from agent events */
|
||||
unsubscribe: (agentId: string) => ipcRenderer.invoke('localChat:unsubscribe', agentId),
|
||||
/** Subscribe to conversation events for local direct chat */
|
||||
subscribe: (conversationId: string) => ipcRenderer.invoke('localChat:subscribe', conversationId),
|
||||
/** Unsubscribe from conversation events */
|
||||
unsubscribe: (conversationId: string) => ipcRenderer.invoke('localChat:unsubscribe', conversationId),
|
||||
/** Get message history for local chat with pagination (returns raw AgentMessageItem[]) */
|
||||
getHistory: (agentId: string, options?: { offset?: number; limit?: number; conversationId?: string }) =>
|
||||
ipcRenderer.invoke('localChat:getHistory', agentId, options),
|
||||
/** Send message to agent via direct IPC (no Gateway) */
|
||||
send: (agentId: string, content: string, conversationId?: string) =>
|
||||
ipcRenderer.invoke('localChat:send', agentId, content, conversationId),
|
||||
getHistory: (conversationId: string, options?: { offset?: number; limit?: number }) =>
|
||||
ipcRenderer.invoke('localChat:getHistory', conversationId, options),
|
||||
/** Send message to conversation via direct IPC (no Gateway) */
|
||||
send: (conversationId: string, content: string) =>
|
||||
ipcRenderer.invoke('localChat:send', conversationId, content),
|
||||
/** Abort the current agent run */
|
||||
abort: (agentId: string, conversationId?: string) =>
|
||||
ipcRenderer.invoke('localChat:abort', agentId, conversationId),
|
||||
abort: (conversationId: string) =>
|
||||
ipcRenderer.invoke('localChat:abort', conversationId),
|
||||
/** Resolve an exec approval request */
|
||||
resolveExecApproval: (approvalId: string, decision: string) =>
|
||||
ipcRenderer.invoke('localChat:resolveExecApproval', approvalId, decision),
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export interface ConnectionQRCodeProps {
|
|||
gateway: string
|
||||
hubId: string
|
||||
agentId: string
|
||||
conversationId?: string
|
||||
conversationId: string
|
||||
expirySeconds?: number
|
||||
size?: number
|
||||
}
|
||||
|
|
@ -131,8 +131,7 @@ export function ConnectionQRCode({
|
|||
expirySeconds = 30,
|
||||
size = 200,
|
||||
}: ConnectionQRCodeProps) {
|
||||
const resolvedConversationId = conversationId ?? agentId
|
||||
const { token, expiresAt, refresh } = useQRToken(agentId, resolvedConversationId, expirySeconds)
|
||||
const { token, expiresAt, refresh } = useQRToken(agentId, conversationId, expirySeconds)
|
||||
const remaining = useCountdown(expiresAt, refresh)
|
||||
|
||||
// Derive QR data and URL from current token (computed during render)
|
||||
|
|
@ -142,11 +141,11 @@ export function ConnectionQRCode({
|
|||
gateway,
|
||||
hubId,
|
||||
agentId,
|
||||
conversationId: resolvedConversationId,
|
||||
conversationId,
|
||||
token,
|
||||
expires: expiresAt,
|
||||
}),
|
||||
[gateway, hubId, agentId, resolvedConversationId, token, expiresAt]
|
||||
[gateway, hubId, agentId, conversationId, token, expiresAt]
|
||||
)
|
||||
|
||||
const connectionUrl = useMemo(() => {
|
||||
|
|
@ -154,12 +153,12 @@ export function ConnectionQRCode({
|
|||
gateway,
|
||||
hub: hubId,
|
||||
agent: agentId,
|
||||
conversation: resolvedConversationId,
|
||||
conversation: conversationId,
|
||||
token,
|
||||
exp: expiresAt.toString(),
|
||||
})
|
||||
return `multica://connect?${params.toString()}`
|
||||
}, [gateway, hubId, agentId, resolvedConversationId, token, expiresAt])
|
||||
}, [gateway, hubId, agentId, conversationId, token, expiresAt])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export interface TelegramConnectQRProps {
|
|||
gateway: string
|
||||
hubId: string
|
||||
agentId: string
|
||||
conversationId?: string
|
||||
conversationId: string
|
||||
expirySeconds?: number
|
||||
size?: number
|
||||
}
|
||||
|
|
@ -28,8 +28,7 @@ export function TelegramConnectQR({
|
|||
expirySeconds = 30,
|
||||
size = 200,
|
||||
}: TelegramConnectQRProps) {
|
||||
const resolvedConversationId = conversationId ?? agentId
|
||||
const { token, expiresAt, refresh } = useQRToken(agentId, resolvedConversationId, expirySeconds)
|
||||
const { token, expiresAt, refresh } = useQRToken(agentId, conversationId, expirySeconds)
|
||||
const remaining = useCountdown(expiresAt, refresh)
|
||||
|
||||
const [deepLink, setDeepLink] = useState<string | null>(null)
|
||||
|
|
@ -51,7 +50,7 @@ export function TelegramConnectQR({
|
|||
gateway,
|
||||
hubId,
|
||||
agentId,
|
||||
conversationId: resolvedConversationId,
|
||||
conversationId,
|
||||
token,
|
||||
expires: expiresAt,
|
||||
}),
|
||||
|
|
@ -82,7 +81,7 @@ export function TelegramConnectQR({
|
|||
|
||||
fetchCode()
|
||||
return () => { cancelled = true }
|
||||
}, [token, expiresAt, gateway, hubId, agentId, resolvedConversationId])
|
||||
}, [token, expiresAt, gateway, hubId, agentId, conversationId])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ export function useLocalChat(options: UseLocalChatOptions = {}) {
|
|||
|
||||
window.electronAPI.hub.init()
|
||||
.then((result) => {
|
||||
const r = result as { defaultAgentId?: string; defaultConversationId?: string }
|
||||
const defaultConversationId = r.defaultConversationId ?? r.defaultAgentId
|
||||
const r = result as { defaultConversationId?: string }
|
||||
const defaultConversationId = r.defaultConversationId
|
||||
console.log('[LocalChat] hub.init → defaultConversationId:', defaultConversationId)
|
||||
if (defaultConversationId) {
|
||||
setAgentId(defaultConversationId)
|
||||
|
|
@ -68,7 +68,6 @@ export function useLocalChat(options: UseLocalChatOptions = {}) {
|
|||
// Subscribe to events + fetch history once conversation is available
|
||||
useEffect(() => {
|
||||
if (!activeConversationId) return
|
||||
const resolvedAgentId = agentId ?? activeConversationId
|
||||
setQueuedMessages([])
|
||||
offsetRef.current = null
|
||||
setIsLoading(false)
|
||||
|
|
@ -125,33 +124,32 @@ export function useLocalChat(options: UseLocalChatOptions = {}) {
|
|||
chatRef.current.addUserMessage(
|
||||
event.content,
|
||||
event.agentId,
|
||||
event.source as MessageSource,
|
||||
eventConversationId,
|
||||
event.source as MessageSource,
|
||||
)
|
||||
setIsLoading(true)
|
||||
}
|
||||
})
|
||||
|
||||
// Fetch history with pagination
|
||||
window.electronAPI.localChat.getHistory(resolvedAgentId, {
|
||||
window.electronAPI.localChat.getHistory(activeConversationId, {
|
||||
limit: DEFAULT_MESSAGES_LIMIT,
|
||||
conversationId: activeConversationId,
|
||||
})
|
||||
.then((result) => {
|
||||
console.log('[LocalChat] getHistory result:', result.messages?.length, 'messages, total:', result.total)
|
||||
if (result.messages?.length) {
|
||||
chatRef.current.setHistory(result.messages as AgentMessageItem[], resolvedAgentId, {
|
||||
chatRef.current.setHistory(result.messages as AgentMessageItem[], activeConversationId, activeConversationId, {
|
||||
total: result.total,
|
||||
offset: result.offset,
|
||||
contextWindowTokens: result.contextWindowTokens,
|
||||
}, activeConversationId)
|
||||
})
|
||||
offsetRef.current = result.offset
|
||||
} else {
|
||||
chatRef.current.setHistory([], resolvedAgentId, {
|
||||
chatRef.current.setHistory([], activeConversationId, activeConversationId, {
|
||||
total: 0,
|
||||
offset: 0,
|
||||
contextWindowTokens: result.contextWindowTokens,
|
||||
}, activeConversationId)
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
|
|
@ -163,7 +161,7 @@ export function useLocalChat(options: UseLocalChatOptions = {}) {
|
|||
window.electronAPI.hub.offInboundMessage()
|
||||
window.electronAPI.localChat.unsubscribe(activeConversationId).catch(() => {})
|
||||
}
|
||||
}, [agentId, activeConversationId])
|
||||
}, [activeConversationId])
|
||||
|
||||
useEffect(() => {
|
||||
isLoadingRef.current = isLoading
|
||||
|
|
@ -172,11 +170,10 @@ export function useLocalChat(options: UseLocalChatOptions = {}) {
|
|||
const dispatchMessageNow = useCallback((text: string) => {
|
||||
const trimmed = text.trim()
|
||||
if (!trimmed || !activeConversationId) return
|
||||
const resolvedAgentId = agentId ?? activeConversationId
|
||||
chatRef.current.addUserMessage(trimmed, resolvedAgentId, { type: 'local' }, activeConversationId)
|
||||
chatRef.current.addUserMessage(trimmed, activeConversationId, activeConversationId, { type: 'local' })
|
||||
chatRef.current.setError(null)
|
||||
setIsLoading(true)
|
||||
window.electronAPI.localChat.send(resolvedAgentId, trimmed, activeConversationId)
|
||||
window.electronAPI.localChat.send(activeConversationId, trimmed)
|
||||
.then((result) => {
|
||||
const response = result as { ok?: boolean; error?: string } | undefined
|
||||
if (response?.error) {
|
||||
|
|
@ -186,7 +183,7 @@ export function useLocalChat(options: UseLocalChatOptions = {}) {
|
|||
.catch(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}, [agentId, activeConversationId])
|
||||
}, [activeConversationId])
|
||||
|
||||
const sendMessage = useCallback((text: string) => {
|
||||
const trimmed = text.trim()
|
||||
|
|
@ -225,10 +222,9 @@ export function useLocalChat(options: UseLocalChatOptions = {}) {
|
|||
|
||||
const abortGeneration = useCallback(() => {
|
||||
if (!activeConversationId) return
|
||||
const resolvedAgentId = agentId ?? activeConversationId
|
||||
window.electronAPI.localChat.abort(resolvedAgentId, activeConversationId).catch(() => {})
|
||||
window.electronAPI.localChat.abort(activeConversationId).catch(() => {})
|
||||
setIsLoading(false)
|
||||
}, [agentId, activeConversationId])
|
||||
}, [activeConversationId])
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
const currentOffset = offsetRef.current
|
||||
|
|
@ -237,20 +233,18 @@ export function useLocalChat(options: UseLocalChatOptions = {}) {
|
|||
isLoadingMoreRef.current = true
|
||||
setIsLoadingMore(true)
|
||||
try {
|
||||
const resolvedAgentId = agentId ?? activeConversationId
|
||||
const newOffset = Math.max(0, currentOffset - DEFAULT_MESSAGES_LIMIT)
|
||||
const limit = currentOffset - newOffset
|
||||
const result = await window.electronAPI.localChat.getHistory(resolvedAgentId, {
|
||||
const result = await window.electronAPI.localChat.getHistory(activeConversationId, {
|
||||
offset: newOffset,
|
||||
limit,
|
||||
conversationId: activeConversationId,
|
||||
})
|
||||
if (result.messages?.length) {
|
||||
chatRef.current.prependHistory(result.messages as AgentMessageItem[], resolvedAgentId, {
|
||||
chatRef.current.prependHistory(result.messages as AgentMessageItem[], activeConversationId, activeConversationId, {
|
||||
total: result.total,
|
||||
offset: result.offset,
|
||||
contextWindowTokens: result.contextWindowTokens,
|
||||
}, activeConversationId)
|
||||
})
|
||||
offsetRef.current = result.offset
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -259,7 +253,7 @@ export function useLocalChat(options: UseLocalChatOptions = {}) {
|
|||
isLoadingMoreRef.current = false
|
||||
setIsLoadingMore(false)
|
||||
}
|
||||
}, [agentId, activeConversationId])
|
||||
}, [activeConversationId])
|
||||
|
||||
const resolveApproval = useCallback(
|
||||
(approvalId: string, decision: ApprovalDecision) => {
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ export default function HomePage() {
|
|||
gateway={hubInfo?.url ?? 'http://localhost:3000'}
|
||||
hubId={hubInfo?.hubId ?? 'unknown'}
|
||||
agentId={primaryAgent?.id}
|
||||
conversationId={primaryAgent?.id}
|
||||
expirySeconds={30}
|
||||
size={180}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ function ChannelsContent() {
|
|||
gateway={hubInfo?.url ?? 'http://localhost:3000'}
|
||||
hubId={hubInfo?.hubId ?? 'unknown'}
|
||||
agentId={primaryAgent?.id ?? 'unknown'}
|
||||
conversationId={primaryAgent?.id ?? 'unknown'}
|
||||
expirySeconds={30}
|
||||
size={200}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ export default function ConnectStep({ onNext, onBack }: ConnectStepProps) {
|
|||
gateway={hubInfo?.url ?? 'http://localhost:3000'}
|
||||
hubId={hubInfo?.hubId ?? 'unknown'}
|
||||
agentId={primaryAgent?.id ?? 'unknown'}
|
||||
conversationId={primaryAgent?.id ?? 'unknown'}
|
||||
expirySeconds={30}
|
||||
size={180}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -726,7 +726,7 @@
|
|||
from: deviceId,
|
||||
to: targetDeviceId,
|
||||
action: 'message',
|
||||
payload: { agentId: targetAgentId, conversationId: targetAgentId, sessionId: targetAgentId, content: text },
|
||||
payload: { agentId: targetAgentId, conversationId: targetAgentId, content: text },
|
||||
});
|
||||
|
||||
appendMsg('self', text);
|
||||
|
|
|
|||
|
|
@ -33,8 +33,7 @@ export class TelegramController {
|
|||
gateway: string;
|
||||
hubId: string;
|
||||
agentId: string;
|
||||
conversationId?: string;
|
||||
sessionId?: string;
|
||||
conversationId: string;
|
||||
token: string;
|
||||
expires: number;
|
||||
},
|
||||
|
|
@ -53,7 +52,7 @@ export class TelegramController {
|
|||
gateway: body.gateway,
|
||||
hubId: body.hubId,
|
||||
agentId: body.agentId,
|
||||
...((body.sessionId ?? body.conversationId) ? { conversationId: body.sessionId ?? body.conversationId } : {}),
|
||||
conversationId: body.conversationId,
|
||||
token: body.token,
|
||||
expires: body.expires,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -92,14 +92,6 @@ interface GenerateChannelWelcomeResult {
|
|||
text: string;
|
||||
}
|
||||
|
||||
interface ListAgentsResult {
|
||||
agents: Array<{ id: string; closed: boolean }>;
|
||||
}
|
||||
|
||||
interface CreateAgentResult {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface ListConversationsResult {
|
||||
conversations: Array<{ id: string; closed: boolean }>;
|
||||
}
|
||||
|
|
@ -611,7 +603,7 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy {
|
|||
`<b>Welcome back!</b>\n\n` +
|
||||
`${statusEmoji} Status: <b>${statusText}</b>\n` +
|
||||
`Agent: <code>${user.agentId}</code>\n` +
|
||||
`Session: <code>${user.conversationId ?? user.agentId}</code>\n\n` +
|
||||
`Conversation: <code>${user.conversationId ?? user.agentId}</code>\n\n` +
|
||||
(online
|
||||
? `Your agent is ready. Just send a message to start chatting.`
|
||||
: `Your Hub is offline. Make sure the Multica Desktop app is running.`);
|
||||
|
|
@ -646,7 +638,7 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy {
|
|||
`${statusEmoji} <b>${statusLabel}</b>\n\n` +
|
||||
`Hub: <code>${user.hubId}</code>\n` +
|
||||
`Agent: <code>${user.agentId}</code>\n` +
|
||||
`Session: <code>${user.conversationId ?? user.agentId}</code>\n\n` +
|
||||
`Conversation: <code>${user.conversationId ?? user.agentId}</code>\n\n` +
|
||||
(online
|
||||
? `Your Hub is online and ready to receive messages.`
|
||||
: `Your Hub is offline. Make sure the Multica Desktop app is running.`);
|
||||
|
|
@ -668,9 +660,9 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy {
|
|||
`<b>Commands</b>\n` +
|
||||
` /start \u2014 Connect your account or see welcome\n` +
|
||||
` /status \u2014 Check connection status\n` +
|
||||
` /new \u2014 Start a new isolated session\n` +
|
||||
` /session [id] \u2014 Show or switch current session\n` +
|
||||
` /sessions \u2014 List available sessions\n` +
|
||||
` /new \u2014 Start a new isolated conversation\n` +
|
||||
` /session [id] \u2014 Show or switch current conversation\n` +
|
||||
` /sessions \u2014 List available conversations\n` +
|
||||
` /help \u2014 Show this message\n\n` +
|
||||
`<b>How to connect</b>\n` +
|
||||
` <b>1.</b> Open Multica Desktop app\n` +
|
||||
|
|
@ -696,9 +688,9 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy {
|
|||
await this.bot.api.setMyCommands([
|
||||
{ command: "start", description: "Connect or show welcome" },
|
||||
{ command: "status", description: "Check connection status" },
|
||||
{ command: "new", description: "Create a new session" },
|
||||
{ command: "session", description: "Show/switch current session" },
|
||||
{ command: "sessions", description: "List sessions" },
|
||||
{ command: "new", description: "Create a new conversation" },
|
||||
{ command: "session", description: "Show/switch current conversation" },
|
||||
{ command: "sessions", description: "List conversations" },
|
||||
{ command: "help", description: "Show help and instructions" },
|
||||
]);
|
||||
|
||||
|
|
@ -765,59 +757,28 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy {
|
|||
await ctx.reply(welcome.text, { parse_mode: "HTML", reply_markup: welcome.keyboard });
|
||||
}
|
||||
|
||||
private isMethodNotFoundError(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return message.includes("METHOD_NOT_FOUND") || message.includes("Unknown RPC method");
|
||||
}
|
||||
|
||||
private async createConversationViaRpc(deviceId: string, hubId: string, agentId?: string): Promise<{ id: string }> {
|
||||
try {
|
||||
const created = await this.sendRpc<{ agentId?: string }, CreateConversationResult>(
|
||||
deviceId,
|
||||
hubId,
|
||||
"createConversation",
|
||||
agentId ? { agentId } : {},
|
||||
VERIFY_TIMEOUT_MS,
|
||||
"Create session request timed out",
|
||||
"Create conversation request timed out",
|
||||
);
|
||||
return { id: created.id };
|
||||
} catch (error) {
|
||||
if (!this.isMethodNotFoundError(error)) throw error;
|
||||
const created = await this.sendRpc<Record<string, never>, CreateAgentResult>(
|
||||
deviceId,
|
||||
hubId,
|
||||
"createAgent",
|
||||
{},
|
||||
VERIFY_TIMEOUT_MS,
|
||||
"Create session request timed out",
|
||||
);
|
||||
return { id: created.id };
|
||||
}
|
||||
}
|
||||
|
||||
private async listConversationsViaRpc(deviceId: string, hubId: string): Promise<Array<{ id: string; closed: boolean }>> {
|
||||
try {
|
||||
const result = await this.sendRpc<Record<string, never>, ListConversationsResult>(
|
||||
deviceId,
|
||||
hubId,
|
||||
"listConversations",
|
||||
{},
|
||||
VERIFY_TIMEOUT_MS,
|
||||
"List sessions request timed out",
|
||||
"List conversations request timed out",
|
||||
);
|
||||
return result.conversations;
|
||||
} catch (error) {
|
||||
if (!this.isMethodNotFoundError(error)) throw error;
|
||||
const result = await this.sendRpc<Record<string, never>, ListAgentsResult>(
|
||||
deviceId,
|
||||
hubId,
|
||||
"listAgents",
|
||||
{},
|
||||
VERIFY_TIMEOUT_MS,
|
||||
"List sessions request timed out",
|
||||
);
|
||||
return result.agents;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleNewConversationCommand(ctx: Context): Promise<void> {
|
||||
|
|
@ -856,14 +817,14 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy {
|
|||
await this.bindConversationToContext(user, ctx, created.id);
|
||||
|
||||
await ctx.reply(
|
||||
`<b>\u2705 New session created</b>\n\n` +
|
||||
`Session: <code>${created.id}</code>\n\n` +
|
||||
`All next messages in this Telegram thread will use this session.`,
|
||||
`<b>\u2705 New conversation created</b>\n\n` +
|
||||
`Conversation: <code>${created.id}</code>\n\n` +
|
||||
`All next messages in this Telegram thread will use this conversation.`,
|
||||
{ parse_mode: "HTML" },
|
||||
);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
await ctx.reply(`Failed to create session: ${message}`);
|
||||
await ctx.reply(`Failed to create conversation: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -886,28 +847,28 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy {
|
|||
|
||||
try {
|
||||
const conversations = await this.listConversationsViaRpc(user.deviceId, user.hubId);
|
||||
const sessions = conversations.filter((item) => !item.closed).map((item) => item.id);
|
||||
if (sessions.length === 0) {
|
||||
await ctx.reply("No sessions found.");
|
||||
const conversationIds = conversations.filter((item) => !item.closed).map((item) => item.id);
|
||||
if (conversationIds.length === 0) {
|
||||
await ctx.reply("No conversations found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const current = await this.resolveConversationForContext(user, ctx);
|
||||
const lines = sessions.slice(0, 20).map((id) => {
|
||||
const lines = conversationIds.slice(0, 20).map((id) => {
|
||||
const marker = id === current ? "\u2022 current" : "";
|
||||
return `<code>${id}</code>${marker ? ` ${marker}` : ""}`;
|
||||
});
|
||||
const extra = sessions.length > 20 ? `\n...and ${sessions.length - 20} more` : "";
|
||||
const extra = conversationIds.length > 20 ? `\n...and ${conversationIds.length - 20} more` : "";
|
||||
|
||||
await ctx.reply(
|
||||
`<b>Available sessions</b>\n\n` +
|
||||
`<b>Available conversations</b>\n\n` +
|
||||
`${lines.join("\n")}${extra}\n\n` +
|
||||
`Use <code>/session <id></code> to switch.`,
|
||||
{ parse_mode: "HTML" },
|
||||
);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
await ctx.reply(`Failed to load sessions: ${message}`);
|
||||
await ctx.reply(`Failed to load conversations: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -923,7 +884,7 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy {
|
|||
const current = await this.resolveConversationForContext(user, ctx);
|
||||
if (!target) {
|
||||
await ctx.reply(
|
||||
`<b>Current session</b>\n\n` +
|
||||
`<b>Current conversation</b>\n\n` +
|
||||
`<code>${current}</code>\n\n` +
|
||||
`Use <code>/session <id></code> to switch.`,
|
||||
{ parse_mode: "HTML" },
|
||||
|
|
@ -945,7 +906,7 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy {
|
|||
const exists = conversations.some((item) => item.id === target && !item.closed);
|
||||
if (!exists) {
|
||||
await ctx.reply(
|
||||
`Session not found: <code>${target}</code>\n\nUse /sessions to list available sessions.`,
|
||||
`Conversation not found: <code>${target}</code>\n\nUse /sessions to list available conversations.`,
|
||||
{ parse_mode: "HTML" },
|
||||
);
|
||||
return;
|
||||
|
|
@ -964,13 +925,13 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy {
|
|||
await this.bindConversationToContext(user, ctx, target);
|
||||
|
||||
await ctx.reply(
|
||||
`<b>\u2705 Session switched</b>\n\n` +
|
||||
`Current session: <code>${target}</code>`,
|
||||
`<b>\u2705 Conversation switched</b>\n\n` +
|
||||
`Current conversation: <code>${target}</code>`,
|
||||
{ parse_mode: "HTML" },
|
||||
);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
await ctx.reply(`Failed to switch session: ${message}`);
|
||||
await ctx.reply(`Failed to switch conversation: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1372,19 +1333,14 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy {
|
|||
? `Telegram @${msg.from.username}`
|
||||
: `Telegram ${msg?.from?.first_name ?? telegramUserId}`,
|
||||
});
|
||||
const sessionId =
|
||||
connectionInfo.conversationId
|
||||
?? result.sessionId
|
||||
?? result.conversationId
|
||||
?? result.mainConversationId
|
||||
?? result.agentId;
|
||||
const conversationId = connectionInfo.conversationId ?? result.conversationId;
|
||||
|
||||
// 5. Save to DB
|
||||
await this.userStore.upsert({
|
||||
telegramUserId,
|
||||
hubId: connectionInfo.hubId,
|
||||
agentId: connectionInfo.agentId,
|
||||
conversationId: sessionId,
|
||||
conversationId,
|
||||
deviceId,
|
||||
telegramUsername: msg?.from?.username,
|
||||
telegramFirstName: msg?.from?.first_name,
|
||||
|
|
@ -1395,7 +1351,7 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy {
|
|||
await this.userStore.setThreadConversation(
|
||||
telegramUserId,
|
||||
threadRoute.chatId,
|
||||
sessionId,
|
||||
conversationId,
|
||||
threadRoute.threadId,
|
||||
);
|
||||
}
|
||||
|
|
@ -1408,7 +1364,7 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy {
|
|||
`<b>\u2705 Connected successfully!</b>\n\n` +
|
||||
`Hub: <code>${result.hubId}</code>\n` +
|
||||
`Agent: <code>${result.agentId}</code>\n` +
|
||||
`Session: <code>${sessionId}</code>\n\n` +
|
||||
`Conversation: <code>${conversationId}</code>\n\n` +
|
||||
`You can now send messages to interact with your agent.`,
|
||||
{ parse_mode: "HTML", reply_markup: successKeyboard },
|
||||
);
|
||||
|
|
@ -1626,7 +1582,7 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy {
|
|||
from: user.deviceId,
|
||||
to: user.hubId,
|
||||
action: "message",
|
||||
payload: { agentId: user.agentId, conversationId, sessionId: conversationId, content: text },
|
||||
payload: { agentId: user.agentId, conversationId, content: text },
|
||||
};
|
||||
|
||||
const sent = this.eventsGateway.routeFromVirtualDevice(message);
|
||||
|
|
@ -1677,7 +1633,7 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy {
|
|||
const streamPayload = msg.payload as StreamPayload;
|
||||
const event = streamPayload?.event;
|
||||
if (!event || !("type" in event)) return;
|
||||
const conversationId = streamPayload?.sessionId ?? streamPayload?.conversationId;
|
||||
const conversationId = streamPayload?.conversationId;
|
||||
const contextKey = this.makeConversationContextKey(deviceId, conversationId);
|
||||
|
||||
// Start typing when LLM begins generating
|
||||
|
|
@ -1745,10 +1701,9 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy {
|
|||
caption?: string;
|
||||
filename?: string;
|
||||
conversationId?: string;
|
||||
sessionId?: string;
|
||||
};
|
||||
if (payload?.data) {
|
||||
const conversationId = payload.sessionId ?? payload.conversationId;
|
||||
const conversationId = payload.conversationId;
|
||||
const contextKey = this.makeConversationContextKey(deviceId, conversationId);
|
||||
void this.sendFileToTelegram(
|
||||
deviceId,
|
||||
|
|
@ -1769,10 +1724,9 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy {
|
|||
content?: string;
|
||||
agentId?: string;
|
||||
conversationId?: string;
|
||||
sessionId?: string;
|
||||
};
|
||||
if (payload?.content) {
|
||||
const conversationId = payload.sessionId ?? payload.conversationId;
|
||||
const conversationId = payload.conversationId;
|
||||
const contextKey = this.makeConversationContextKey(deviceId, conversationId);
|
||||
void this.sendToTelegram(deviceId, payload.content, conversationId).then(() => {
|
||||
void this.clearMessageContext(contextKey);
|
||||
|
|
@ -1787,9 +1741,8 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy {
|
|||
message?: string;
|
||||
code?: string;
|
||||
conversationId?: string;
|
||||
sessionId?: string;
|
||||
};
|
||||
const conversationId = payload.sessionId ?? payload.conversationId;
|
||||
const conversationId = payload.conversationId;
|
||||
const contextKey = this.makeConversationContextKey(deviceId, conversationId);
|
||||
this.stopTyping(contextKey);
|
||||
void this.clearMessageContext(contextKey);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export class AppController {
|
|||
hubId: this.hub.hubId,
|
||||
url: this.hub.url,
|
||||
connectionState: this.hub.connectionState,
|
||||
agentCount: this.hub.listAgents().length,
|
||||
agentCount: this.hub.listConversations().length,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -33,26 +33,6 @@ export class AppController {
|
|||
};
|
||||
}
|
||||
|
||||
@Get("agents")
|
||||
listAgents() {
|
||||
return this.hub.listAgents().map((id) => {
|
||||
const agent = this.hub.getAgent(id);
|
||||
return { id, closed: agent?.closed ?? true };
|
||||
});
|
||||
}
|
||||
|
||||
@Post("agents")
|
||||
createAgent(@Body() body?: { id?: string }) {
|
||||
const agent = this.hub.createAgent(body?.id);
|
||||
return { id: agent.sessionId };
|
||||
}
|
||||
|
||||
@Delete("agents/:id")
|
||||
deleteAgent(@Param("id") id: string) {
|
||||
const ok = this.hub.closeAgent(id);
|
||||
return { ok };
|
||||
}
|
||||
|
||||
@Get("conversations")
|
||||
listConversations() {
|
||||
return this.hub.listConversations().map((id) => {
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@
|
|||
color: var(--green);
|
||||
}
|
||||
|
||||
/* ── Agents section ── */
|
||||
/* ── Conversations section ── */
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -347,7 +347,7 @@
|
|||
<div class="stat-value" id="stat-state">—</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Agents</div>
|
||||
<div class="stat-label">Conversations</div>
|
||||
<div class="stat-value" id="stat-agents">—</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -359,17 +359,17 @@
|
|||
<button class="btn btn-accent" onclick="updateGateway()">Connect</button>
|
||||
</div>
|
||||
|
||||
<!-- Agents -->
|
||||
<!-- Conversations -->
|
||||
<div class="section-header">
|
||||
<span class="section-title">Agents</span>
|
||||
<button class="btn btn-accent" onclick="createAgent()">
|
||||
<span class="section-title">Conversations</span>
|
||||
<button class="btn btn-accent" onclick="createConversation()">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
Create Agent
|
||||
Create Conversation
|
||||
</button>
|
||||
</div>
|
||||
<div class="agent-table" id="agent-table">
|
||||
<div class="agent-table-head">
|
||||
<span>Agent ID</span>
|
||||
<span>Conversation ID</span>
|
||||
<span>Actions</span>
|
||||
</div>
|
||||
<div id="agent-list"></div>
|
||||
|
|
@ -419,9 +419,9 @@
|
|||
}
|
||||
|
||||
async function refresh() {
|
||||
const [hubRes, agentsRes] = await Promise.all([
|
||||
const [hubRes, conversationsRes] = await Promise.all([
|
||||
fetch(`${API}/hub`).then(r => r.json()),
|
||||
fetch(`${API}/agents`).then(r => r.json()),
|
||||
fetch(`${API}/conversations`).then(r => r.json()),
|
||||
]);
|
||||
|
||||
// Stats
|
||||
|
|
@ -446,19 +446,19 @@
|
|||
input.value = hubRes.url || '';
|
||||
}
|
||||
|
||||
// Agent list
|
||||
// Conversation list
|
||||
const list = document.getElementById('agent-list');
|
||||
if (agentsRes.length === 0) {
|
||||
list.innerHTML = '<div class="empty-state">No agents yet. Create one to get started.</div>';
|
||||
if (conversationsRes.length === 0) {
|
||||
list.innerHTML = '<div class="empty-state">No conversations yet. Create one to get started.</div>';
|
||||
} else {
|
||||
list.innerHTML = agentsRes.map(a =>
|
||||
list.innerHTML = conversationsRes.map(a =>
|
||||
`<div class="agent-row">` +
|
||||
`<div class="agent-id">` +
|
||||
`<span title="${a.id}">${a.id}</span>` +
|
||||
`<button class="copy-btn" onclick="copyText('${a.id}', this)">${copyIcon()}</button>` +
|
||||
`</div>` +
|
||||
`<div class="agent-actions">` +
|
||||
`<button class="btn-danger-ghost btn" onclick="deleteAgent('${a.id}')" title="Delete">${trashIcon()}</button>` +
|
||||
`<button class="btn-danger-ghost btn" onclick="deleteConversation('${a.id}')" title="Delete">${trashIcon()}</button>` +
|
||||
`</div>` +
|
||||
`</div>`
|
||||
).join('');
|
||||
|
|
@ -482,13 +482,13 @@
|
|||
refresh();
|
||||
}
|
||||
|
||||
async function createAgent() {
|
||||
await fetch(`${API}/agents`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
||||
async function createConversation() {
|
||||
await fetch(`${API}/conversations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
||||
refresh();
|
||||
}
|
||||
|
||||
async function deleteAgent(id) {
|
||||
await fetch(`${API}/agents/${id}`, { method: 'DELETE' });
|
||||
async function deleteConversation(id) {
|
||||
await fetch(`${API}/conversations/${id}`, { method: 'DELETE' });
|
||||
refresh();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,17 +56,13 @@ pnpm dev:local:archive
|
|||
|
||||
- `agentId`: logical owner identity (capabilities/profile scope).
|
||||
- `conversationId`: isolated runtime thread under an agent.
|
||||
- `sessionId`: runtime/storage id for a conversation (currently same as `conversationId`).
|
||||
- `sessionId`: internal runner/storage identifier for a conversation. External protocols use `conversationId`.
|
||||
|
||||
Compatibility behavior:
|
||||
Protocol rules:
|
||||
|
||||
- If only `agentId` is provided, runtime resolves to that agent's `mainConversationId`.
|
||||
- Legacy fallback is still supported: when no mapping exists, `conversationId = agentId`.
|
||||
- New integrations should pass `conversationId` explicitly.
|
||||
- Hub RPC supports both naming sets:
|
||||
- Legacy: `createAgent/listAgents/deleteAgent`
|
||||
- Conversation-first aliases: `createConversation/listConversations/deleteConversation`
|
||||
- `createConversation` supports optional `agentId` to create a new thread under a specific agent.
|
||||
- Hub RPC is conversation-first: `createConversation/listConversations/deleteConversation`.
|
||||
- All message, stream, and verify payloads use `conversationId` (no `sessionId` alias fields).
|
||||
- New integrations should always pass `conversationId` explicitly.
|
||||
|
||||
Telegram behavior:
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue