@opengeni/react 0.4.0 → 0.5.0
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.
- package/dist/index.d.ts +23 -16
- package/dist/index.js +176 -120
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +1 -0
- package/src/components/fleet-tile.tsx +5 -0
- package/src/hooks/use-session.ts +80 -12
- package/src/index.ts +1 -1
- package/src/lib/git-patch.ts +10 -4
- package/src/timeline/parsers.ts +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "React hooks and styled components for OpenGeni: live session streaming, chat composer, message timeline, session status, and fleet views — token-themed (CSS variables), dark-first, built on Tailwind v4 + Radix + Motion.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"demo:build": "vite build demo"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@opengeni/sdk": "^0.
|
|
43
|
+
"@opengeni/sdk": "^0.5.0",
|
|
44
44
|
"clsx": "^2.1.1",
|
|
45
45
|
"lucide-react": "^1.8.0",
|
|
46
46
|
"motion": "^12.0.0",
|
package/src/client.ts
CHANGED
|
@@ -15,6 +15,11 @@ export type FleetTileProps = {
|
|
|
15
15
|
|
|
16
16
|
/** Best-effort display title for a session. */
|
|
17
17
|
export function sessionDisplayTitle(session: Session): string {
|
|
18
|
+
// A set title (agent-generated or user-renamed) wins over metadata + the
|
|
19
|
+
// initial-message fallback.
|
|
20
|
+
if (typeof session.title === "string" && session.title.trim().length > 0) {
|
|
21
|
+
return session.title;
|
|
22
|
+
}
|
|
18
23
|
for (const key of ["title", "name"] as const) {
|
|
19
24
|
const value = session.metadata[key];
|
|
20
25
|
if (typeof value === "string" && value.trim().length > 0) {
|
package/src/hooks/use-session.ts
CHANGED
|
@@ -1,33 +1,101 @@
|
|
|
1
|
-
import type { Session } from "@opengeni/sdk";
|
|
2
|
-
import { useCallback } from "react";
|
|
1
|
+
import type { Session, SessionEvent } from "@opengeni/sdk";
|
|
2
|
+
import { useCallback, useState } from "react";
|
|
3
3
|
import { useOpenGeni, type ClientOverride } from "../provider";
|
|
4
|
-
import { usePolledValue } from "./internal";
|
|
4
|
+
import { useMutationRunner, usePolledValue, useSessionEventTrigger, type SessionEventFeedOptions } from "./internal";
|
|
5
5
|
|
|
6
|
-
export type UseSessionOptions = ClientOverride &
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
};
|
|
6
|
+
export type UseSessionOptions = ClientOverride &
|
|
7
|
+
SessionEventFeedOptions & {
|
|
8
|
+
/** Re-fetch on an interval (ms). Off by default — pair with `useSessionEvents` for live status. */
|
|
9
|
+
pollIntervalMs?: number | undefined;
|
|
10
|
+
};
|
|
11
11
|
|
|
12
12
|
export type UseSessionResult = {
|
|
13
13
|
session: Session | null;
|
|
14
14
|
loading: boolean;
|
|
15
15
|
error: Error | null;
|
|
16
16
|
refresh: () => Promise<void>;
|
|
17
|
+
/** Manually rename the session (PATCH, source='user'). Returns the updated session, or null on failure. */
|
|
18
|
+
updateTitle: (title: string) => Promise<Session | null>;
|
|
19
|
+
/** True while a rename is in flight. */
|
|
20
|
+
updating: boolean;
|
|
21
|
+
mutationError: Error | null;
|
|
22
|
+
clearMutationError: () => void;
|
|
17
23
|
};
|
|
18
24
|
|
|
19
|
-
/**
|
|
25
|
+
/** Event types that change the session title (auto + cross-client renames). */
|
|
26
|
+
export function isTitleEvent(event: Pick<SessionEvent, "type">): boolean {
|
|
27
|
+
return event.type === "session.title_set";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Fetch one session (with optional polling), live-patching its title on `session.title_set`. */
|
|
20
31
|
export function useSession(sessionId: string | null | undefined, options: UseSessionOptions = {}): UseSessionResult {
|
|
21
32
|
const { client, workspaceId } = useOpenGeni(options);
|
|
33
|
+
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
34
|
+
const [override, setOverride] = useState<Session | null>(null);
|
|
35
|
+
const mutation = useMutationRunner();
|
|
22
36
|
const load = useCallback(async () => {
|
|
23
37
|
if (!sessionId) {
|
|
24
38
|
return null;
|
|
25
39
|
}
|
|
26
|
-
|
|
40
|
+
const fetched = await client.getSession(workspaceId, sessionId);
|
|
41
|
+
// A fresh server read supersedes any optimistic/event-driven override.
|
|
42
|
+
setOverride(null);
|
|
43
|
+
return fetched;
|
|
27
44
|
}, [client, workspaceId, sessionId]);
|
|
28
45
|
const state = usePolledValue(load, {
|
|
29
46
|
pollIntervalMs: options.pollIntervalMs,
|
|
30
|
-
enabled
|
|
47
|
+
enabled,
|
|
31
48
|
});
|
|
32
|
-
|
|
49
|
+
|
|
50
|
+
const base = state.data ?? null;
|
|
51
|
+
// The override only ever carries title/titleSource patches; it is reset on
|
|
52
|
+
// every fresh load so it can never go stale against the server snapshot.
|
|
53
|
+
const session = base && override && override.id === base.id ? { ...base, title: override.title, titleSource: override.titleSource } : base;
|
|
54
|
+
|
|
55
|
+
// Live-patch the title on auto (agent) + cross-client (user/agent) renames so
|
|
56
|
+
// the UI reflects the new title without polling or a full re-fetch.
|
|
57
|
+
const onTitleEvent = useCallback((event: SessionEvent) => {
|
|
58
|
+
const payload = (event.payload ?? {}) as { title?: unknown; source?: unknown };
|
|
59
|
+
const title = payload.title;
|
|
60
|
+
if (typeof title !== "string") {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const source: "user" | "agent" | null = payload.source === "user" || payload.source === "agent" ? payload.source : null;
|
|
64
|
+
setOverride((current): Session | null => {
|
|
65
|
+
const next = current ?? base;
|
|
66
|
+
if (!next) {
|
|
67
|
+
return current;
|
|
68
|
+
}
|
|
69
|
+
return { ...next, title, titleSource: source };
|
|
70
|
+
});
|
|
71
|
+
}, [base]);
|
|
72
|
+
useSessionEventTrigger(client, workspaceId, sessionId, isTitleEvent, onTitleEvent, {
|
|
73
|
+
enabled,
|
|
74
|
+
...(options.events !== undefined ? { events: options.events } : {}),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const updateTitle = useCallback(
|
|
78
|
+
async (title: string): Promise<Session | null> => {
|
|
79
|
+
if (!sessionId) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const result = await mutation.run(() => client.updateSession(workspaceId, sessionId, { title }));
|
|
83
|
+
if (result) {
|
|
84
|
+
setOverride(result);
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
},
|
|
88
|
+
[client, workspaceId, sessionId, mutation.run],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
session,
|
|
93
|
+
loading: state.loading,
|
|
94
|
+
error: state.error,
|
|
95
|
+
refresh: state.refresh,
|
|
96
|
+
updateTitle,
|
|
97
|
+
updating: mutation.mutating,
|
|
98
|
+
mutationError: mutation.mutationError,
|
|
99
|
+
clearMutationError: mutation.clearMutationError,
|
|
100
|
+
};
|
|
33
101
|
}
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { OpenGeniProvider, useOpenGeni, useOpenGeniClient } from "./provider";
|
|
|
9
9
|
export type { ClientOverride, OpenGeniContextValue, OpenGeniProviderProps } from "./provider";
|
|
10
10
|
|
|
11
11
|
// Hooks
|
|
12
|
-
export { useSession } from "./hooks/use-session";
|
|
12
|
+
export { useSession, isTitleEvent } from "./hooks/use-session";
|
|
13
13
|
export type { UseSessionOptions, UseSessionResult } from "./hooks/use-session";
|
|
14
14
|
export { useSessionEvents } from "./hooks/use-session-events";
|
|
15
15
|
export type { SessionEventsConnectionState, UseSessionEventsOptions, UseSessionEventsResult } from "./hooks/use-session-events";
|
package/src/lib/git-patch.ts
CHANGED
|
@@ -22,10 +22,16 @@ export function gitFileDiffToPatch(file: GitFileDiff): string {
|
|
|
22
22
|
lines.push(`+++ b/${newPath}`);
|
|
23
23
|
}
|
|
24
24
|
for (const hunk of file.hunks) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
// Only trust a pre-parsed header if it carries the full unified range form
|
|
26
|
+
// `@@ -<o>[,<n>] +<o>[,<n>] @@`. A synthesized create_file hunk (parsers.ts)
|
|
27
|
+
// can carry a degenerate `@@ +1 @@` with no `-`/`+` ranges; a generic patch
|
|
28
|
+
// parser (Pierre) renders zero lines from it, so the expanded diff comes up
|
|
29
|
+
// empty while the collapsed chip still shows the (correct) addition count.
|
|
30
|
+
// In that case regenerate a valid header from the hunk's range fields.
|
|
31
|
+
const headerIsValid = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/.test(hunk.header ?? "");
|
|
32
|
+
const header = headerIsValid
|
|
33
|
+
? hunk.header
|
|
34
|
+
: `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`;
|
|
29
35
|
lines.push(header);
|
|
30
36
|
for (const line of hunk.lines) {
|
|
31
37
|
if (line.type === "meta") continue;
|
package/src/timeline/parsers.ts
CHANGED
|
@@ -131,7 +131,12 @@ export function v4aToGitFileDiff(op: ApplyPatchOperation): GitFileDiff {
|
|
|
131
131
|
hunks.push(cur);
|
|
132
132
|
} else if (cur || op.type === "create_file") {
|
|
133
133
|
if (!cur) {
|
|
134
|
-
|
|
134
|
+
// No `@@` anchor on a create_file body: synthesize an add-only hunk.
|
|
135
|
+
// Leave `header` empty so `gitFileDiffToPatch` regenerates a valid
|
|
136
|
+
// `@@ -0,0 +1,N @@` from the range fields once newLines is counted — a
|
|
137
|
+
// pre-baked partial header (e.g. `@@ +1 @@`) renders zero lines in a
|
|
138
|
+
// generic unified-diff parser.
|
|
139
|
+
cur = { oldStart: 0, oldLines: 0, newStart: 1, newLines: 0, header: "", lines: [] };
|
|
135
140
|
hunks.push(cur);
|
|
136
141
|
oldNo = 0;
|
|
137
142
|
newNo = 1;
|