Archived
fix(sessions): stop duplicating user message for slash commands
A slash command sent as the first (or any idle) message showed the raw command text twice until reload: once from the client's optimistic insert and once from the server's message.append echo, neither of which converges with the agent's canonical expanded message (e.g. a /skill:* block). Make commands obey the same source-of-truth contract as prompts: - client no longer inserts the raw command text optimistically; it shows the existing per-session sending indicator instead - forwarded runtime/skill commands return a bare done result rather than a synthetic "Accepted ..." line - server suppresses the raw message.append echo for command-forwarded prompts (threaded through the compaction queue too) Result: pre-reload state matches reload, with no transient duplicate.
This commit is contained in:
@@ -308,6 +308,36 @@ describe("SessionController", () => {
|
||||
expect(state.sendingPrompts).toEqual({});
|
||||
});
|
||||
|
||||
it("sends slash commands without inserting an optimistic transcript line and toggles the sending state", async () => {
|
||||
let state: AppState = { ...initialAppState(), selectedWorkspace: workspace, selectedSession: oldSession, sessions: [oldSession] };
|
||||
let resolveCommand: (() => void) | undefined;
|
||||
const seenDuringCommand: Record<string, true>[] = [];
|
||||
const api: typeof defaultApi = {
|
||||
...defaultApi,
|
||||
runCommand: (_session, text) => new Promise((resolve) => {
|
||||
seenDuringCommand.push({ ...state.sendingPrompts });
|
||||
resolveCommand = () => { resolve(text.startsWith("/skill") ? { type: "done" } : { type: "done", message: "stats" }); };
|
||||
}),
|
||||
};
|
||||
const controller = new SessionController(
|
||||
() => state,
|
||||
(patch) => { state = { ...state, ...patch }; },
|
||||
() => undefined,
|
||||
undefined,
|
||||
{ api, socket: new FakeSocket() },
|
||||
);
|
||||
|
||||
const run = controller.send("/skill:skill-creator");
|
||||
expect(seenDuringCommand).toEqual([{ [oldSession.id]: true }]);
|
||||
// No raw command text is added to the transcript; the agent streams the
|
||||
// canonical expanded message back instead.
|
||||
expect(state.messages).toEqual([]);
|
||||
resolveCommand?.();
|
||||
await run;
|
||||
expect(state.messages).toEqual([]);
|
||||
expect(state.sendingPrompts).toEqual({});
|
||||
});
|
||||
|
||||
it("keeps live message count updates when a cached new session becomes persisted", async () => {
|
||||
const cachedSession = markCachedNewSessionInfo(oldSession);
|
||||
let resolvePrompt: (() => void) | undefined;
|
||||
|
||||
@@ -235,12 +235,21 @@ export class SessionController {
|
||||
async runCommand(text: string) {
|
||||
const session = this.getState().selectedSession;
|
||||
if (!session || session.archived === true) return;
|
||||
this.setState({ messages: [...this.getState().messages, textMessage("user", text)] });
|
||||
// Commands are not inserted into the transcript optimistically: a builtin
|
||||
// command produces its own result line, and a runtime/skill command is
|
||||
// forwarded to the agent, which streams back the canonical (expanded)
|
||||
// message. Inserting the raw text here would leave a line that doesn't
|
||||
// converge with server history and disappears on reload. Surface the same
|
||||
// per-session sending indicator that send() uses for the pre-receipt window.
|
||||
const sessionId = session.id;
|
||||
this.markSendingPrompt(sessionId, true);
|
||||
try {
|
||||
this.applyCommandResult(await this.api.runCommand(session, text, selectedMachineId(this.getState())));
|
||||
this.markCachedNewSessionPersisted(session);
|
||||
} catch (error) {
|
||||
this.setState({ messages: [...this.getState().messages, textMessage("system", String(error))], error: String(error) });
|
||||
} finally {
|
||||
this.markSendingPrompt(sessionId, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user