@wildix/wilma-copilot-headless 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/hooks/CopilotSessionStreamSlot.d.ts +18 -0
  2. package/dist/hooks/CopilotSessionStreamSlot.d.ts.map +1 -0
  3. package/dist/hooks/CopilotSessionStreamSlot.js +20 -0
  4. package/dist/hooks/CopilotSessionStreamSlot.js.map +1 -0
  5. package/dist/hooks/index.d.ts +10 -0
  6. package/dist/hooks/index.d.ts.map +1 -0
  7. package/dist/hooks/index.js +10 -0
  8. package/dist/hooks/index.js.map +1 -0
  9. package/dist/hooks/shared.d.ts +8 -0
  10. package/dist/hooks/shared.d.ts.map +1 -0
  11. package/dist/hooks/shared.js +5 -0
  12. package/dist/hooks/shared.js.map +1 -0
  13. package/dist/hooks/useCopilotActions.d.ts +36 -0
  14. package/dist/hooks/useCopilotActions.d.ts.map +1 -0
  15. package/dist/hooks/useCopilotActions.js +64 -0
  16. package/dist/hooks/useCopilotActions.js.map +1 -0
  17. package/dist/hooks/useCopilotAlerts.d.ts +24 -0
  18. package/dist/hooks/useCopilotAlerts.d.ts.map +1 -0
  19. package/dist/hooks/useCopilotAlerts.js +32 -0
  20. package/dist/hooks/useCopilotAlerts.js.map +1 -0
  21. package/dist/hooks/useCopilotAsk.d.ts +11 -0
  22. package/dist/hooks/useCopilotAsk.d.ts.map +1 -0
  23. package/dist/hooks/useCopilotAsk.js +31 -0
  24. package/dist/hooks/useCopilotAsk.js.map +1 -0
  25. package/dist/hooks/useCopilotPanelView.d.ts +48 -0
  26. package/dist/hooks/useCopilotPanelView.d.ts.map +1 -0
  27. package/dist/hooks/useCopilotPanelView.js +41 -0
  28. package/dist/hooks/useCopilotPanelView.js.map +1 -0
  29. package/dist/hooks/useCopilotRecentSessions.d.ts +12 -0
  30. package/dist/hooks/useCopilotRecentSessions.d.ts.map +1 -0
  31. package/dist/hooks/useCopilotRecentSessions.js +38 -0
  32. package/dist/hooks/useCopilotRecentSessions.js.map +1 -0
  33. package/dist/hooks/useCopilotRelationship.d.ts +10 -0
  34. package/dist/hooks/useCopilotRelationship.d.ts.map +1 -0
  35. package/dist/hooks/useCopilotRelationship.js +74 -0
  36. package/dist/hooks/useCopilotRelationship.js.map +1 -0
  37. package/dist/hooks/useCopilotSessionStream.d.ts +12 -0
  38. package/dist/hooks/useCopilotSessionStream.d.ts.map +1 -0
  39. package/dist/hooks/useCopilotSessionStream.js +61 -0
  40. package/dist/hooks/useCopilotSessionStream.js.map +1 -0
  41. package/package.json +5 -5
  42. package/src/hooks/CopilotSessionStreamSlot.tsx +28 -0
  43. package/src/hooks/index.ts +9 -0
  44. package/src/hooks/shared.ts +12 -0
  45. package/src/hooks/useCopilotActions.ts +115 -0
  46. package/src/hooks/useCopilotAlerts.ts +50 -0
  47. package/src/hooks/useCopilotAsk.ts +46 -0
  48. package/src/hooks/useCopilotPanelView.ts +84 -0
  49. package/src/hooks/useCopilotRecentSessions.ts +45 -0
  50. package/src/hooks/useCopilotRelationship.ts +102 -0
  51. package/src/hooks/useCopilotSessionStream.ts +94 -0
  52. package/dist/hooks.d.ts +0 -119
  53. package/dist/hooks.d.ts.map +0 -1
  54. package/dist/hooks.js +0 -282
  55. package/dist/hooks.js.map +0 -1
  56. package/src/hooks.ts +0 -457
