From 0326da08061727cd797b88d7148020340b6a01ce Mon Sep 17 00:00:00 2001 From: DarkSky <25152247+darkskygit@users.noreply.github.com> Date: Tue, 1 Jul 2025 19:31:37 +0800 Subject: [PATCH] feat(server): add typed list session gql (#12979) ## Summary by CodeRabbit * **New Features** * Introduced new API endpoints and GraphQL queries to retrieve Copilot chat sessions by workspace, document, and pinned status, with detailed session and message information. * Added support for filtering and querying Copilot chat histories with new options such as pinned status and message ordering. * **Bug Fixes** * Improved filtering logic for listing and retrieving chat sessions, ensuring accurate results for workspace, document, and pinned session queries. * **Tests** * Expanded and refactored test coverage for session listing, filtering, and new query options to ensure reliability and correctness of Copilot session retrieval. * Updated snapshot data to reflect new session types and filtering capabilities. --- .../__tests__/__snapshots__/copilot.e2e.ts.md | 2 + .../__snapshots__/copilot.e2e.ts.snap | Bin 1110 -> 1127 bytes .../server/src/__tests__/copilot.e2e.ts | 100 +++++++-- .../__snapshots__/copilot-session.spec.ts.md | 196 ++++++++---------- .../copilot-session.spec.ts.snap | Bin 3763 -> 3633 bytes .../__tests__/models/copilot-session.spec.ts | 6 + .../server/src/__tests__/utils/copilot.ts | 171 ++++++++++++++- .../server/src/models/copilot-session.ts | 40 ++-- .../copilot-history-list-doc-sessions.gql | 32 +++ .../copilot-history-list-pinned-session.gql | 38 ++++ ...opilot-history-list-workspace-sessions.gql | 31 +++ packages/common/graphql/src/graphql/index.ts | 102 +++++++++ packages/common/graphql/src/schema.ts | 138 ++++++++++++ 13 files changed, 704 insertions(+), 152 deletions(-) create mode 100644 packages/common/graphql/src/graphql/copilot-history-list-doc-sessions.gql create mode 100644 packages/common/graphql/src/graphql/copilot-history-list-pinned-session.gql create mode 100644 packages/common/graphql/src/graphql/copilot-history-list-workspace-sessions.gql diff --git a/packages/backend/server/src/__tests__/__snapshots__/copilot.e2e.ts.md b/packages/backend/server/src/__tests__/__snapshots__/copilot.e2e.ts.md index 9ce1e157b4..1f1840ff67 100644 --- a/packages/backend/server/src/__tests__/__snapshots__/copilot.e2e.ts.md +++ b/packages/backend/server/src/__tests__/__snapshots__/copilot.e2e.ts.md @@ -16,6 +16,7 @@ Generated by [AVA](https://avajs.dev). role: 'assistant', }, ], + pinned: false, tokens: 8, }, ] @@ -30,6 +31,7 @@ Generated by [AVA](https://avajs.dev). role: 'assistant', }, ], + pinned: false, tokens: 8, }, ] diff --git a/packages/backend/server/src/__tests__/__snapshots__/copilot.e2e.ts.snap b/packages/backend/server/src/__tests__/__snapshots__/copilot.e2e.ts.snap index b70f3bf91e0d70e880dfead62afd653117f66233..a668f573a699d225d4f279147f21570967b177b9 100644 GIT binary patch literal 1127 zcmV-t1ep6lRzVv*{&%AOqBT{STl!IKxogG4W;d%I_y zp3TlIyCI3}T&AkKUiEvQdhgp+eYw+)RnRjVSA>mhEY_Lns(u`@wqU#+3uYD5!s-FL z7}+l4X%rdBQ`1$}ynZDNx#cg?TP|jh{isgP0)WQ=oS@oq^3Q6`2SjCSZf*{A1Vo?) z(V5c7>jAg`;9US;md3Sb+N&b45V!Z-7hnD1waDe{mHDVspdpOPPI@4YQI6$ z41fWEuK`>IFiXIhs+5ceq9=I39{_uTtB0nYlG+~LJqr5zV84H2YA#0o^^mVXLqHplmC?i zY&yWt4)B))oOCND{zP?hg&dY#z1du9HkVF4F@`o3yeD+or?XXz#&T^d(>&g+j2h)u zUY%bSajX`YSQC1nq*$EJ&DK(@I{%oPPfg?Isnb)rxv(59Fx%y_#|Em;I!d!u6=fC4 zY*(;E2%EQ6$Onr>kv>oqX`ZmV*XaL3XZ%+Vb>^n(jQ<`19}w^<0bdZXIZ1E4obUHT zz4^V+n+DPS4oW?GK5M%96W(@!OAhdj16*~0zbC6u(*;huz)LRhnhRW>ph9)>|8#+C zE^yKVp7emwtC-ZrOg{F2&pqG=5BSvs8h*uO=1}Wyh1N|KLhE2iw{9}&*1?c&-3D)! zVz+y)9IQpAH(}K22rZ;#CfhYD$?PB%hH)9PH6BM{HYn99N%PIKv6@HHn(b}z8^!DD z&0F@m8aXW&NS+>zvWzYNqBDByqS8H+@}M#(*Ws=^XX-j6|ke)icuH({q|ynGCy^-A;@s3ELKWN)!@rqzmvurE006SvC*S}8 literal 1110 zcmV-c1gZN$RzVa68AVvsm@Sq@w zA9$$guGy)b?y9D$X0xL=T@*zOh+b6C1jLJYSPve=no z%(5F2C3Bgoo_f{q{a(F)Rn3)lE74KcZeJBnxCg(s<=3Tyegu%HE;BCmX2bGj>( z&AS2BbMJlt@MFb0e{b)88F?2H&>-MRs(2Ud<=vPMbCFg~C|CCHUKdIj?gVpU%MIub zzAHw7(TONK^W0i#9al~yyd{#YhbA+WNb6WfTBl8)@wO9Yn6x_yy4@3W^yCKB6t)uGx)_gWwq+tN#?8v8`KZ%V-RI=1Ludi2 zJQYtzJdatPW{FtN;&%0A)^XRfqk);xk+4>(^@9Ap1;!kAL5>YvvO{Pud~;-l@ZL;? zFy_7|N1ST0jwUfpjqjel;)``;%XM_$IGEK3GIyd#zMD#(S|Gi$g26p<6{A`1-Ix)GUBLfUf{t129j(xvG>*1{GGNghvD4 z?17}DN+L?QOhsIs+V;|voBn~O4)ZlJ&Ey2p%e1T2$HzG5OMaVxcm5gj0Zox*FX#Fx zaxoF$t%tlP}b$05O z|3YWNSMKP{P1Tw3T>{=G;1dGAAYf;j-UK<{??!s_d!aWCqWe9Rdi4CD>E~~F+XF6p zz}Fsd%>(|Pu0kt5aMlN2@`2ZU;K~#gs#Exf5B%i=rvu=L0EmN%No~U9qX76U0KN}^ z-vXc!R!rvZXx**Qx|u>~9u4W{O(xwu8q&?%U{@(lyBEsAMhx^OmhHAMLOEs!r)I5~ z>u16;u41;y6B!Q%Wk#o2et5RdaH*W#Jr=)FY*%mIvh6C9N{M0Cyu|{^)8kQ=vE~2A zjQ?~=o371xR2h`(@G$Noz?~zz27P$&YfzZqeReN7dB_jPI@%A$LIGoKW*9^3{}KtM cP0EvLB+6xRVP+Dsj&6wII;eZZVE_~W0RCYjVgLXD diff --git a/packages/backend/server/src/__tests__/copilot.e2e.ts b/packages/backend/server/src/__tests__/copilot.e2e.ts index 3de76d525c..d47e1f48f9 100644 --- a/packages/backend/server/src/__tests__/copilot.e2e.ts +++ b/packages/backend/server/src/__tests__/copilot.e2e.ts @@ -53,7 +53,10 @@ import { createWorkspaceCopilotSession, forkCopilotSession, getCopilotSession, + getDocSessions, getHistories, + getPinnedSessions, + getWorkspaceSessions, listContext, listContextDocAndFiles, matchFiles, @@ -1140,31 +1143,94 @@ test('should list histories for different session types correctly', async t => { ]); const testHistoryQuery = async ( - queryDocId: string | undefined, - expectedSessionId: string, + queryFn: () => Promise, + opts: { + sessionIds?: string[]; + sessionId?: string; + pinned?: boolean; + isEmpty?: boolean; + }, description: string ) => { - const histories = await getHistories(app, { - workspaceId, - docId: queryDocId, - }); - t.is(histories.length, 1, `should return ${description}`); - t.is( - histories[0].sessionId, - expectedSessionId, - `should return correct ${description}` - ); + const s = await queryFn(); + + if (opts.isEmpty) { + t.is(s.length, 0, `should return ${description}`); + return; + } + + if (opts.sessionIds) { + t.is(s.length, opts.sessionIds.length, `should return ${description}`); + const ids = s.map(h => h.sessionId).sort((a, b) => a.localeCompare(b)); + const expectedIds = opts.sessionIds.sort((a, b) => a.localeCompare(b)); + t.deepEqual(ids, expectedIds, `should return correct ${description}`); + } else if (opts.sessionId) { + t.is(s.length, 1, `should return ${description}`); + t.is( + s[0].sessionId, + opts.sessionId, + `should return correct ${description}` + ); + if (opts.pinned !== undefined) { + t.is(s[0].pinned, opts.pinned, `pinned status for ${description}`); + } + } }; + // test for getHistories await testHistoryQuery( - undefined, - workspaceSessionId, + () => getHistories(app, { workspaceId, docId: null }), + { sessionId: workspaceSessionId }, 'workspace session history' ); await testHistoryQuery( - pinnedDocId, - pinnedSessionId, + () => getHistories(app, { workspaceId, docId: pinnedDocId }), + { sessionId: pinnedSessionId }, 'pinned session history' ); - await testHistoryQuery(docId, docSessionId, 'doc session history'); + await testHistoryQuery( + () => getHistories(app, { workspaceId, docId }), + { sessionId: docSessionId }, + 'doc session history' + ); + + // test for getWorkspaceSessions + await testHistoryQuery( + () => getWorkspaceSessions(app, { workspaceId }), + { sessionId: workspaceSessionId, pinned: false }, + 'workspace-level sessions' + ); + + // test for getDocSessions + await testHistoryQuery( + () => + getDocSessions(app, { workspaceId, docId, options: { pinned: false } }), + { sessionId: docSessionId, pinned: false }, + 'doc sessions' + ); + + await testHistoryQuery( + () => getDocSessions(app, { workspaceId, docId: pinnedDocId }), + { sessionId: pinnedSessionId, pinned: true }, + 'pinned doc sessions' + ); + + // test for getPinnedSessions + await testHistoryQuery( + () => getPinnedSessions(app, { workspaceId }), + { sessionId: pinnedSessionId, pinned: true }, + 'pinned sessions' + ); + + await testHistoryQuery( + () => getPinnedSessions(app, { workspaceId, docId: pinnedDocId }), + { sessionId: pinnedSessionId, pinned: true }, + 'pinned session for specific doc' + ); + + await testHistoryQuery( + () => getPinnedSessions(app, { workspaceId, docId }), + { isEmpty: true }, + 'no pinned sessions for non-pinned doc' + ); }); diff --git a/packages/backend/server/src/__tests__/models/__snapshots__/copilot-session.spec.ts.md b/packages/backend/server/src/__tests__/models/__snapshots__/copilot-session.spec.ts.md index 45d7f5da75..0324d0b5a1 100644 --- a/packages/backend/server/src/__tests__/models/__snapshots__/copilot-session.spec.ts.md +++ b/packages/backend/server/src/__tests__/models/__snapshots__/copilot-session.spec.ts.md @@ -262,16 +262,53 @@ Generated by [AVA](https://avajs.dev). { all_workspace_sessions: { - count: 2, + count: 7, sessionTypes: [ { hasMessages: false, + isAction: false, + isFork: true, + messageCount: 0, + type: 'doc', + }, + { + hasMessages: false, + isAction: false, + isFork: false, + messageCount: 0, + type: 'doc', + }, + { + hasMessages: false, + isAction: false, + isFork: false, + messageCount: 0, + type: 'doc', + }, + { + hasMessages: false, + isAction: true, + isFork: false, + messageCount: 0, + type: 'doc', + }, + { + hasMessages: false, + isAction: false, + isFork: false, + messageCount: 0, + type: 'doc', + }, + { + hasMessages: false, + isAction: false, isFork: false, messageCount: 0, type: 'pinned', }, { hasMessages: false, + isAction: false, isFork: false, messageCount: 0, type: 'workspace', @@ -283,30 +320,35 @@ Generated by [AVA](https://avajs.dev). sessionTypes: [ { hasMessages: false, + isAction: false, isFork: true, messageCount: 0, type: 'doc', }, { hasMessages: true, + isAction: false, isFork: false, messageCount: 1, type: 'doc', }, { hasMessages: true, + isAction: false, isFork: false, messageCount: 1, type: 'doc', }, { hasMessages: false, + isAction: true, isFork: false, messageCount: 0, type: 'doc', }, { hasMessages: true, + isAction: false, isFork: false, messageCount: 1, type: 'doc', @@ -318,6 +360,7 @@ Generated by [AVA](https://avajs.dev). sessionTypes: [ { hasMessages: false, + isAction: false, isFork: false, messageCount: 0, type: 'doc', @@ -325,28 +368,39 @@ Generated by [AVA](https://avajs.dev). ], }, non_action_sessions: { - count: 4, + count: 5, sessionTypes: [ { hasMessages: false, + isAction: false, isFork: true, messageCount: 0, type: 'doc', }, { hasMessages: false, + isAction: false, isFork: false, messageCount: 0, type: 'doc', }, { hasMessages: false, + isAction: false, isFork: false, messageCount: 0, type: 'doc', }, { hasMessages: false, + isAction: true, + isFork: false, + messageCount: 0, + type: 'doc', + }, + { + hasMessages: false, + isAction: false, isFork: false, messageCount: 0, type: 'doc', @@ -354,28 +408,25 @@ Generated by [AVA](https://avajs.dev). ], }, non_fork_sessions: { - count: 4, + count: 3, sessionTypes: [ { hasMessages: false, + isAction: false, isFork: false, messageCount: 0, type: 'doc', }, { hasMessages: false, + isAction: false, isFork: false, messageCount: 0, type: 'doc', }, { hasMessages: false, - isFork: false, - messageCount: 0, - type: 'doc', - }, - { - hasMessages: false, + isAction: false, isFork: false, messageCount: 0, type: 'doc', @@ -383,16 +434,44 @@ Generated by [AVA](https://avajs.dev). ], }, recent_top3_sessions: { + count: 3, + sessionTypes: [ + { + hasMessages: false, + isAction: false, + isFork: true, + messageCount: 0, + type: 'doc', + }, + { + hasMessages: false, + isAction: false, + isFork: false, + messageCount: 0, + type: 'doc', + }, + { + hasMessages: false, + isAction: false, + isFork: false, + messageCount: 0, + type: 'doc', + }, + ], + }, + workspace_sessions_with_messages: { count: 2, sessionTypes: [ { hasMessages: false, + isAction: false, isFork: false, messageCount: 0, type: 'pinned', }, { hasMessages: false, + isAction: false, isFork: false, messageCount: 0, type: 'workspace', @@ -486,102 +565,3 @@ Generated by [AVA](https://avajs.dev). workspaceSessionExists: true, }, } - -## should handle session updates and validations - -> should unpin existing when pinning new session - - [ - { - docId: null, - id: 'session-update-id', - pinned: true, - }, - { - docId: null, - id: 'existing-pinned-session-id', - pinned: false, - }, - ] - -> session type conversion steps - - [ - { - session: { - docId: 'doc-update-id', - pinned: false, - }, - step: 'workspace_to_doc', - type: 'doc', - }, - { - session: { - docId: null, - pinned: false, - }, - step: 'doc_to_workspace', - type: 'workspace', - }, - { - session: { - docId: null, - pinned: true, - }, - step: 'workspace_to_pinned', - type: 'pinned', - }, - ] - -## should create multiple doc sessions and query latest - -> multiple doc sessions for same document with order verification - - [ - { - docId: 'multi-session-doc', - hasMessages: true, - isFirstSession: false, - isSecondSession: false, - isThirdSession: true, - messageCount: 1, - }, - { - docId: 'multi-session-doc', - hasMessages: true, - isFirstSession: false, - isSecondSession: true, - isThirdSession: false, - messageCount: 1, - }, - { - docId: 'multi-session-doc', - hasMessages: true, - isFirstSession: true, - isSecondSession: false, - isThirdSession: false, - messageCount: 1, - }, - ] - -## should query recent topK sessions of different types - -> should include different session types in recent topK query - - [ - { - docId: null, - pinned: false, - type: 'workspace', - }, - { - docId: null, - pinned: true, - type: 'pinned', - }, - { - docId: null, - pinned: false, - type: 'workspace', - }, - ] diff --git a/packages/backend/server/src/__tests__/models/__snapshots__/copilot-session.spec.ts.snap b/packages/backend/server/src/__tests__/models/__snapshots__/copilot-session.spec.ts.snap index 385eed31663ae2a644766b9731ad639b2ce14f0f..09fa4b095b1fa40f791019495597b93fd41f2212 100644 GIT binary patch literal 3633 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 DPj(_6 literal 3763 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 diff --git a/packages/backend/server/src/__tests__/models/copilot-session.spec.ts b/packages/backend/server/src/__tests__/models/copilot-session.spec.ts index 5586dfc512..32ac7ffee7 100644 --- a/packages/backend/server/src/__tests__/models/copilot-session.spec.ts +++ b/packages/backend/server/src/__tests__/models/copilot-session.spec.ts @@ -169,6 +169,7 @@ test('should list and filter session type', async t => { const workspaceSessions = await copilotSession.list({ userId: user.id, workspaceId: workspace.id, + docId: null, }); t.snapshot( @@ -575,6 +576,10 @@ test('should handle session queries, ordering, and filtering', async t => { const docParams = { ...baseParams, docId }; const queryTestCases = [ { name: 'all_workspace_sessions', params: baseParams }, + { + name: 'workspace_sessions_with_messages', + params: { ...baseParams, docId: null, withMessages: true }, + }, { name: 'doc_sessions_with_messages', params: { ...docParams, withMessages: true }, @@ -609,6 +614,7 @@ test('should handle session queries, ordering, and filtering', async t => { type: copilotSession.getSessionType(s), hasMessages: !!s.messages?.length, messageCount: s.messages?.length || 0, + isAction: s.promptName === TEST_PROMPTS.ACTION, isFork: !!s.parentSessionId, })), }; diff --git a/packages/backend/server/src/__tests__/utils/copilot.ts b/packages/backend/server/src/__tests__/utils/copilot.ts index 30009e8516..5b4019d8ac 100644 --- a/packages/backend/server/src/__tests__/utils/copilot.ts +++ b/packages/backend/server/src/__tests__/utils/copilot.ts @@ -709,26 +709,30 @@ type ChatMessage = { type History = { sessionId: string; + pinned: boolean; tokens: number; action: string | null; createdAt: string; messages: ChatMessage[]; }; +type HistoryOptions = { + action?: boolean; + fork?: boolean; + pinned?: boolean; + limit?: number; + skip?: number; + sessionOrder?: 'asc' | 'desc'; + messageOrder?: 'asc' | 'desc'; + sessionId?: string; +}; + export async function getHistories( app: TestingApp, variables: { workspaceId: string; - docId?: string; - options?: { - action?: boolean; - fork?: boolean; - limit?: number; - skip?: number; - sessionOrder?: 'asc' | 'desc'; - messageOrder?: 'asc' | 'desc'; - sessionId?: string; - }; + docId?: string | null; + options?: HistoryOptions; } ): Promise { const res = await app.gql( @@ -742,6 +746,7 @@ export async function getHistories( copilot(workspaceId: $workspaceId) { histories(docId: $docId, options: $options) { sessionId + pinned tokens action createdAt @@ -763,6 +768,152 @@ export async function getHistories( return res.currentUser?.copilot?.histories || []; } +export async function getWorkspaceSessions( + app: TestingApp, + variables: { + workspaceId: string; + options?: HistoryOptions; + } +): Promise { + const res = await app.gql( + `query getCopilotWorkspaceSessions( + $workspaceId: String! + $options: QueryChatHistoriesInput + ) { + currentUser { + copilot(workspaceId: $workspaceId) { + histories(docId: null, options: $options) { + sessionId + pinned + tokens + action + createdAt + messages { + id + role + content + streamObjects { + type + textDelta + toolCallId + toolName + args + result + } + attachments + createdAt + } + } + } + } + }`, + variables + ); + + return res.currentUser?.copilot?.histories || []; +} + +export async function getDocSessions( + app: TestingApp, + variables: { + workspaceId: string; + docId: string; + options?: HistoryOptions; + } +): Promise { + const res = await app.gql( + `query getCopilotDocSessions( + $workspaceId: String! + $docId: String! + $options: QueryChatHistoriesInput + ) { + currentUser { + copilot(workspaceId: $workspaceId) { + histories(docId: $docId, options: $options) { + sessionId + pinned + tokens + action + createdAt + messages { + id + role + content + streamObjects { + type + textDelta + toolCallId + toolName + args + result + } + attachments + createdAt + } + } + } + } + }`, + variables + ); + + return res.currentUser?.copilot?.histories || []; +} + +export async function getPinnedSessions( + app: TestingApp, + variables: { + workspaceId: string; + docId?: string; + messageOrder?: 'asc' | 'desc'; + withPrompt?: boolean; + } +): Promise { + const res = await app.gql( + `query getCopilotPinnedSessions( + $workspaceId: String! + $docId: String + $messageOrder: ChatHistoryOrder + $withPrompt: Boolean + ) { + currentUser { + copilot(workspaceId: $workspaceId) { + histories(docId: $docId, options: { + limit: 1, + pinned: true, + messageOrder: $messageOrder, + withPrompt: $withPrompt + }) { + sessionId + pinned + tokens + action + createdAt + messages { + id + role + content + streamObjects { + type + textDelta + toolCallId + toolName + args + result + } + attachments + createdAt + } + } + } + } + }`, + variables + ); + + return res.currentUser?.copilot?.histories || []; +} + type Prompt = { name: string; model: string; diff --git a/packages/backend/server/src/models/copilot-session.ts b/packages/backend/server/src/models/copilot-session.ts index a10f68fb16..1d09605800 100644 --- a/packages/backend/server/src/models/copilot-session.ts +++ b/packages/backend/server/src/models/copilot-session.ts @@ -285,38 +285,44 @@ export class CopilotSessionModel extends BaseModel { } async list(options: ListSessionOptions) { - const { userId, sessionId, workspaceId, docId } = options; + const { userId, sessionId, workspaceId, docId, action, fork } = options; + + function getNullCond( + maybeBool: boolean | undefined, + wrap: (ret: { not: null } | null) => T = ret => ret as T + ): T | undefined { + return maybeBool === true + ? wrap({ not: null }) + : maybeBool === false + ? wrap(null) + : undefined; + } + + function getEqCond(maybeValue: T | undefined): T | undefined { + return maybeValue !== undefined ? maybeValue : undefined; + } const conditions: Prisma.AiSessionWhereInput['OR'] = [ { userId, workspaceId, - docId: docId ?? null, - id: sessionId ? { equals: sessionId } : undefined, + docId: getEqCond(docId), + id: getEqCond(sessionId), deletedAt: null, - prompt: - typeof options.action === 'boolean' - ? options.action - ? { action: { not: null } } - : { action: null } - : undefined, - parentSessionId: - typeof options.fork === 'boolean' - ? options.fork - ? { not: null } - : null - : undefined, + pinned: getEqCond(options.pinned), + prompt: getNullCond(fork, ret => ({ action: ret })), + parentSessionId: getNullCond(fork), }, ]; - if (!options?.action && options?.fork) { + if (!action && fork) { // query forked sessions from other users // only query forked session if fork == true and action == false conditions.push({ userId: { not: userId }, workspaceId: workspaceId, docId: docId ?? null, - id: sessionId ? { equals: sessionId } : undefined, + id: getEqCond(sessionId), prompt: { action: null }, // should only find forked session parentSessionId: { not: null }, diff --git a/packages/common/graphql/src/graphql/copilot-history-list-doc-sessions.gql b/packages/common/graphql/src/graphql/copilot-history-list-doc-sessions.gql new file mode 100644 index 0000000000..cb57c859c4 --- /dev/null +++ b/packages/common/graphql/src/graphql/copilot-history-list-doc-sessions.gql @@ -0,0 +1,32 @@ +query getCopilotDocSessions( + $workspaceId: String! + $docId: String! + $options: QueryChatHistoriesInput +) { + currentUser { + copilot(workspaceId: $workspaceId) { + histories(docId: $docId, options: $options) { + sessionId + pinned + tokens + action + createdAt + messages { + id + role + content + streamObjects { + type + textDelta + toolCallId + toolName + args + result + } + attachments + createdAt + } + } + } + } +} diff --git a/packages/common/graphql/src/graphql/copilot-history-list-pinned-session.gql b/packages/common/graphql/src/graphql/copilot-history-list-pinned-session.gql new file mode 100644 index 0000000000..6c390721e8 --- /dev/null +++ b/packages/common/graphql/src/graphql/copilot-history-list-pinned-session.gql @@ -0,0 +1,38 @@ +query getCopilotPinnedSessions( + $workspaceId: String! + $docId: String + $messageOrder: ChatHistoryOrder + $withPrompt: Boolean +) { + currentUser { + copilot(workspaceId: $workspaceId) { + histories(docId: $docId, options: { + limit: 1, + pinned: true, + messageOrder: $messageOrder, + withPrompt: $withPrompt + }) { + sessionId + pinned + tokens + action + createdAt + messages { + id + role + content + streamObjects { + type + textDelta + toolCallId + toolName + args + result + } + attachments + createdAt + } + } + } + } +} diff --git a/packages/common/graphql/src/graphql/copilot-history-list-workspace-sessions.gql b/packages/common/graphql/src/graphql/copilot-history-list-workspace-sessions.gql new file mode 100644 index 0000000000..eaa74dd7a0 --- /dev/null +++ b/packages/common/graphql/src/graphql/copilot-history-list-workspace-sessions.gql @@ -0,0 +1,31 @@ +query getCopilotWorkspaceSessions( + $workspaceId: String! + $options: QueryChatHistoriesInput +) { + currentUser { + copilot(workspaceId: $workspaceId) { + histories(docId: null, options: $options) { + sessionId + pinned + tokens + action + createdAt + messages { + id + role + content + streamObjects { + type + textDelta + toolCallId + toolName + args + result + } + attachments + createdAt + } + } + } + } +} diff --git a/packages/common/graphql/src/graphql/index.ts b/packages/common/graphql/src/graphql/index.ts index 8cb20a09e2..ba7be4e6e6 100644 --- a/packages/common/graphql/src/graphql/index.ts +++ b/packages/common/graphql/src/graphql/index.ts @@ -604,6 +604,108 @@ export const getCopilotHistoryIdsQuery = { }`, }; +export const getCopilotDocSessionsQuery = { + id: 'getCopilotDocSessionsQuery' as const, + op: 'getCopilotDocSessions', + query: `query getCopilotDocSessions($workspaceId: String!, $docId: String!, $options: QueryChatHistoriesInput) { + currentUser { + copilot(workspaceId: $workspaceId) { + histories(docId: $docId, options: $options) { + sessionId + pinned + tokens + action + createdAt + messages { + id + role + content + streamObjects { + type + textDelta + toolCallId + toolName + args + result + } + attachments + createdAt + } + } + } + } +}`, +}; + +export const getCopilotPinnedSessionsQuery = { + id: 'getCopilotPinnedSessionsQuery' as const, + op: 'getCopilotPinnedSessions', + query: `query getCopilotPinnedSessions($workspaceId: String!, $docId: String, $messageOrder: ChatHistoryOrder, $withPrompt: Boolean) { + currentUser { + copilot(workspaceId: $workspaceId) { + histories( + docId: $docId + options: {limit: 1, pinned: true, messageOrder: $messageOrder, withPrompt: $withPrompt} + ) { + sessionId + pinned + tokens + action + createdAt + messages { + id + role + content + streamObjects { + type + textDelta + toolCallId + toolName + args + result + } + attachments + createdAt + } + } + } + } +}`, +}; + +export const getCopilotWorkspaceSessionsQuery = { + id: 'getCopilotWorkspaceSessionsQuery' as const, + op: 'getCopilotWorkspaceSessions', + query: `query getCopilotWorkspaceSessions($workspaceId: String!, $options: QueryChatHistoriesInput) { + currentUser { + copilot(workspaceId: $workspaceId) { + histories(docId: null, options: $options) { + sessionId + pinned + tokens + action + createdAt + messages { + id + role + content + streamObjects { + type + textDelta + toolCallId + toolName + args + result + } + attachments + createdAt + } + } + } + } +}`, +}; + export const getCopilotHistoriesQuery = { id: 'getCopilotHistoriesQuery' as const, op: 'getCopilotHistories', diff --git a/packages/common/graphql/src/schema.ts b/packages/common/graphql/src/schema.ts index 88d5342fc7..6da7294e84 100644 --- a/packages/common/graphql/src/schema.ts +++ b/packages/common/graphql/src/schema.ts @@ -3390,6 +3390,129 @@ export type GetCopilotHistoryIdsQuery = { } | null; }; +export type GetCopilotDocSessionsQueryVariables = Exact<{ + workspaceId: Scalars['String']['input']; + docId: Scalars['String']['input']; + options?: InputMaybe; +}>; + +export type GetCopilotDocSessionsQuery = { + __typename?: 'Query'; + currentUser: { + __typename?: 'UserType'; + copilot: { + __typename?: 'Copilot'; + histories: Array<{ + __typename?: 'CopilotHistories'; + sessionId: string; + pinned: boolean; + tokens: number; + action: string | null; + createdAt: string; + messages: Array<{ + __typename?: 'ChatMessage'; + id: string | null; + role: string; + content: string; + attachments: Array | null; + createdAt: string; + streamObjects: Array<{ + __typename?: 'StreamObject'; + type: string; + textDelta: string | null; + toolCallId: string | null; + toolName: string | null; + args: Record | null; + result: Record | null; + }> | null; + }>; + }>; + }; + } | null; +}; + +export type GetCopilotPinnedSessionsQueryVariables = Exact<{ + workspaceId: Scalars['String']['input']; + docId?: InputMaybe; + messageOrder?: InputMaybe; + withPrompt?: InputMaybe; +}>; + +export type GetCopilotPinnedSessionsQuery = { + __typename?: 'Query'; + currentUser: { + __typename?: 'UserType'; + copilot: { + __typename?: 'Copilot'; + histories: Array<{ + __typename?: 'CopilotHistories'; + sessionId: string; + pinned: boolean; + tokens: number; + action: string | null; + createdAt: string; + messages: Array<{ + __typename?: 'ChatMessage'; + id: string | null; + role: string; + content: string; + attachments: Array | null; + createdAt: string; + streamObjects: Array<{ + __typename?: 'StreamObject'; + type: string; + textDelta: string | null; + toolCallId: string | null; + toolName: string | null; + args: Record | null; + result: Record | null; + }> | null; + }>; + }>; + }; + } | null; +}; + +export type GetCopilotWorkspaceSessionsQueryVariables = Exact<{ + workspaceId: Scalars['String']['input']; + options?: InputMaybe; +}>; + +export type GetCopilotWorkspaceSessionsQuery = { + __typename?: 'Query'; + currentUser: { + __typename?: 'UserType'; + copilot: { + __typename?: 'Copilot'; + histories: Array<{ + __typename?: 'CopilotHistories'; + sessionId: string; + pinned: boolean; + tokens: number; + action: string | null; + createdAt: string; + messages: Array<{ + __typename?: 'ChatMessage'; + id: string | null; + role: string; + content: string; + attachments: Array | null; + createdAt: string; + streamObjects: Array<{ + __typename?: 'StreamObject'; + type: string; + textDelta: string | null; + toolCallId: string | null; + toolName: string | null; + args: Record | null; + result: Record | null; + }> | null; + }>; + }>; + }; + } | null; +}; + export type GetCopilotHistoriesQueryVariables = Exact<{ workspaceId: Scalars['String']['input']; docId?: InputMaybe; @@ -5252,6 +5375,21 @@ export type Queries = variables: GetCopilotHistoryIdsQueryVariables; response: GetCopilotHistoryIdsQuery; } + | { + name: 'getCopilotDocSessionsQuery'; + variables: GetCopilotDocSessionsQueryVariables; + response: GetCopilotDocSessionsQuery; + } + | { + name: 'getCopilotPinnedSessionsQuery'; + variables: GetCopilotPinnedSessionsQueryVariables; + response: GetCopilotPinnedSessionsQuery; + } + | { + name: 'getCopilotWorkspaceSessionsQuery'; + variables: GetCopilotWorkspaceSessionsQueryVariables; + response: GetCopilotWorkspaceSessionsQuery; + } | { name: 'getCopilotHistoriesQuery'; variables: GetCopilotHistoriesQueryVariables;