@stigmer/react 0.5.0 → 0.5.1
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/composer/ContextChip.d.ts +7 -2
- package/composer/ContextChip.d.ts.map +1 -1
- package/composer/ContextChip.js +2 -1
- package/composer/ContextChip.js.map +1 -1
- package/composer/SessionComposer.d.ts +11 -0
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +33 -4
- package/composer/SessionComposer.js.map +1 -1
- package/environment/usePersonalEnvironment.d.ts.map +1 -1
- package/environment/usePersonalEnvironment.js +1 -0
- package/environment/usePersonalEnvironment.js.map +1 -1
- package/index.d.ts +2 -2
- package/index.d.ts.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/inline-edit/InlineEditKeyValue.d.ts +5 -1
- package/inline-edit/InlineEditKeyValue.d.ts.map +1 -1
- package/inline-edit/InlineEditKeyValue.js +3 -3
- package/inline-edit/InlineEditKeyValue.js.map +1 -1
- package/internal/useFetch.js +2 -2
- package/internal/useFetch.js.map +1 -1
- package/mcp-server/McpServerDetailView.d.ts.map +1 -1
- package/mcp-server/McpServerDetailView.js +145 -46
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/models/ModelRegistryContext.d.ts +2 -0
- package/models/ModelRegistryContext.d.ts.map +1 -1
- package/models/ModelRegistryContext.js +1 -0
- package/models/ModelRegistryContext.js.map +1 -1
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +2 -2
- package/models/ModelSelector.js.map +1 -1
- package/models/__tests__/useModelRegistry.test.js +4 -3
- package/models/__tests__/useModelRegistry.test.js.map +1 -1
- package/models/useModelRegistry.d.ts +2 -0
- package/models/useModelRegistry.d.ts.map +1 -1
- package/models/useModelRegistry.js +3 -2
- package/models/useModelRegistry.js.map +1 -1
- package/package.json +4 -4
- package/provider.d.ts.map +1 -1
- package/provider.js +69 -22
- package/provider.js.map +1 -1
- package/session/__tests__/session-spec-converters.test.d.ts +2 -0
- package/session/__tests__/session-spec-converters.test.d.ts.map +1 -0
- package/session/__tests__/session-spec-converters.test.js +162 -0
- package/session/__tests__/session-spec-converters.test.js.map +1 -0
- package/session/__tests__/useNewSessionFlow.test.js +2 -2
- package/session/__tests__/useNewSessionFlow.test.js.map +1 -1
- package/session/__tests__/usePersistedModel.test.js +1 -1
- package/session/__tests__/usePersistedModel.test.js.map +1 -1
- package/session/group-sessions.d.ts +17 -0
- package/session/group-sessions.d.ts.map +1 -1
- package/session/group-sessions.js +46 -0
- package/session/group-sessions.js.map +1 -1
- package/session/index.d.ts +4 -2
- package/session/index.d.ts.map +1 -1
- package/session/index.js +2 -1
- package/session/index.js.map +1 -1
- package/session/session-spec-converters.d.ts +24 -0
- package/session/session-spec-converters.d.ts.map +1 -0
- package/session/session-spec-converters.js +72 -0
- package/session/session-spec-converters.js.map +1 -0
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +1 -56
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionPageFlow.d.ts +5 -0
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +20 -6
- package/session/useSessionPageFlow.js.map +1 -1
- package/session/useSessionSearch.d.ts +57 -0
- package/session/useSessionSearch.d.ts.map +1 -0
- package/session/useSessionSearch.js +94 -0
- package/session/useSessionSearch.js.map +1 -0
- package/src/composer/ContextChip.tsx +20 -11
- package/src/composer/SessionComposer.tsx +52 -3
- package/src/environment/usePersonalEnvironment.ts +1 -0
- package/src/index.ts +5 -0
- package/src/inline-edit/InlineEditKeyValue.tsx +23 -0
- package/src/internal/useFetch.ts +2 -2
- package/src/mcp-server/McpServerDetailView.tsx +429 -55
- package/src/models/ModelRegistryContext.ts +3 -0
- package/src/models/ModelSelector.tsx +25 -2
- package/src/models/__tests__/useModelRegistry.test.tsx +5 -3
- package/src/models/useModelRegistry.ts +5 -2
- package/src/provider.tsx +69 -18
- package/src/session/__tests__/session-spec-converters.test.ts +185 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +2 -2
- package/src/session/__tests__/usePersistedModel.test.tsx +1 -1
- package/src/session/group-sessions.ts +65 -0
- package/src/session/index.ts +8 -2
- package/src/session/session-spec-converters.ts +86 -0
- package/src/session/useSessionConversation.ts +5 -64
- package/src/session/useSessionPageFlow.ts +28 -7
- package/src/session/useSessionSearch.ts +149 -0
- package/styles.css +1 -1
|
@@ -90,8 +90,10 @@ const TEST_REGISTRY_JSON = {
|
|
|
90
90
|
|
|
91
91
|
const TEST_MODELS: readonly ModelInfo[] = parseRegistryJson(TEST_REGISTRY_JSON);
|
|
92
92
|
|
|
93
|
+
const noopRefetch = () => {};
|
|
94
|
+
|
|
93
95
|
function createWrapper(models: readonly ModelInfo[] = TEST_MODELS) {
|
|
94
|
-
const state: ModelRegistryState = { models, isLoading: false, error: null };
|
|
96
|
+
const state: ModelRegistryState = { models, isLoading: false, error: null, refetch: noopRefetch };
|
|
95
97
|
return function Wrapper({ children }: { children: ReactNode }) {
|
|
96
98
|
return (
|
|
97
99
|
<ModelRegistryContext.Provider value={state}>
|
|
@@ -285,7 +287,7 @@ describe("useModelRegistry", () => {
|
|
|
285
287
|
|
|
286
288
|
describe("loading state", () => {
|
|
287
289
|
it("exposes isLoading from context", () => {
|
|
288
|
-
const loadingState: ModelRegistryState = { models: [], isLoading: true, error: null };
|
|
290
|
+
const loadingState: ModelRegistryState = { models: [], isLoading: true, error: null, refetch: noopRefetch };
|
|
289
291
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
290
292
|
<ModelRegistryContext.Provider value={loadingState}>
|
|
291
293
|
{children}
|
|
@@ -298,7 +300,7 @@ describe("useModelRegistry", () => {
|
|
|
298
300
|
|
|
299
301
|
it("exposes error from context", () => {
|
|
300
302
|
const err = new Error("fetch failed");
|
|
301
|
-
const errorState: ModelRegistryState = { models: [], isLoading: false, error: err };
|
|
303
|
+
const errorState: ModelRegistryState = { models: [], isLoading: false, error: err, refetch: noopRefetch };
|
|
302
304
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
303
305
|
<ModelRegistryContext.Provider value={errorState}>
|
|
304
306
|
{children}
|
|
@@ -55,6 +55,8 @@ export interface UseModelRegistryReturn {
|
|
|
55
55
|
readonly isLoading: boolean;
|
|
56
56
|
/** Non-null if the API fetch failed. Models will be empty in this case. */
|
|
57
57
|
readonly error: Error | null;
|
|
58
|
+
/** Retry fetching the model registry. No-op while a fetch is in flight. */
|
|
59
|
+
readonly refetch: () => void;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
/**
|
|
@@ -85,7 +87,7 @@ export interface UseModelRegistryReturn {
|
|
|
85
87
|
*/
|
|
86
88
|
export function useModelRegistry(options?: UseModelRegistryOptions): UseModelRegistryReturn {
|
|
87
89
|
const harness = options?.harness;
|
|
88
|
-
const { models: allModels, isLoading, error } = useModelRegistryContext();
|
|
90
|
+
const { models: allModels, isLoading, error, refetch } = useModelRegistryContext();
|
|
89
91
|
|
|
90
92
|
return useMemo(() => {
|
|
91
93
|
const isUnified = harness === undefined;
|
|
@@ -143,6 +145,7 @@ export function useModelRegistry(options?: UseModelRegistryOptions): UseModelReg
|
|
|
143
145
|
getByKey: (key: string) => byCompoundKey.get(key),
|
|
144
146
|
isLoading,
|
|
145
147
|
error,
|
|
148
|
+
refetch,
|
|
146
149
|
};
|
|
147
|
-
}, [harness, allModels, isLoading, error]);
|
|
150
|
+
}, [harness, allModels, isLoading, error, refetch]);
|
|
148
151
|
}
|
package/src/provider.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
|
4
4
|
import type { Stigmer, DeploymentMode } from "@stigmer/sdk";
|
|
5
5
|
import { cn, resolvePresetClass } from "@stigmer/theme";
|
|
6
6
|
import type { ThemePresetId } from "@stigmer/theme";
|
|
@@ -151,15 +151,23 @@ export function StigmerProvider({
|
|
|
151
151
|
);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
const RETRY_DELAYS_MS = [1_000, 2_000, 4_000];
|
|
155
|
+
const TOKEN_POLL_INTERVAL_MS = 500;
|
|
156
|
+
const TOKEN_POLL_MAX_MS = 10_000;
|
|
157
|
+
|
|
154
158
|
/**
|
|
155
|
-
* Fetches the model registry from the authenticated API
|
|
156
|
-
*
|
|
159
|
+
* Fetches the model registry from the authenticated API and caches
|
|
160
|
+
* the result for the lifetime of the provider.
|
|
157
161
|
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
162
|
+
* Handles the auth race condition where `StigmerProvider` mounts before
|
|
163
|
+
* PKCE authentication is established (e.g. desktop release builds with
|
|
164
|
+
* a fresh localStorage). When the initial token is `null`, polls for
|
|
165
|
+
* a valid token before giving up. Retries on transient failures with
|
|
166
|
+
* exponential backoff. Exposes `refetch` for manual retry from the UI.
|
|
160
167
|
*/
|
|
161
168
|
function useModelRegistryFetch(client: Stigmer): ModelRegistryState {
|
|
162
|
-
const [
|
|
169
|
+
const [version, setVersion] = useState(0);
|
|
170
|
+
const [state, setState] = useState<Omit<ModelRegistryState, "refetch">>({
|
|
163
171
|
models: [],
|
|
164
172
|
isLoading: true,
|
|
165
173
|
error: null,
|
|
@@ -168,31 +176,74 @@ function useModelRegistryFetch(client: Stigmer): ModelRegistryState {
|
|
|
168
176
|
const clientRef = useRef(client);
|
|
169
177
|
clientRef.current = client;
|
|
170
178
|
|
|
171
|
-
|
|
172
|
-
let cancelled = false;
|
|
179
|
+
const fetchAttemptRef = useRef(0);
|
|
173
180
|
|
|
181
|
+
const doFetch = useCallback(async (signal: AbortSignal) => {
|
|
174
182
|
const c = clientRef.current;
|
|
175
|
-
c.getAuthCredential()
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
183
|
+
let token = await c.getAuthCredential();
|
|
184
|
+
|
|
185
|
+
if (!token) {
|
|
186
|
+
const start = Date.now();
|
|
187
|
+
while (!token && Date.now() - start < TOKEN_POLL_MAX_MS) {
|
|
188
|
+
if (signal.aborted) return;
|
|
189
|
+
await new Promise((r) => setTimeout(r, TOKEN_POLL_INTERVAL_MS));
|
|
190
|
+
if (signal.aborted) return;
|
|
191
|
+
token = await c.getAuthCredential();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return fetchModelRegistry(c.baseUrl, token, c.fetch);
|
|
196
|
+
}, []);
|
|
197
|
+
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
const { signal } = controller;
|
|
201
|
+
|
|
202
|
+
fetchAttemptRef.current = 0;
|
|
203
|
+
|
|
204
|
+
const attempt = async () => {
|
|
205
|
+
if (signal.aborted) return;
|
|
206
|
+
|
|
207
|
+
setState((prev) => (prev.isLoading ? prev : { ...prev, isLoading: true }));
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const models = await doFetch(signal);
|
|
211
|
+
if (!signal.aborted && models) {
|
|
179
212
|
setState({ models, isLoading: false, error: null });
|
|
213
|
+
fetchAttemptRef.current = 0;
|
|
180
214
|
}
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
|
|
215
|
+
} catch (err: unknown) {
|
|
216
|
+
if (signal.aborted) return;
|
|
217
|
+
|
|
218
|
+
const retryIdx = fetchAttemptRef.current;
|
|
219
|
+
fetchAttemptRef.current = retryIdx + 1;
|
|
220
|
+
|
|
221
|
+
if (retryIdx < RETRY_DELAYS_MS.length) {
|
|
222
|
+
setTimeout(() => { if (!signal.aborted) attempt(); }, RETRY_DELAYS_MS[retryIdx]);
|
|
223
|
+
} else {
|
|
184
224
|
setState({
|
|
185
225
|
models: [],
|
|
186
226
|
isLoading: false,
|
|
187
227
|
error: err instanceof Error ? err : new Error(String(err)),
|
|
188
228
|
});
|
|
189
229
|
}
|
|
190
|
-
}
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
attempt();
|
|
234
|
+
|
|
235
|
+
return () => { controller.abort(); };
|
|
236
|
+
}, [doFetch, version]);
|
|
191
237
|
|
|
192
|
-
|
|
238
|
+
const refetch = useCallback(() => {
|
|
239
|
+
setState((prev) => {
|
|
240
|
+
if (prev.isLoading) return prev;
|
|
241
|
+
return { ...prev, isLoading: true, error: null };
|
|
242
|
+
});
|
|
243
|
+
setVersion((v) => v + 1);
|
|
193
244
|
}, []);
|
|
194
245
|
|
|
195
|
-
return state;
|
|
246
|
+
return { ...state, refetch };
|
|
196
247
|
}
|
|
197
248
|
|
|
198
249
|
/**
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
specWorkspaceToInput,
|
|
4
|
+
specMcpUsagesToInput,
|
|
5
|
+
specSkillRefsToInput,
|
|
6
|
+
} from "../session-spec-converters";
|
|
7
|
+
import type { Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
|
|
8
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
9
|
+
|
|
10
|
+
type SessionSpec = Session["spec"];
|
|
11
|
+
|
|
12
|
+
function makeSpec(overrides: Partial<NonNullable<SessionSpec>> = {}): SessionSpec {
|
|
13
|
+
return {
|
|
14
|
+
agentInstanceId: "",
|
|
15
|
+
subject: "",
|
|
16
|
+
threadId: "",
|
|
17
|
+
sandboxId: "",
|
|
18
|
+
metadata: {},
|
|
19
|
+
workspaceEntries: [],
|
|
20
|
+
mcpServerUsages: [],
|
|
21
|
+
skillRefs: [],
|
|
22
|
+
runnerId: "",
|
|
23
|
+
harness: 0,
|
|
24
|
+
cursorMode: 0,
|
|
25
|
+
...overrides,
|
|
26
|
+
} as unknown as SessionSpec;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("specWorkspaceToInput", () => {
|
|
30
|
+
it("returns undefined for undefined spec", () => {
|
|
31
|
+
expect(specWorkspaceToInput(undefined)).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("returns empty array for spec with no workspace entries", () => {
|
|
35
|
+
expect(specWorkspaceToInput(makeSpec())).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("converts git repo entries", () => {
|
|
39
|
+
const spec = makeSpec({
|
|
40
|
+
workspaceEntries: [
|
|
41
|
+
{
|
|
42
|
+
name: "my-repo",
|
|
43
|
+
source: {
|
|
44
|
+
source: {
|
|
45
|
+
case: "gitRepo" as const,
|
|
46
|
+
value: { url: "https://github.com/org/repo", branch: "main", commit: "", depth: 0 },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
] as unknown as SessionSpec extends undefined ? never : NonNullable<SessionSpec>["workspaceEntries"],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const result = specWorkspaceToInput(spec);
|
|
54
|
+
expect(result).toEqual([
|
|
55
|
+
{
|
|
56
|
+
name: "my-repo",
|
|
57
|
+
source: {
|
|
58
|
+
gitRepo: {
|
|
59
|
+
url: "https://github.com/org/repo",
|
|
60
|
+
branch: "main",
|
|
61
|
+
commit: undefined,
|
|
62
|
+
depth: undefined,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("converts local path entries", () => {
|
|
70
|
+
const spec = makeSpec({
|
|
71
|
+
workspaceEntries: [
|
|
72
|
+
{
|
|
73
|
+
name: "local",
|
|
74
|
+
source: {
|
|
75
|
+
source: {
|
|
76
|
+
case: "localPath" as const,
|
|
77
|
+
value: { path: "/home/user/project" },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
] as unknown as SessionSpec extends undefined ? never : NonNullable<SessionSpec>["workspaceEntries"],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const result = specWorkspaceToInput(spec);
|
|
85
|
+
expect(result).toEqual([
|
|
86
|
+
{
|
|
87
|
+
name: "local",
|
|
88
|
+
source: {
|
|
89
|
+
localPath: { path: "/home/user/project" },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
]);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("specMcpUsagesToInput", () => {
|
|
97
|
+
it("returns undefined for undefined spec", () => {
|
|
98
|
+
expect(specMcpUsagesToInput(undefined)).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("returns empty array for spec with no MCP server usages", () => {
|
|
102
|
+
expect(specMcpUsagesToInput(makeSpec())).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("converts MCP server usages with ref and enabled tools", () => {
|
|
106
|
+
const spec = makeSpec({
|
|
107
|
+
mcpServerUsages: [
|
|
108
|
+
{
|
|
109
|
+
mcpServerRef: {
|
|
110
|
+
org: "acme",
|
|
111
|
+
slug: "github-tools",
|
|
112
|
+
version: "v1",
|
|
113
|
+
kind: ApiResourceKind.mcp_server,
|
|
114
|
+
},
|
|
115
|
+
enabledTools: ["create_issue", "list_prs"],
|
|
116
|
+
toolApprovalOverrides: [],
|
|
117
|
+
},
|
|
118
|
+
] as unknown as NonNullable<SessionSpec>["mcpServerUsages"],
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const result = specMcpUsagesToInput(spec);
|
|
122
|
+
expect(result).toEqual([
|
|
123
|
+
{
|
|
124
|
+
mcpServerRef: {
|
|
125
|
+
org: "acme",
|
|
126
|
+
slug: "github-tools",
|
|
127
|
+
version: "v1",
|
|
128
|
+
kind: ApiResourceKind.mcp_server,
|
|
129
|
+
},
|
|
130
|
+
enabledTools: ["create_issue", "list_prs"],
|
|
131
|
+
toolApprovalOverrides: undefined,
|
|
132
|
+
},
|
|
133
|
+
]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("converts MCP server usages with tool approval overrides", () => {
|
|
137
|
+
const spec = makeSpec({
|
|
138
|
+
mcpServerUsages: [
|
|
139
|
+
{
|
|
140
|
+
mcpServerRef: {
|
|
141
|
+
org: "acme",
|
|
142
|
+
slug: "shell",
|
|
143
|
+
version: "",
|
|
144
|
+
kind: ApiResourceKind.mcp_server,
|
|
145
|
+
},
|
|
146
|
+
enabledTools: [],
|
|
147
|
+
toolApprovalOverrides: [
|
|
148
|
+
{ toolName: "exec", requiresApproval: true, message: "Dangerous" },
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
] as unknown as NonNullable<SessionSpec>["mcpServerUsages"],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const result = specMcpUsagesToInput(spec);
|
|
155
|
+
expect(result).toHaveLength(1);
|
|
156
|
+
expect(result![0].toolApprovalOverrides).toEqual([
|
|
157
|
+
{ toolName: "exec", requiresApproval: true, message: "Dangerous" },
|
|
158
|
+
]);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("specSkillRefsToInput", () => {
|
|
163
|
+
it("returns undefined for undefined spec", () => {
|
|
164
|
+
expect(specSkillRefsToInput(undefined)).toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("returns empty array for spec with no skill refs", () => {
|
|
168
|
+
expect(specSkillRefsToInput(makeSpec())).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("converts skill references", () => {
|
|
172
|
+
const spec = makeSpec({
|
|
173
|
+
skillRefs: [
|
|
174
|
+
{ org: "acme", slug: "code-review", version: "v2", kind: ApiResourceKind.skill },
|
|
175
|
+
{ org: "acme", slug: "testing", version: "", kind: ApiResourceKind.skill },
|
|
176
|
+
] as unknown as NonNullable<SessionSpec>["skillRefs"],
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const result = specSkillRefsToInput(spec);
|
|
180
|
+
expect(result).toEqual([
|
|
181
|
+
{ org: "acme", slug: "code-review", version: "v2", kind: ApiResourceKind.skill },
|
|
182
|
+
{ org: "acme", slug: "testing", version: undefined, kind: ApiResourceKind.skill },
|
|
183
|
+
]);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -82,7 +82,7 @@ const TEST_MODELS = parseRegistryJson({
|
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
function createWrapper() {
|
|
85
|
-
const state: ModelRegistryState = { models: TEST_MODELS, isLoading: false, error: null };
|
|
85
|
+
const state: ModelRegistryState = { models: TEST_MODELS, isLoading: false, error: null, refetch: () => {} };
|
|
86
86
|
return function Wrapper({ children }: { children: ReactNode }) {
|
|
87
87
|
return (
|
|
88
88
|
<ModelRegistryContext.Provider value={state}>
|
|
@@ -284,7 +284,7 @@ describe("useNewSessionFlow", () => {
|
|
|
284
284
|
describe("model validation timing", () => {
|
|
285
285
|
it("does not restore model while registry is loading", () => {
|
|
286
286
|
localStorage.setItem(STORAGE_KEY_MODEL_NATIVE, DEFAULT_MODEL_ID);
|
|
287
|
-
const loadingState: ModelRegistryState = { models: [], isLoading: true, error: null };
|
|
287
|
+
const loadingState: ModelRegistryState = { models: [], isLoading: true, error: null, refetch: () => {} };
|
|
288
288
|
|
|
289
289
|
function LoadingWrapper({ children }: { children: ReactNode }) {
|
|
290
290
|
return (
|
|
@@ -14,7 +14,7 @@ const TEST_MODELS = parseRegistryJson({
|
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
function createWrapper() {
|
|
17
|
-
const state: ModelRegistryState = { models: TEST_MODELS, isLoading: false, error: null };
|
|
17
|
+
const state: ModelRegistryState = { models: TEST_MODELS, isLoading: false, error: null, refetch: () => {} };
|
|
18
18
|
return function Wrapper({ children }: { children: ReactNode }) {
|
|
19
19
|
return (
|
|
20
20
|
<ModelRegistryContext.Provider value={state}>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
|
|
2
|
+
import type { SearchResult } from "@stigmer/protos/ai/stigmer/search/v1/io_pb";
|
|
2
3
|
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
|
3
4
|
|
|
4
5
|
/** A time-based group of sessions produced by {@link groupSessionsByTime}. */
|
|
@@ -9,11 +10,24 @@ export interface SessionGroup {
|
|
|
9
10
|
readonly sessions: readonly Session[];
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
/** A time-based group of search results produced by {@link groupSearchResultsByTime}. */
|
|
14
|
+
export interface SearchResultGroup {
|
|
15
|
+
/** Display label for this group (e.g. "Today", "Yesterday"). */
|
|
16
|
+
readonly label: string;
|
|
17
|
+
/** Search results belonging to this group, in their original input order. */
|
|
18
|
+
readonly entries: readonly SearchResult[];
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
interface Bucket {
|
|
13
22
|
label: string;
|
|
14
23
|
sessions: Session[];
|
|
15
24
|
}
|
|
16
25
|
|
|
26
|
+
interface SearchResultBucket {
|
|
27
|
+
label: string;
|
|
28
|
+
entries: SearchResult[];
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
/**
|
|
18
32
|
* Groups sessions by their creation timestamp into time-based buckets:
|
|
19
33
|
* "Today", "Yesterday", "Previous 7 Days", "Previous 30 Days", "Older".
|
|
@@ -91,6 +105,57 @@ export function groupSessionsByTime(
|
|
|
91
105
|
return buckets.filter((b) => b.sessions.length > 0);
|
|
92
106
|
}
|
|
93
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Groups SearchResult entries by their `createdAt` timestamp into time-based
|
|
110
|
+
* buckets, identical to {@link groupSessionsByTime} but for lightweight
|
|
111
|
+
* search results.
|
|
112
|
+
*
|
|
113
|
+
* @param entries - SearchResult entries (from SearchService session search).
|
|
114
|
+
* @param now - Reference time for grouping; defaults to current time.
|
|
115
|
+
*/
|
|
116
|
+
export function groupSearchResultsByTime(
|
|
117
|
+
entries: readonly SearchResult[],
|
|
118
|
+
now?: Date,
|
|
119
|
+
): readonly SearchResultGroup[] {
|
|
120
|
+
const ref = now ?? new Date();
|
|
121
|
+
const todayStart = startOfDay(ref);
|
|
122
|
+
const yesterdayStart = addDays(todayStart, -1);
|
|
123
|
+
const sevenDaysAgo = addDays(todayStart, -6);
|
|
124
|
+
const thirtyDaysAgo = addDays(todayStart, -29);
|
|
125
|
+
|
|
126
|
+
const buckets: SearchResultBucket[] = [
|
|
127
|
+
{ label: "Today", entries: [] },
|
|
128
|
+
{ label: "Yesterday", entries: [] },
|
|
129
|
+
{ label: "Previous 7 Days", entries: [] },
|
|
130
|
+
{ label: "Previous 30 Days", entries: [] },
|
|
131
|
+
{ label: "Older", entries: [] },
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
const ts = entry.createdAt;
|
|
136
|
+
const date = ts ? timestampDate(ts) : null;
|
|
137
|
+
|
|
138
|
+
if (!date) {
|
|
139
|
+
buckets[buckets.length - 1].entries.push(entry);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (date >= todayStart) {
|
|
144
|
+
buckets[0].entries.push(entry);
|
|
145
|
+
} else if (date >= yesterdayStart) {
|
|
146
|
+
buckets[1].entries.push(entry);
|
|
147
|
+
} else if (date >= sevenDaysAgo) {
|
|
148
|
+
buckets[2].entries.push(entry);
|
|
149
|
+
} else if (date >= thirtyDaysAgo) {
|
|
150
|
+
buckets[3].entries.push(entry);
|
|
151
|
+
} else {
|
|
152
|
+
buckets[4].entries.push(entry);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return buckets.filter((b) => b.entries.length > 0);
|
|
157
|
+
}
|
|
158
|
+
|
|
94
159
|
function startOfDay(date: Date): Date {
|
|
95
160
|
const d = new Date(date);
|
|
96
161
|
d.setHours(0, 0, 0, 0);
|
package/src/session/index.ts
CHANGED
|
@@ -76,8 +76,14 @@ export type {
|
|
|
76
76
|
DraftParams,
|
|
77
77
|
} from "./draft";
|
|
78
78
|
|
|
79
|
-
export { groupSessionsByTime } from "./group-sessions";
|
|
80
|
-
export type { SessionGroup } from "./group-sessions";
|
|
79
|
+
export { groupSessionsByTime, groupSearchResultsByTime } from "./group-sessions";
|
|
80
|
+
export type { SessionGroup, SearchResultGroup } from "./group-sessions";
|
|
81
|
+
|
|
82
|
+
export { useSessionSearch } from "./useSessionSearch";
|
|
83
|
+
export type {
|
|
84
|
+
UseSessionSearchOptions,
|
|
85
|
+
UseSessionSearchReturn,
|
|
86
|
+
} from "./useSessionSearch";
|
|
81
87
|
|
|
82
88
|
// Session utilities (re-exported from @stigmer/sdk)
|
|
83
89
|
export { PENDING_SUBJECT, resolvedSubject } from "@stigmer/sdk";
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
|
|
2
|
+
import type {
|
|
3
|
+
McpServerUsageInput,
|
|
4
|
+
ResourceRef,
|
|
5
|
+
WorkspaceEntryInput,
|
|
6
|
+
} from "@stigmer/sdk";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Convert proto workspace entries back to SDK input format.
|
|
10
|
+
*
|
|
11
|
+
* Used by `useSessionConversation` (session update with replace semantics)
|
|
12
|
+
* and `useSessionPageFlow` (hydrating composer state from a loaded session).
|
|
13
|
+
*/
|
|
14
|
+
export function specWorkspaceToInput(
|
|
15
|
+
spec: Session["spec"],
|
|
16
|
+
): WorkspaceEntryInput[] | undefined {
|
|
17
|
+
return spec?.workspaceEntries?.map((e): WorkspaceEntryInput => {
|
|
18
|
+
if (e.source?.source.case === "gitRepo") {
|
|
19
|
+
const v = e.source.source.value;
|
|
20
|
+
return {
|
|
21
|
+
name: e.name || undefined,
|
|
22
|
+
source: {
|
|
23
|
+
gitRepo: {
|
|
24
|
+
url: v.url,
|
|
25
|
+
branch: v.branch || undefined,
|
|
26
|
+
commit: v.commit || undefined,
|
|
27
|
+
depth: v.depth || undefined,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (e.source?.source.case === "localPath") {
|
|
33
|
+
return {
|
|
34
|
+
name: e.name || undefined,
|
|
35
|
+
source: {
|
|
36
|
+
localPath: { path: e.source.source.value.path || undefined },
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return { name: e.name || undefined, source: {} };
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Convert proto MCP server usages back to SDK input format.
|
|
46
|
+
*
|
|
47
|
+
* Used by `useSessionConversation` (session update with replace semantics)
|
|
48
|
+
* and `useSessionPageFlow` (hydrating composer state from a loaded session).
|
|
49
|
+
*/
|
|
50
|
+
export function specMcpUsagesToInput(
|
|
51
|
+
spec: Session["spec"],
|
|
52
|
+
): McpServerUsageInput[] | undefined {
|
|
53
|
+
return spec?.mcpServerUsages?.map((u) => ({
|
|
54
|
+
mcpServerRef: {
|
|
55
|
+
org: u.mcpServerRef?.org ?? "",
|
|
56
|
+
slug: u.mcpServerRef?.slug ?? "",
|
|
57
|
+
version: u.mcpServerRef?.version || undefined,
|
|
58
|
+
kind: u.mcpServerRef?.kind,
|
|
59
|
+
},
|
|
60
|
+
enabledTools: u.enabledTools?.length ? [...u.enabledTools] : undefined,
|
|
61
|
+
toolApprovalOverrides: u.toolApprovalOverrides?.length
|
|
62
|
+
? u.toolApprovalOverrides.map((o) => ({
|
|
63
|
+
toolName: o.toolName || undefined,
|
|
64
|
+
requiresApproval: o.requiresApproval || undefined,
|
|
65
|
+
message: o.message || undefined,
|
|
66
|
+
}))
|
|
67
|
+
: undefined,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Convert proto skill references back to SDK input format.
|
|
73
|
+
*
|
|
74
|
+
* Used by `useSessionConversation` (session update with replace semantics)
|
|
75
|
+
* and `useSessionPageFlow` (hydrating composer state from a loaded session).
|
|
76
|
+
*/
|
|
77
|
+
export function specSkillRefsToInput(
|
|
78
|
+
spec: Session["spec"],
|
|
79
|
+
): ResourceRef[] | undefined {
|
|
80
|
+
return spec?.skillRefs?.map((r) => ({
|
|
81
|
+
org: r.org ?? "",
|
|
82
|
+
slug: r.slug ?? "",
|
|
83
|
+
version: r.version || undefined,
|
|
84
|
+
kind: r.kind,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
@@ -24,6 +24,11 @@ import { useSubmitApproval } from "../execution/useSubmitApproval";
|
|
|
24
24
|
import { useSession } from "./useSession";
|
|
25
25
|
import { useSessionExecutions } from "./useSessionExecutions";
|
|
26
26
|
import { useUpdateSession } from "./useUpdateSession";
|
|
27
|
+
import {
|
|
28
|
+
specWorkspaceToInput,
|
|
29
|
+
specMcpUsagesToInput,
|
|
30
|
+
specSkillRefsToInput,
|
|
31
|
+
} from "./session-spec-converters";
|
|
27
32
|
|
|
28
33
|
/**
|
|
29
34
|
* Options for {@link UseSessionConversationReturn.sendFollowUp}.
|
|
@@ -479,67 +484,3 @@ function buildUpdateInput(
|
|
|
479
484
|
};
|
|
480
485
|
}
|
|
481
486
|
|
|
482
|
-
/** Convert proto workspace entries back to SDK input format. */
|
|
483
|
-
function specWorkspaceToInput(
|
|
484
|
-
spec: Session["spec"],
|
|
485
|
-
): WorkspaceEntryInput[] | undefined {
|
|
486
|
-
return spec?.workspaceEntries?.map((e): WorkspaceEntryInput => {
|
|
487
|
-
if (e.source?.source.case === "gitRepo") {
|
|
488
|
-
const v = e.source.source.value;
|
|
489
|
-
return {
|
|
490
|
-
name: e.name || undefined,
|
|
491
|
-
source: {
|
|
492
|
-
gitRepo: {
|
|
493
|
-
url: v.url,
|
|
494
|
-
branch: v.branch || undefined,
|
|
495
|
-
commit: v.commit || undefined,
|
|
496
|
-
depth: v.depth || undefined,
|
|
497
|
-
},
|
|
498
|
-
},
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
if (e.source?.source.case === "localPath") {
|
|
502
|
-
return {
|
|
503
|
-
name: e.name || undefined,
|
|
504
|
-
source: {
|
|
505
|
-
localPath: { path: e.source.source.value.path || undefined },
|
|
506
|
-
},
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
return { name: e.name || undefined, source: {} };
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/** Convert proto MCP server usages back to SDK input format. */
|
|
514
|
-
function specMcpUsagesToInput(
|
|
515
|
-
spec: Session["spec"],
|
|
516
|
-
): McpServerUsageInput[] | undefined {
|
|
517
|
-
return spec?.mcpServerUsages?.map((u) => ({
|
|
518
|
-
mcpServerRef: {
|
|
519
|
-
org: u.mcpServerRef?.org ?? "",
|
|
520
|
-
slug: u.mcpServerRef?.slug ?? "",
|
|
521
|
-
version: u.mcpServerRef?.version || undefined,
|
|
522
|
-
kind: u.mcpServerRef?.kind,
|
|
523
|
-
},
|
|
524
|
-
enabledTools: u.enabledTools?.length ? [...u.enabledTools] : undefined,
|
|
525
|
-
toolApprovalOverrides: u.toolApprovalOverrides?.length
|
|
526
|
-
? u.toolApprovalOverrides.map((o) => ({
|
|
527
|
-
toolName: o.toolName || undefined,
|
|
528
|
-
requiresApproval: o.requiresApproval || undefined,
|
|
529
|
-
message: o.message || undefined,
|
|
530
|
-
}))
|
|
531
|
-
: undefined,
|
|
532
|
-
}));
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/** Convert proto skill references back to SDK input format. */
|
|
536
|
-
function specSkillRefsToInput(
|
|
537
|
-
spec: Session["spec"],
|
|
538
|
-
): ResourceRef[] | undefined {
|
|
539
|
-
return spec?.skillRefs?.map((r) => ({
|
|
540
|
-
org: r.org ?? "",
|
|
541
|
-
slug: r.slug ?? "",
|
|
542
|
-
version: r.version || undefined,
|
|
543
|
-
kind: r.kind,
|
|
544
|
-
}));
|
|
545
|
-
}
|