@@ -0,0 +1,61 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { getCopilotEventIndex, initialCopilotUiState, reduceCopilotEvent, } from '@wildix/wilma-copilot-core';
3
+ import { GetSessionEventsCommand } from '@wildix/wilma-copilot-sessions-client';
4
+ import { PAGE_LIMIT } from './shared';
5
+ export function useCopilotSessionStream({ copilotClient, sessionId, live = true, streamEvents, onRelationship, onError, }) {
6
+ const [state, setState] = useState(initialCopilotUiState);
7
+ const [isLoading, setIsLoading] = useState(Boolean(sessionId));
8
+ const [error, setError] = useState();
9
+ useEffect(() => {
10
+ if (!sessionId) {
11
+ setState(initialCopilotUiState);
12
+ setIsLoading(false);
13
+ return;
14
+ }
15
+ const abort = new AbortController();
16
+ let nextIndex = 0;
17
+ setState(initialCopilotUiState);
18
+ setIsLoading(true);
19
+ setError(undefined);
20
+ const apply = (event) => {
21
+ nextIndex = Math.max(nextIndex, getCopilotEventIndex(event) + 1);
22
+ if (event.relationship)
23
+ onRelationship?.(event.relationship.sessionId);
24
+ setState((current) => reduceCopilotEvent(current, event));
25
+ };
26
+ const replay = async (startIndex = 0) => {
27
+ const output = await copilotClient.send(new GetSessionEventsCommand({ sessionId, startIndex, limit: PAGE_LIMIT }), {
28
+ abortSignal: abort.signal,
29
+ });
30
+ for (const event of output.events ?? []) {
31
+ apply(event);
32
+ }
33
+ if (output.nextStartIndex != null && !abort.signal.aborted) {
34
+ await replay(output.nextStartIndex);
35
+ }
36
+ };
37
+ void (async () => {
38
+ try {
39
+ await replay();
40
+ setIsLoading(false);
41
+ if (live && streamEvents && !abort.signal.aborted) {
42
+ for await (const event of streamEvents(sessionId, nextIndex, abort.signal))
43
+ apply(event);
44
+ }
45
+ }
46
+ catch (cause) {
47
+ if (!abort.signal.aborted) {
48
+ setError(cause instanceof Error ? cause : new Error(String(cause)));
49
+ onError?.(cause);
50
+ }
51
+ }
52
+ finally {
53
+ if (!abort.signal.aborted)
54
+ setIsLoading(false);
55
+ }
56
+ })();
57
+ return () => abort.abort();
58
+ }, [copilotClient, live, onError, onRelationship, sessionId, streamEvents]);
59
+ return { state, isLoading, error };
60
+ }
61
+ //# sourceMappingURL=useCopilotSessionStream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCopilotSessionStream.js","sourceRoot":"","sources":["../../src/hooks/useCopilotSessionStream.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wildix/wilma-copilot-headless",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "React hooks for Wilma Copilot relationships, session streams and asks with injected clients.",
6
6
  "main": "./dist/index.js",
@@ -14,20 +14,20 @@
14
14
  "peerDependencies": {
15
15
  "@wildix/wilma-assistant-client": ">=0.0.9 <1",
16
16
  "@wildix/wilma-copilot-core": ">=0.1 <1",
17
- "@wildix/wilma-copilot-sessions-client": ">=1.0.9 <2",
17
+ "@wildix/wilma-copilot-sessions-client": ">=1.0.15 <2",
18
18
  "react": "^19"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/react": "^19.1.9",
22
22
  "@wildix/eslint-config-style-guide": "^1.2.2",
23
- "@wildix/wilma-assistant-client": "0.0.9",
24
- "@wildix/wilma-copilot-sessions-client": "1.0.10",
23
+ "@wildix/wilma-assistant-client": "0.0.13",
24
+ "@wildix/wilma-copilot-sessions-client": "1.0.15",
25
25
  "eslint": "^8",
26
26
  "react": "^19",
27
27
  "rimraf": "^5.0.5",
28
28
  "typescript": "^5.9.2",
29
29
  "vitest": "^4.0.4",
30
- "@wildix/wilma-copilot-core": "0.1.1"
30
+ "@wildix/wilma-copilot-core": "0.1.2"
31
31
  },
