Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/agent-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ consumption of shared panel shells:
| A6.194 | Apply session workspace target |
| A6.195 | Remove session + next active id |
| A6.196 | Maybe auto-derive untitled session title |
| A6.197 | Cut transcript through Nth user turn |

### A5 complete enough

Expand Down
25 changes: 25 additions & 0 deletions packages/agent-ui/src/__tests__/cutThroughNthUser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";
import { cutThroughNthUserMessage } from "../lib/cutThroughNthUser.js";

describe("cutThroughNthUserMessage", () => {
const msgs = [
{ role: "user", id: "1" },
{ role: "assistant", id: "2" },
{ role: "user", id: "3" },
{ role: "assistant", id: "4" },
];
it("keeps through Nth user", () => {
expect(cutThroughNthUserMessage(msgs, 1).map((m) => m.id)).toEqual(["1"]);
expect(cutThroughNthUserMessage(msgs, 2).map((m) => m.id)).toEqual([
"1",
"2",
"3",
]);
});
it("noop when fewer users", () => {
expect(cutThroughNthUserMessage(msgs, 9)).toHaveLength(4);
});
it("empty when keep <= 0", () => {
expect(cutThroughNthUserMessage(msgs, 0)).toEqual([]);
});
});
2 changes: 2 additions & 0 deletions packages/agent-ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,8 @@ export {
nextActiveIdAfterDelete,
} from "./lib/removeSessionFromList.js";
export { maybeDeriveSessionTitleList } from "./lib/maybeDeriveSessionTitle.js";
export { cutThroughNthUserMessage } from "./lib/cutThroughNthUser.js";
export type { MessageWithRole } from "./lib/cutThroughNthUser.js";
export type { DerivableSession } from "./lib/maybeDeriveSessionTitle.js";
export type {
SessionWorkspaceTarget,
Expand Down
23 changes: 23 additions & 0 deletions packages/agent-ui/src/lib/cutThroughNthUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Pure rewind-window cut for chat messages (A6.197).
* Keep the first N user turns (inclusive) and drop everything after.
*/

export type MessageWithRole = { role: string };

/**
* Truncate after the Nth user message. `keep <= 0` → empty.
* If fewer than N user messages exist, return a shallow copy of the input.
*/
export function cutThroughNthUserMessage<T extends MessageWithRole>(
messages: readonly T[],
keep: number,
): T[] {
if (keep <= 0) return [];
let seen = 0;
for (let i = 0; i < messages.length; i++) {
if (messages[i]!.role === "user") seen++;
if (seen >= keep) return messages.slice(0, i + 1) as T[];
}
return messages.slice() as T[];
}
22 changes: 6 additions & 16 deletions src/modules/ai/store/chatStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
removeSessionFromList,
nextActiveIdAfterDelete,
maybeDeriveSessionTitleList,
cutThroughNthUserMessage,
joinMessageTextParts,
} from "@altai/agent-ui";
import type { UIMessage } from "ai";
import { native } from "../lib/native";
Expand Down Expand Up @@ -1117,11 +1119,9 @@ useChatStore.subscribe((state, prev) => {

/** Plain-text body of a user message (text parts joined). */
function userMessageText(m: UIMessage): string {
return m.parts
.filter((p): p is { type: "text"; text: string } => p.type === "text")
.map((p) => p.text)
.join("\n")
.trim();
return joinMessageTextParts(
m.parts as { type?: string; text?: string }[],
).trim();
}

/**
Expand All @@ -1131,16 +1131,6 @@ function userMessageText(m: UIMessage): string {
* (matches the backend no-op). Mirrors `TruncateAfterUserMessage` so the
* frontend transcript stays in sync after a rewind.
*/
function cutThroughNthUser(messages: UIMessage[], keep: number): UIMessage[] {
if (keep <= 0) return [];
let seen = 0;
for (let i = 0; i < messages.length; i++) {
if (messages[i].role === "user") seen++;
if (seen >= keep) return messages.slice(0, i + 1);
}
return messages.slice();
}

function workspacePathForChat(chatId?: string | null): string | undefined {
const state = useChatStore.getState();
const id = chatId ?? state.activeSessionId;
Expand Down Expand Up @@ -1198,7 +1188,7 @@ async function rewindAndResend(
});
return false;
}
const cut = cutThroughNthUser(
const cut = cutThroughNthUserMessage(
useChatStore.getState().nativeMessages,
keepUserMessages,
);
Expand Down
Loading