mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-09 15:52:14 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
832c221aff |
No files matched your search
@@ -333,7 +333,7 @@ test('AI side panel skips re-render when only the composer draft text changes',
|
||||
assert.equal(aiChatSidePanelPropsAreEqual(prev, next), true);
|
||||
});
|
||||
|
||||
test('workspace AI panel memo follows visible inherited session, not hidden terminal maps', () => {
|
||||
test('workspace AI panel memo follows visible inherited session, not nonmember terminal maps', () => {
|
||||
const visibleMemberChat = session({
|
||||
id: 'chat-visible',
|
||||
scope: { type: 'terminal', targetId: 'terminal-b', hostIds: ['host-b'] },
|
||||
@@ -364,7 +364,7 @@ test('workspace AI panel memo follows visible inherited session, not hidden term
|
||||
},
|
||||
],
|
||||
activeSessionIdMap: {
|
||||
'terminal:terminal-a': 'chat-hidden',
|
||||
'terminal:terminal-outside': 'chat-hidden',
|
||||
'terminal:terminal-b': 'chat-visible',
|
||||
},
|
||||
sessions: [visibleMemberChat, hiddenFocusedChat],
|
||||
@@ -380,3 +380,39 @@ test('workspace AI panel memo follows visible inherited session, not hidden term
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('merged AI panel displays a member chat resumed from an older terminal', () => {
|
||||
const resumed = session({
|
||||
id: 'resumed-chat',
|
||||
scope: { type: 'terminal', targetId: 'closed-terminal', hostIds: ['host-a'] },
|
||||
messages: [{ id: 'm1', role: 'user', content: 'keep-resumed-conversation', timestamp: 1 }],
|
||||
});
|
||||
const props = baseProps({
|
||||
isVisible: true,
|
||||
scopeType: 'workspace',
|
||||
scopeTargetId: 'merged',
|
||||
focusedSessionId: 'terminal-a',
|
||||
scopeHostIds: ['host-a'],
|
||||
terminalSessions: [{ sessionId: 'terminal-a', hostId: 'host-a', hostname: 'a', label: 'A', connected: true }],
|
||||
sessions: [resumed],
|
||||
activeSessionIdMap: { 'terminal:terminal-a': resumed.id },
|
||||
});
|
||||
const render = (panelProps: AIChatSidePanelProps) => renderToStaticMarkup(
|
||||
React.createElement(I18nProvider, { locale: 'en' },
|
||||
React.createElement(TooltipProvider, null,
|
||||
React.createElement(AIChatSidePanel, panelProps))),
|
||||
);
|
||||
// Check both the first workspace paint and the explicit merge handoff.
|
||||
assert.match(render(props), /keep-resumed-conversation/);
|
||||
assert.match(render({
|
||||
...props,
|
||||
activeSessionIdMap: { ...props.activeSessionIdMap, 'workspace:merged': resumed.id },
|
||||
panelViewByScope: { 'workspace:merged': { mode: 'session', sessionId: resumed.id } },
|
||||
}), /keep-resumed-conversation/);
|
||||
assert.equal(aiChatSidePanelPropsAreEqual(props, {
|
||||
...props,
|
||||
sessions: [{ ...resumed, messages: [...resumed.messages, {
|
||||
id: 'm2', role: 'assistant', content: 'continued response', timestamp: 2,
|
||||
}] }],
|
||||
}), false);
|
||||
});
|
||||
@@ -374,9 +374,10 @@ const AIChatSidePanelActive: React.FC<AIChatSidePanelProps> = ({
|
||||
scopeHostIds,
|
||||
activeTerminalSessionIds,
|
||||
workspaceMemberTerminalIds,
|
||||
activeSessionIdMap,
|
||||
),
|
||||
),
|
||||
[sessions, scopeType, scopeTargetId, scopeHostIds, activeTerminalSessionIds, workspaceMemberTerminalIds],
|
||||
[sessions, scopeType, scopeTargetId, scopeHostIds, activeTerminalSessionIds, workspaceMemberTerminalIds, activeSessionIdMap],
|
||||
);
|
||||
|
||||
const explicitPanelView = panelViewByScope[scopeKey];
|
||||
@@ -1850,6 +1851,7 @@ export function aiChatSidePanelPropsAreEqual(
|
||||
props.scopeHostIds,
|
||||
activeTerminalSessionIds,
|
||||
workspaceMemberTerminalIds,
|
||||
props.activeSessionIdMap,
|
||||
).map((session) => session.id),
|
||||
);
|
||||
return resolveInheritedAIActiveSessionId({
|
||||
|
||||
@@ -173,3 +173,52 @@ test("terminal scoped history cache has a hard LRU bound", () => {
|
||||
}
|
||||
assert.ok(_getScopedHistoryCacheSizeForTests(sessions) <= 64);
|
||||
});
|
||||
|
||||
test("workspace history retains chats resumed by members from older terminals", () => {
|
||||
const resumed = createSession("resumed", {
|
||||
type: "terminal", targetId: "closed-terminal", hostIds: ["host-a"],
|
||||
}, 1);
|
||||
const unrelated = createSession("unrelated", {
|
||||
type: "terminal", targetId: "another-closed-terminal", hostIds: ["host-a"],
|
||||
}, 2);
|
||||
const staleWorkspace = createSession("stale-workspace", {
|
||||
type: "workspace", targetId: "old-workspace",
|
||||
}, 99);
|
||||
const sessions = [staleWorkspace, unrelated, resumed];
|
||||
const members = new Set(["terminal-a", "terminal-b"]);
|
||||
const selected = {
|
||||
"terminal:terminal-a": "resumed",
|
||||
"terminal:terminal-outside": "unrelated",
|
||||
};
|
||||
|
||||
assert.ok(getScopedHistorySessions(
|
||||
sessions, "terminal", "terminal-a", ["host-a"], new Set(["unrelated"]),
|
||||
).includes(resumed));
|
||||
assert.deepEqual(getScopedHistorySessions(
|
||||
sessions, "workspace", "merged", ["host-a"], new Set(Object.values(selected)),
|
||||
members, selected,
|
||||
), [resumed, staleWorkspace]);
|
||||
// Returning to A makes the same stored conversation available again.
|
||||
assert.ok(getScopedHistorySessions(
|
||||
sessions, "terminal", "terminal-a", ["host-a"], new Set(["unrelated"]),
|
||||
).includes(resumed));
|
||||
assert.equal(resumed.scope.targetId, "closed-terminal");
|
||||
});
|
||||
|
||||
test("workspace history cache tracks member selections but ignores unrelated selections", () => {
|
||||
const sessions = [createSession("resumed", { type: "terminal", targetId: "closed" }, 1)];
|
||||
const members = new Set(["terminal-a"]);
|
||||
const history = (selected: Record<string, string | null>) => getScopedHistorySessions(
|
||||
sessions, "workspace", "merged", undefined, new Set(), members, selected,
|
||||
);
|
||||
const empty = history({});
|
||||
assert.deepEqual(empty, []);
|
||||
const inherited = history({ "terminal:terminal-a": "resumed" });
|
||||
assert.deepEqual(inherited, sessions);
|
||||
assert.equal(history({
|
||||
"terminal:terminal-a": "resumed", "terminal:outside": "other",
|
||||
}), inherited);
|
||||
assert.equal(history({ "terminal:terminal-a": null }), empty);
|
||||
assert.equal(history({ "workspace:unrelated": "resumed" }), empty);
|
||||
assert.equal(_getScopedHistoryCacheSizeForTests(sessions), 2);
|
||||
});
|
||||
@@ -11,6 +11,7 @@ function buildHistoryCacheKey(
|
||||
scopeHostIds: string[] | undefined,
|
||||
activeTerminalSessionIds: Set<string>,
|
||||
workspaceMemberTerminalIds: Set<string> | undefined,
|
||||
workspaceMemberActiveSessionIds: Set<string>,
|
||||
): HistoryCacheKey {
|
||||
const hostKey = scopeHostIds ? [...scopeHostIds].sort().join(',') : '';
|
||||
const terminalKey = scopeType === 'terminal'
|
||||
@@ -19,7 +20,8 @@ function buildHistoryCacheKey(
|
||||
const memberKey = scopeType === 'workspace' && workspaceMemberTerminalIds
|
||||
? [...workspaceMemberTerminalIds].sort().join(',')
|
||||
: '';
|
||||
return `${scopeType}:${scopeTargetId ?? ''}:${hostKey}:${terminalKey}:${memberKey}`;
|
||||
const memberActiveKey = [...workspaceMemberActiveSessionIds].sort().join(',');
|
||||
return `${scopeType}:${scopeTargetId ?? ''}:${hostKey}:${terminalKey}:${memberKey}:${memberActiveKey}`;
|
||||
}
|
||||
|
||||
export function getScopedHistorySessions(
|
||||
@@ -29,7 +31,18 @@ export function getScopedHistorySessions(
|
||||
scopeHostIds: string[] | undefined,
|
||||
activeTerminalSessionIds: Set<string>,
|
||||
workspaceMemberTerminalIds?: Set<string>,
|
||||
activeSessionIdMap?: Readonly<Record<string, string | null | undefined>>,
|
||||
): AISession[] {
|
||||
// A member can be continuing history created on an older terminal. Its
|
||||
// selected chat belongs in workspace history even though scope.targetId
|
||||
// still identifies that older terminal. Do not include nonmember selections.
|
||||
const workspaceMemberActiveSessionIds = new Set<string>();
|
||||
if (scopeType === 'workspace' && workspaceMemberTerminalIds && activeSessionIdMap) {
|
||||
for (const terminalId of workspaceMemberTerminalIds) {
|
||||
const sessionId = activeSessionIdMap[`terminal:${terminalId}`];
|
||||
if (sessionId) workspaceMemberActiveSessionIds.add(sessionId);
|
||||
}
|
||||
}
|
||||
let scopeCache = historyCache.get(sessions);
|
||||
if (!scopeCache) {
|
||||
scopeCache = new Map();
|
||||
@@ -42,6 +55,7 @@ export function getScopedHistorySessions(
|
||||
scopeHostIds,
|
||||
activeTerminalSessionIds,
|
||||
workspaceMemberTerminalIds,
|
||||
workspaceMemberActiveSessionIds,
|
||||
);
|
||||
const cached = scopeCache.get(cacheKey);
|
||||
if (cached) {
|
||||
@@ -60,6 +74,7 @@ export function getScopedHistorySessions(
|
||||
scopeHostIds,
|
||||
activeTerminalSessionIds,
|
||||
workspaceMemberTerminalIds,
|
||||
workspaceMemberActiveSessionIds,
|
||||
),
|
||||
}))
|
||||
.filter(({ matchRank }) => matchRank > 0)
|
||||
|
||||
@@ -18,15 +18,19 @@ export function getSessionScopeMatchRank(
|
||||
* member chats as exact workspace matches keeps history/resume working.
|
||||
*/
|
||||
workspaceMemberTerminalIds?: Set<string>,
|
||||
/** Chats currently selected by workspace members, including resumed history. */
|
||||
workspaceMemberActiveSessionIds?: Set<string>,
|
||||
): number {
|
||||
// After a terminal merge the AI panel flips to workspace scope, but chats
|
||||
// created on the member terminals remain terminal-scoped. Rank those as
|
||||
// created or resumed on member terminals remain terminal-scoped. Rank them as
|
||||
// exact matches so they stay visible and preferred over stale workspaces.
|
||||
if (
|
||||
scopeType === "workspace"
|
||||
&& session.scope.type === "terminal"
|
||||
&& session.scope.targetId
|
||||
&& workspaceMemberTerminalIds?.has(session.scope.targetId)
|
||||
&& (
|
||||
(session.scope.targetId && workspaceMemberTerminalIds?.has(session.scope.targetId))
|
||||
|| workspaceMemberActiveSessionIds?.has(session.id)
|
||||
)
|
||||
) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user