32
32
  "exports": {
33
33
  ".": {
@@ -0,0 +1,28 @@
1
+ import {createElement, Fragment, type ReactElement, type ReactNode} from 'react';
2
+
3
+ import type {CopilotSessionStreamHandle} from '@wildix/wilma-copilot-core';
4
+
5
+ import {useCopilotSessionStream, type UseCopilotSessionStreamOptions} from './useCopilotSessionStream';
6
+
7
+ export interface CopilotSessionStreamSlotProps extends UseCopilotSessionStreamOptions {
8
+ children: (handle: CopilotSessionStreamHandle) => ReactNode;
9
+ }
10
+
11
+ /**
12
+ * Runs `useCopilotSessionStream` for one included session and hands the result to
13
+ * `children`.
14
+ *
15
+ * Hooks cannot be called in a loop, and an "included" session list is exactly that — an
16
+ * array whose length changes at runtime. Rendering one of these per included session id
17
+ * gives each its own stream and its own component identity, so React (not a manual map
18
+ * of subscriptions) is what starts and tears them down as sessions are included and
19
+ * removed.
20
+ */
21
+ export function CopilotSessionStreamSlot({children, ...options}: CopilotSessionStreamSlotProps): ReactElement {
22
+ const handle = useCopilotSessionStream(options);
23
+
24
+ // `createElement` rather than JSX: this package ships `.ts` sources with no JSX
25
+ // transform configured, and a bare fragment is the only wrapper a "returns whatever
26
+ // `children` produced" component needs.
27
+ return createElement(Fragment, null, children(handle));
28
+ }
@@ -0,0 +1,9 @@
1
+ export * from './CopilotSessionStreamSlot';
2
+ export * from './shared';
3
+ export * from './useCopilotActions';
4
+ export * from './useCopilotAlerts';
5
+ export * from './useCopilotAsk';
6
+ export * from './useCopilotPanelView';
7
+ export * from './useCopilotRecentSessions';
8
+ export * from './useCopilotRelationship';
9
+ export * from './useCopilotSessionStream';
@@ -0,0 +1,12 @@
1
+ import type {CopilotContextLookup, WilmaCopilotSessionsClient} from '@wildix/wilma-copilot-sessions-client';
2
+
3
+ export const PAGE_LIMIT = 1000;
4
+
5
+ export interface CopilotDependencies {
6
+ copilotClient: WilmaCopilotSessionsClient;
7
+ onError?: (error: unknown) => void;
8
+ }
9
+
10
+ export function lookupKey(lookup: CopilotContextLookup | undefined): string {
11
+ return JSON.stringify(lookup ?? {});
12
+ }
@@ -0,0 +1,115 @@
1
+ import {useCallback, useState} from 'react';
2
+
3
+ import {asRecord, type CopilotSmartActionEntry, getSmartActionType} from '@wildix/wilma-copilot-core';
4
+ import {ActCommand} from '@wildix/wilma-copilot-sessions-client';
5
+
6
+ import {type CopilotDependencies} from './shared';
7
+
8
+ export interface UseCopilotActionsOptions extends CopilotDependencies {
9
+ sessionId?: string;
10
+ /**
11
+ * Opens the URL of an `openUrl` smart action. Navigation belongs to the host, so the
12
+ * SDK reports the invocation and leaves the window to the app.
13
+ */
14
+ onOpenUrl?: (url: string, openInNewTab: boolean) => void;
15
+ }
16
+
17
+ /** Playbook progress the agent made by hand, as `Act` takes it. */
18
+ export interface CopilotPlaybookProgress {
19
+ /** Steps the agent ticked. Append-only server-side, so re-sending an id is a no-op. */
20
+ stepIds?: readonly string[];
21
+ /** Marks the whole playbook complete. Honoured only while the view reports `canMarkComplete`. */
22
+ complete?: boolean;
23
+ }
24
+
25
+ export interface CopilotActionsHandle {
26
+ /** Keys of the smart actions this client already invoked, for the muted styling. */
27
+ usedKeys: ReadonlySet<string>;
28
+ /**
29
+ * Runs a suggestion against `sessionId`, or against `overrideSessionId` when it came
30
+ * from another session. A panel showing included sessions offers their actions too,
31
+ * and an action belongs to the session that suggested it, not to the one in focus.
32
+ */
33
+ runSmartAction: (entry: CopilotSmartActionEntry, overrideSessionId?: string) => Promise<void>;
34
+ searchKnowledge: (questionId: string, question?: string) => Promise<void>;
35
+ markPlaybookProgress: (assetId: string, progress: CopilotPlaybookProgress) => Promise<void>;
36
+ }
37
+
38
+ /**
39
+ * The three members of `CopilotClientAction`. All are accepted into the session mailbox, so the
40
+ * outcome arrives as an `action` event and — for a playbook tick or a knowledge answer — as the
41
+ * asset re-emitted under its own id, rather than from the call itself.
42
+ */
43
+ export function useCopilotActions({
44
+ copilotClient,
45
+ sessionId,
46
+ onOpenUrl,
47
+ onError,
48
+ }: UseCopilotActionsOptions): CopilotActionsHandle {
49
+ const [usedKeys, setUsedKeys] = useState<ReadonlySet<string>>(new Set());
50
+
51
+ const runSmartAction = useCallback(
52
+ async (entry: CopilotSmartActionEntry, overrideSessionId?: string) => {
53
+ const type = getSmartActionType(entry.action);
54
+ const target = overrideSessionId ?? sessionId;
55
+ if (!type || !target) return;
56
+ setUsedKeys((current) => new Set(current).add(entry.key));
57
+
58
+ if (type === 'openUrl') {
59
+ const {url, openInNewTab} = asRecord(entry.action.openUrl);
60
+ if (typeof url === 'string' && url) onOpenUrl?.(url, openInNewTab !== false);
61
+ }
62
+
63
+ try {
64
+ await copilotClient.send(
65
+ new ActCommand({
66
+ sessionId: target,
67
+ action: {smartAction: {assetId: entry.assetId, actionId: entry.itemId}},
68
+ }),
69
+ );
70
+ } catch (cause) {
71
+ onError?.(cause);
72
+ }
73
+ },
74
+ [copilotClient, onError, onOpenUrl, sessionId],
75
+ );
76
+
77
+ const searchKnowledge = useCallback(
78
+ async (questionId: string, question?: string) => {
79
+ if (!sessionId) return;
80
+
81
+ try {
82
+ await copilotClient.send(new ActCommand({sessionId, action: {knowledgeSearch: {questionId, question}}}));
83
+ } catch (cause) {
84
+ onError?.(cause);
85
+ }
86
+ },
87
+ [copilotClient, onError, sessionId],
88
+ );
89
+
90
+ const markPlaybookProgress = useCallback(
91
+ async (assetId: string, {stepIds, complete}: CopilotPlaybookProgress) => {
92
+ if (!sessionId) return;
93
+
94
+ try {
95
+ await copilotClient.send(
96
+ new ActCommand({
97
+ sessionId,
98
+ action: {
99
+ playbook: {
100
+ assetId,
101
+ ...(stepIds ? {stepIds: [...stepIds]} : {}),
102
+ ...(complete === undefined ? {} : {complete}),
103
+ },
104
+ },
105
+ }),
106
+ );
107
+ } catch (cause) {
108
+ onError?.(cause);
109
+ }
110
+ },
111
+ [copilotClient, onError, sessionId],
112
+ );
113
+
114
+ return {usedKeys, runSmartAction, searchKnowledge, markPlaybookProgress};
115
+ }
@@ -0,0 +1,50 @@
1
+ import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
2
+
3
+ import {collectCopilotAlerts, type CopilotAlert, type CopilotUiState} from '@wildix/wilma-copilot-core';
4
+
5
+ export interface UseCopilotAlertsOptions {
6
+ state: CopilotUiState;
7
+ /** True while recorded events are replaying. Those are history, and history never alerts. */
8
+ isLoading?: boolean;
9
+ /** Called once per batch of alerts that arrived live, oldest first. */
10
+ onAlert?: (alerts: CopilotAlert[]) => void;
11
+ }
12
+
13
+ export interface CopilotAlertsHandle {
14
+ /** Alerts seen since the last `clear`, oldest first. Drives an unread badge. */
15
+ unseen: CopilotAlert[];
16
+ clear: () => void;
17
+ }
18
+
19
+ /**
20
+ * Watches a session for the few things worth interrupting for (`collectCopilotAlerts`).
21
+ *
22
+ * The point of the hook is the baseline. A session is rehydrated by replaying its
23
+ * recorded events, so a panel opened next to a call that has been running for ten
24
+ * minutes sees every earlier detection arrive at once — and firing a notification for
25
+ * each would be both wrong and unbearable. Everything present when the stream settles
26
+ * is therefore marked seen without alerting, and only what arrives afterwards is news.
27
+ */
28
+ export function useCopilotAlerts({state, isLoading = false, onAlert}: UseCopilotAlertsOptions): CopilotAlertsHandle {
29
+ const [unseen, setUnseen] = useState<CopilotAlert[]>([]);
30
+ const alerts = useMemo(() => collectCopilotAlerts(state), [state]);
31
+ const seenRef = useRef<Set<string>>(new Set());
32
+ const settledRef = useRef(false);
33
+
34
+ useEffect(() => {
35
+ const fresh = alerts.filter((alert) => !seenRef.current.has(alert.key));
36
+ for (const alert of fresh) seenRef.current.add(alert.key);
37
+
38
+ if (!settledRef.current) {
39
+ settledRef.current = !isLoading;
40
+
41
+ return;
42
+ }
43
+
44
+ if (fresh.length === 0) return;
45
+ setUnseen((current) => [...current, ...fresh]);
46
+ onAlert?.(fresh);
47
+ }, [alerts, isLoading, onAlert]);
48
+
49
+ return {unseen, clear: useCallback(() => setUnseen([]), [])};
50
+ }
@@ -0,0 +1,46 @@
1
+ import {useCallback, useState} from 'react';
2
+
3
+ import {StartSessionCommand, type WilmaAssistantClient} from '@wildix/wilma-assistant-client';
4
+ import {AskCommand} from '@wildix/wilma-copilot-sessions-client';
5
+
6
+ import {type CopilotDependencies} from './shared';
7
+
8
+ export interface UseCopilotAskOptions extends CopilotDependencies {
9
+ assistantClient: WilmaAssistantClient;
10
+ copilotSessionId?: string;
11
+ }
12
+
13
+ export function useCopilotAsk({assistantClient, copilotClient, copilotSessionId, onError}: UseCopilotAskOptions): {
14
+ ask: (question: string) => Promise<string | undefined>;
15
+ isAsking: boolean;
16
+ } {
17
+ const [isAsking, setIsAsking] = useState(false);
18
+ const ask = useCallback(
19
+ async (question: string) => {
20
+ const message = question.trim();
21
+ if (!message || !copilotSessionId) return undefined;
22
+ setIsAsking(true);
23
+
24
+ try {
25
+ const started = await assistantClient.send(
26
+ new StartSessionCommand({
27
+ message,
28
+ visibility: 'hidden',
29
+ target: `copilot://sessions/${copilotSessionId}`,
30
+ }),
31
+ );
32
+ await copilotClient.send(new AskCommand({sessionId: copilotSessionId, wilmaSessionId: started.sessionId}));
33
+
34
+ return started.sessionId;
35
+ } catch (cause) {
36
+ onError?.(cause);
37
+ throw cause;
38
+ } finally {
39
+ setIsAsking(false);
40
+ }
41
+ },
42
+ [assistantClient, copilotClient, copilotSessionId, onError],
43
+ );
44
+
45
+ return {ask, isAsking};
46
+ }
@@ -0,0 +1,84 @@
1
+ import {useCallback, useEffect, useMemo, useState} from 'react';
2
+
3
+ export type CopilotPanelView =
4
+ /** The session the panel opened on: guidance, smart actions and the composer. */
5
+ | {kind: 'live'}
6
+ | {kind: 'history'}
7
+ /** An earlier session from the history, read-only. */
8
+ | {kind: 'session'; sessionId: string};
9
+
10
+ export interface CopilotPanelViewHandle {
11
+ view: CopilotPanelView;
12
+ /** The session the current view is about, if it is about one. */
13
+ sessionId: string | undefined;
14
+ /** Whether transcript lines are interleaved in the live timeline. */
15
+ showTranscript: boolean;
16
+ toggleTranscript: () => void;
17
+ setShowTranscript: (value: boolean) => void;
18
+ openHistory: () => void;
19
+ openSession: (sessionId: string) => void;
20
+ /** Returns to the view that opened this one, and to the live view from the root. */
21
+ back: () => void;
22
+ goLive: () => void;
23
+ /**
24
+ * Past sessions folded into the live timeline instead of opened on their own, oldest
25
+ * first. History offers this as "include" beside "switch to", for pulling an earlier
26
+ * conversation's guidance into view without losing the current one.
27
+ */
28
+ includedSessionIds: string[];
29
+ isIncluded: (sessionId: string) => boolean;
30
+ includeSession: (sessionId: string) => void;
31
+ removeIncludedSession: (sessionId: string) => void;
32
+ }
33
+
34
+ const LIVE_VIEW: CopilotPanelView = {kind: 'live'};
35
+
36
+ /**
37
+ * Which view the panel is showing, as a stack.
38
+ *
39
+ * A stack rather than a flag because the views nest: history opens a session, and every
40
+ * back step has to land where the reader came from rather than dropping them at the live
41
+ * session. Transcript lines are toggled inline on the timeline instead of opening a
42
+ * separate view.
43
+ *
44
+ * `initialView` opens the panel on that view with the live one beneath it, so a deep
45
+ * link into the history still has somewhere to go back to.
46
+ */
47
+ export function useCopilotPanelView(initialView?: CopilotPanelView): CopilotPanelViewHandle {
48
+ const [stack, setStack] = useState<CopilotPanelView[]>(() =>
49
+ initialView && initialView.kind !== 'live' ? [LIVE_VIEW, initialView] : [LIVE_VIEW],
50
+ );
51
+ const [showTranscript, setShowTranscript] = useState(false);
52
+ const [includedSessionIds, setIncludedSessionIds] = useState<string[]>([]);
53
+ const view = stack[stack.length - 1] ?? LIVE_VIEW;
54
+ const sessionId = view.kind === 'session' ? view.sessionId : undefined;
55
+ const push = useCallback((next: CopilotPanelView) => setStack((current) => [...current, next]), []);
56
+
57
+ useEffect(() => {
58
+ setShowTranscript(false);
59
+ }, [sessionId, view.kind]);
60
+
61
+ const includedSet = useMemo(() => new Set(includedSessionIds), [includedSessionIds]);
62
+
63
+ return {
64
+ view,
65
+ sessionId,
66
+ showTranscript,
67
+ toggleTranscript: useCallback(() => setShowTranscript((current) => !current), []),
68
+ setShowTranscript,
69
+ openHistory: useCallback(() => push({kind: 'history'}), [push]),
70
+ openSession: useCallback((id: string) => push({kind: 'session', sessionId: id}), [push]),
71
+ back: useCallback(() => setStack((current) => (current.length > 1 ? current.slice(0, -1) : current)), []),
72
+ goLive: useCallback(() => setStack([LIVE_VIEW]), []),
73
+ includedSessionIds,
74
+ isIncluded: useCallback((id: string) => includedSet.has(id), [includedSet]),
75
+ includeSession: useCallback(
76
+ (id: string) => setIncludedSessionIds((current) => (current.includes(id) ? current : [...current, id])),
77
+ [],
78
+ ),
79
+ removeIncludedSession: useCallback(
80
+ (id: string) => setIncludedSessionIds((current) => current.filter((entry) => entry !== id)),
81
+ [],
82
+ ),
83
+ };
84
+ }
@@ -0,0 +1,45 @@
1
+ import {useEffect, useState} from 'react';
2
+
3
+ import {sortSessionsRecentFirst} from '@wildix/wilma-copilot-core';
4
+ import {type CopilotSessionSummary, ListSessionsCommand} from '@wildix/wilma-copilot-sessions-client';
5
+
6
+ import {type CopilotDependencies} from './shared';
7
+
8
+ export function useCopilotRecentSessions(options: CopilotDependencies & {enabled?: boolean; limit?: number}): {
9
+ sessions: CopilotSessionSummary[];
10
+ isLoading: boolean;
11
+ error?: Error;
12
+ refresh: () => void;
13
+ } {
14
+ const {copilotClient, enabled = true, limit = 20, onError} = options;
15
+ const [sessions, setSessions] = useState<CopilotSessionSummary[]>([]);
16
+ const [isLoading, setIsLoading] = useState(enabled);
17
+ const [error, setError] = useState<Error>();
18
+ const [nonce, setNonce] = useState(0);
19
+
20
+ useEffect(() => {
21
+ if (!enabled) return;
22
+ let cancelled = false;
23
+ setIsLoading(true);
24
+ void copilotClient
25
+ .send(new ListSessionsCommand({limit}))
26
+ .then((result) => {
27
+ if (!cancelled) setSessions(sortSessionsRecentFirst(result.sessions ?? []));
28
+ })
29
+ .catch((cause: unknown) => {
30
+ if (!cancelled) {
31
+ setError(cause instanceof Error ? cause : new Error(String(cause)));
32
+ onError?.(cause);
33
+ }
34
+ })
35
+ .finally(() => {
36
+ if (!cancelled) setIsLoading(false);
37
+ });
38
+
39
+ return () => {
40
+ cancelled = true;
41
+ };
42
+ }, [copilotClient, enabled, limit, nonce, onError]);
43
+
44
+ return {sessions, isLoading, error, refresh: () => setNonce((value) => value + 1)};
45
+ }
@@ -0,0 +1,102 @@
1
+ import {useCallback, useEffect, useRef, useState} from 'react';
2
+
3
+ import {type CopilotRelationshipHandle, sortSessionsRecentFirst} from '@wildix/wilma-copilot-core';
4
+ import {
5
+ type CopilotContextLookup,
6
+ type CopilotSessionSummary,
7
+ EnrichCommand,
8
+ ListRelationshipSessionsCommand,
9
+ OpenCommand,
10
+ } from '@wildix/wilma-copilot-sessions-client';
11
+
12
+ import {type CopilotDependencies, lookupKey} from './shared';
13
+
14
+ export interface UseCopilotRelationshipOptions extends CopilotDependencies {
15
+ lookup?: CopilotContextLookup;
16
+ enabled?: boolean;
17
+ sessionsLimit?: number;
18
+ }
19
+
20
+ export function useCopilotRelationship({
21
+ copilotClient,
22
+ lookup,
23
+ enabled = true,
24
+ sessionsLimit = 50,
25
+ onError,
26
+ }: UseCopilotRelationshipOptions): CopilotRelationshipHandle {
27
+ const [relationshipId, setRelationshipId] = useState<string>();
28
+ const [defaultSessionId, setDefaultSessionId] = useState<string>();
29
+ const [sessions, setSessions] = useState<CopilotSessionSummary[]>([]);
30
+ const [isLoading, setIsLoading] = useState(enabled);
31
+ const [error, setError] = useState<Error>();
32
+ const [attempt, setAttempt] = useState(0);
33
+ const relationshipRef = useRef<string | undefined>(undefined);
34
+ const openedLookupRef = useRef<string | undefined>(undefined);
35
+
36
+ relationshipRef.current = relationshipId;
37
+
38
+ const loadSessions = useCallback(
39
+ async (id: string, signal?: AbortSignal) => {
40
+ const result = await copilotClient.send(
41
+ new ListRelationshipSessionsCommand({relationshipId: id, limit: sessionsLimit}),
42
+ signal ? {abortSignal: signal} : undefined,
43
+ );
44
+ if (!signal?.aborted) setSessions(sortSessionsRecentFirst(result.sessions ?? []));
45
+ },
46
+ [copilotClient, sessionsLimit],
47
+ );
48
+
49
+ const refresh = useCallback(() => {
50
+ if (relationshipRef.current) void loadSessions(relationshipRef.current).catch(onError);
51
+ }, [loadSessions, onError]);
52
+
53
+ const retry = useCallback(() => {
54
+ setError(undefined);
55
+ setAttempt((value) => value + 1);
56
+ }, []);
57
+
58
+ useEffect(() => {
59
+ if (!enabled) return;
60
+ const abort = new AbortController();
61
+ setIsLoading(true);
62
+
63
+ void (async () => {
64
+ try {
65
+ const result = await copilotClient.send(new OpenCommand({lookup}), {abortSignal: abort.signal});
66
+ if (abort.signal.aborted) return;
67
+ openedLookupRef.current = lookupKey(lookup);
68
+ setRelationshipId(result.relationshipId);
69
+ setDefaultSessionId(result.session.id);
70
+ await loadSessions(result.relationshipId, abort.signal);
71
+ setError(undefined);
72
+ } catch (cause) {
73
+ if (!abort.signal.aborted) {
74
+ const next = cause instanceof Error ? cause : new Error(String(cause));
75
+ setError(next);
76
+ onError?.(cause);
77
+ }
78
+ } finally {
79
+ if (!abort.signal.aborted) setIsLoading(false);
80
+ }
81
+ })();
82
+
83
+ return () => abort.abort();
84
+ }, [attempt, copilotClient, enabled, loadSessions, onError]);
85
+
86
+ const key = lookupKey(lookup);
87
+ useEffect(() => {
88
+ const currentId = relationshipRef.current;
89
+ if (!enabled || !currentId || openedLookupRef.current === undefined || openedLookupRef.current === key) return;
90
+ openedLookupRef.current = key;
91
+
92
+ void copilotClient
93
+ .send(new EnrichCommand({relationshipId: currentId, lookup: lookup as CopilotContextLookup}))
94
+ .then((result) => {
95
+ setRelationshipId(result.relationshipId);
96
+ setSessions(sortSessionsRecentFirst(result.related ?? []));
97
+ })
98
+ .catch(onError);
99
+ }, [copilotClient, enabled, key, lookup, onError]);
100
+
101
+ return {relationshipId, sessions, defaultSessionId, isLoading, error, retry, refresh};
102
+ }