refactor(interact): separate runtime actions from ui projection

This commit is contained in:
2026-08-06 00:32:59 +08:00
parent c77a0cf69b
commit 823e71a162
8 changed files with 125 additions and 57 deletions
@@ -105,6 +105,10 @@ describe("LineUpRuntime", () => {
]));
const chat = runtime.openApp("chat");
expect(chat.app_scope).toBe("chat");
expect(chat.runtime.dispatch).toBe(chat.dispatch);
expect(chat.runtime.interactions).toBe(chat.interactions);
expect(chat.runtime.tasks).toBe(chat.tasks);
expect(chat.ui.snapshot).toBe(chat.snapshot);
expect(chat.snapshot()).toMatchObject({ app_scope: "chat", active: false });
await runtime.login(login);
@@ -253,40 +253,43 @@ export class LineUpRuntime {
const app = this.apps.get(appScope);
if (!app?.enabled) throw new Error(`The ${appScope} Core App is unavailable.`);
if (appScope !== "chat") throw new Error(`The ${appScope} Core App SDK is not registered.`);
return {
app_scope: "chat",
const ui = {
snapshot: () => this.chatView(),
subscribe: listener => {
subscribe: (listener: (view: ChatViewModel) => void) => {
this.chatStateListeners.add(listener);
listener(this.chatView());
return () => this.chatStateListeners.delete(listener);
},
subscribeEvents: listener => this.onEvent(event => {
// Agent content has a separate, scope-checked SDK channel. Keeping it
// out of the generic event stream prevents a Chat renderer from
// accidentally consuming an unprojected delivery path.
subscribeEvents: (listener: (event: Exclude<RuntimeAppEvent, { type: "agent-message" }>) => void) => this.onEvent(event => {
if (event.type !== "agent-message") listener(event);
}),
subscribeAgentMessages: listener => {
subscribeAgentMessages: (listener: (message: AgentChatMessage) => void) => {
this.chatMessageListeners.add(listener);
return () => this.chatMessageListeners.delete(listener);
},
listAgentMessages: afterMessageID => this.listChatMessages(afterMessageID),
acknowledgeAgentMessage: messageID => this.acknowledgeChatMessage(messageID),
dispatch: action => this.dispatchChatAction(action),
interactions: {
submit: (callID, result) => this.submitToolCall(callID, result),
cancel: (callID, reason) => this.cancelToolCall(callID, reason),
expire: callID => this.expireToolCall(callID),
read: callID => this.toolCalls().find(record => record.call_id === callID),
loadDraft: callID => this.getToolDraft(callID),
saveDraft: (callID, values) => this.saveToolDraft(callID, values),
clearDraft: callID => this.clearToolDraft(callID),
},
tasks: {
requestCancel: operationID => this.requestTaskCancel(operationID),
read: operationID => this.tasks().find(task => task.operation_id === operationID),
},
listAgentMessages: (afterMessageID?: string) => this.listChatMessages(afterMessageID),
acknowledgeAgentMessage: (messageID: string) => this.acknowledgeChatMessage(messageID),
};
const interactions = {
submit: (callID: string, result: JsonObject) => this.submitToolCall(callID, result),
cancel: (callID: string, reason: string) => this.cancelToolCall(callID, reason),
expire: (callID: string) => this.expireToolCall(callID),
read: (callID: string) => this.toolCalls().find(record => record.call_id === callID),
loadDraft: (callID: string) => this.getToolDraft(callID),
saveDraft: (callID: string, values: JsonObject) => this.saveToolDraft(callID, values),
clearDraft: (callID: string) => this.clearToolDraft(callID),
};
const tasks = {
requestCancel: (operationID: string) => this.requestTaskCancel(operationID),
read: (operationID: string) => this.tasks().find(task => task.operation_id === operationID),
};
const runtime = { dispatch: (action: ChatAction) => this.dispatchChatAction(action), interactions, tasks };
return {
app_scope: "chat",
runtime,
ui,
...ui,
...runtime,
};
}