@tonyclaw/agent-inspector 2.0.4 → 2.0.6

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 (61) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-BCH_fsLm.js → CompareDrawer-DDmqSAfl.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-Cxpdziwd.js +101 -0
  4. package/.output/public/assets/ReplayDialog-Bt5DGzlh.js +1 -0
  5. package/.output/public/assets/RequestAnatomy-BxX3_N9S.js +1 -0
  6. package/.output/public/assets/ResponseView-Bl_5S9gZ.js +1 -0
  7. package/.output/public/assets/StreamingChunkSequence-RJMwNf6F.js +1 -0
  8. package/.output/public/assets/_sessionId-b4isaoDp.js +1 -0
  9. package/.output/public/assets/index-BZ4x5UI6.js +1 -0
  10. package/.output/public/assets/{index-CobXD0yH.css → index-C624DUk9.css} +1 -1
  11. package/.output/public/assets/{json-viewer-BrzjD7qI.js → json-viewer-CRL_gWEZ.js} +1 -1
  12. package/.output/public/assets/{main-mgxeUdZQ.js → main-CKnTJ4-O.js} +6 -6
  13. package/.output/server/_libs/lucide-react.mjs +181 -114
  14. package/.output/server/{_sessionId-C4xsxIWm.mjs → _sessionId-B-x9fRY3.mjs} +3 -3
  15. package/.output/server/_ssr/{CompareDrawer-DuWEpqQ7.mjs → CompareDrawer-BQVNsAY2.mjs} +6 -6
  16. package/.output/server/_ssr/{ProxyViewerContainer-Cckz5qKu.mjs → ProxyViewerContainer-CYm2Dw19.mjs} +766 -122
  17. package/.output/server/_ssr/{ReplayDialog-BDRcr8E5.mjs → ReplayDialog-CaMQBc79.mjs} +240 -14
  18. package/.output/server/_ssr/{RequestAnatomy-BoO2_Ij0.mjs → RequestAnatomy--P5arRH2.mjs} +236 -66
  19. package/.output/server/_ssr/{ResponseView-DZiPBxvO.mjs → ResponseView-RtFwNvgD.mjs} +8 -8
  20. package/.output/server/_ssr/{StreamingChunkSequence-D-be7KEL.mjs → StreamingChunkSequence-B5HPkzab.mjs} +3 -3
  21. package/.output/server/_ssr/{index-5RImHKfu.mjs → index-CZIKZU43.mjs} +2 -2
  22. package/.output/server/_ssr/index.mjs +2 -2
  23. package/.output/server/_ssr/{json-viewer-aJhb93ZK.mjs → json-viewer-d4obyRaA.mjs} +3 -3
  24. package/.output/server/_ssr/{router-Dgkv5nKP.mjs → router-DGPt3MUc.mjs} +145 -71
  25. package/.output/server/_tanstack-start-manifest_v-BzH4pNaI.mjs +4 -0
  26. package/.output/server/index.mjs +64 -64
  27. package/package.json +1 -1
  28. package/src/components/OnboardingBanner.tsx +11 -19
  29. package/src/components/ProxyViewer.tsx +1 -1
  30. package/src/components/providers/ProviderCard.tsx +6 -20
  31. package/src/components/providers/SettingsDialog.tsx +95 -2
  32. package/src/components/proxy-viewer/AgentTraceSummary.tsx +639 -38
  33. package/src/components/proxy-viewer/CompareDrawer.tsx +4 -2
  34. package/src/components/proxy-viewer/LogEntry.tsx +4 -4
  35. package/src/components/proxy-viewer/LogEntryHeader.tsx +15 -25
  36. package/src/components/proxy-viewer/ReplayDialog.tsx +190 -8
  37. package/src/components/proxy-viewer/ResponseView.tsx +2 -2
  38. package/src/components/proxy-viewer/ToolTraceEvents.tsx +37 -16
  39. package/src/components/proxy-viewer/TurnGroup.tsx +14 -2
  40. package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +196 -45
  41. package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +92 -67
  42. package/src/components/proxy-viewer/anatomy/types.ts +15 -13
  43. package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +2 -2
  44. package/src/components/proxy-viewer/log-formats/anthropic.ts +1 -1
  45. package/src/components/proxy-viewer/log-formats/openai.ts +1 -1
  46. package/src/components/proxy-viewer/log-formats/types.ts +1 -1
  47. package/src/components/proxy-viewer/replayComparison.ts +131 -0
  48. package/src/components/proxy-viewer/useKeyboardNavigation.ts +64 -22
  49. package/src/components/proxy-viewer/viewerState.ts +14 -2
  50. package/src/components/ui/json-viewer.tsx +1 -1
  51. package/src/knowledge/candidateStore.ts +32 -1
  52. package/src/routes/api/knowledge.candidates.$candidateId.ts +50 -0
  53. package/src/routes/api/knowledge.sessions.$sessionId.candidates.ts +12 -2
  54. package/.output/public/assets/ProxyViewerContainer-D85_UANk.js +0 -101
  55. package/.output/public/assets/ReplayDialog-DTeaHHit.js +0 -1
  56. package/.output/public/assets/RequestAnatomy-DZ8grAih.js +0 -1
  57. package/.output/public/assets/ResponseView-Cldm6RCi.js +0 -1
  58. package/.output/public/assets/StreamingChunkSequence-3x4p-yT7.js +0 -1
  59. package/.output/public/assets/_sessionId-YqWFBu6d.js +0 -1
  60. package/.output/public/assets/index-BIw2H6jO.js +0 -1
  61. package/.output/server/_tanstack-start-manifest_v-B8rrWXjr.mjs +0 -4
@@ -0,0 +1,131 @@
1
+ export type ReplayMetricValue = number | boolean | null;
2
+
3
+ export type ReplayMetrics = {
4
+ status: number | null;
5
+ elapsedMs: number | null;
6
+ inputTokens: number | null;
7
+ outputTokens: number | null;
8
+ responseBytes: number | null;
9
+ streaming: boolean;
10
+ };
11
+
12
+ export type ReplayMetricComparison = {
13
+ id: "status" | "elapsed" | "input" | "output" | "bytes" | "streaming";
14
+ label: string;
15
+ original: ReplayMetricValue;
16
+ replay: ReplayMetricValue;
17
+ delta: number | null;
18
+ };
19
+
20
+ type JsonObject = Record<string, unknown>;
21
+
22
+ function isJsonObject(value: unknown): value is JsonObject {
23
+ return typeof value === "object" && value !== null && !Array.isArray(value);
24
+ }
25
+
26
+ function byteLength(value: string | null | undefined): number | null {
27
+ if (value === null || value === undefined) return null;
28
+ return new TextEncoder().encode(value).length;
29
+ }
30
+
31
+ export function readRequestModel(body: string): string | null {
32
+ try {
33
+ const parsed: unknown = JSON.parse(body);
34
+ if (!isJsonObject(parsed)) return null;
35
+ const model = parsed["model"];
36
+ return typeof model === "string" && model.length > 0 ? model : null;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ export function replaceRequestModel(
43
+ body: string,
44
+ model: string,
45
+ ): { body: string; error: string | null } {
46
+ try {
47
+ const parsed: unknown = JSON.parse(body);
48
+ if (!isJsonObject(parsed)) {
49
+ return { body, error: "Request body must be a JSON object." };
50
+ }
51
+ return {
52
+ body: JSON.stringify({ ...parsed, model }, null, 2),
53
+ error: null,
54
+ };
55
+ } catch {
56
+ return { body, error: "Request body must be valid JSON before changing the replay model." };
57
+ }
58
+ }
59
+
60
+ export function buildReplayMetrics(input: {
61
+ status: number | null | undefined;
62
+ elapsedMs: number | null | undefined;
63
+ inputTokens: number | null | undefined;
64
+ outputTokens: number | null | undefined;
65
+ responseText: string | null | undefined;
66
+ streaming: boolean | null | undefined;
67
+ }): ReplayMetrics {
68
+ return {
69
+ status: input.status ?? null,
70
+ elapsedMs: input.elapsedMs ?? null,
71
+ inputTokens: input.inputTokens ?? null,
72
+ outputTokens: input.outputTokens ?? null,
73
+ responseBytes: byteLength(input.responseText),
74
+ streaming: input.streaming === true,
75
+ };
76
+ }
77
+
78
+ function numericDelta(original: number | null, replay: number | null): number | null {
79
+ if (original === null || replay === null) return null;
80
+ return replay - original;
81
+ }
82
+
83
+ export function buildReplayComparisons(
84
+ original: ReplayMetrics,
85
+ replay: ReplayMetrics,
86
+ ): ReplayMetricComparison[] {
87
+ return [
88
+ {
89
+ id: "status",
90
+ label: "Status",
91
+ original: original.status,
92
+ replay: replay.status,
93
+ delta: numericDelta(original.status, replay.status),
94
+ },
95
+ {
96
+ id: "elapsed",
97
+ label: "Elapsed",
98
+ original: original.elapsedMs,
99
+ replay: replay.elapsedMs,
100
+ delta: numericDelta(original.elapsedMs, replay.elapsedMs),
101
+ },
102
+ {
103
+ id: "input",
104
+ label: "Input",
105
+ original: original.inputTokens,
106
+ replay: replay.inputTokens,
107
+ delta: numericDelta(original.inputTokens, replay.inputTokens),
108
+ },
109
+ {
110
+ id: "output",
111
+ label: "Output",
112
+ original: original.outputTokens,
113
+ replay: replay.outputTokens,
114
+ delta: numericDelta(original.outputTokens, replay.outputTokens),
115
+ },
116
+ {
117
+ id: "bytes",
118
+ label: "Bytes",
119
+ original: original.responseBytes,
120
+ replay: replay.responseBytes,
121
+ delta: numericDelta(original.responseBytes, replay.responseBytes),
122
+ },
123
+ {
124
+ id: "streaming",
125
+ label: "Stream",
126
+ original: original.streaming,
127
+ replay: replay.streaming,
128
+ delta: null,
129
+ },
130
+ ];
131
+ }
@@ -5,6 +5,11 @@ const NAV_ACTION_ATTR = "data-nav-action";
5
5
 
6
6
  export type NavAction = "toggle" | "expand" | "collapse";
7
7
 
8
+ type KeyboardNavigationOptions = {
9
+ /** When true, Arrow/WASD navigation works even when focus is on the page body. */
10
+ pageWide?: boolean;
11
+ };
12
+
8
13
  function findNavItems(container: HTMLElement): HTMLElement[] {
9
14
  return Array.from(container.querySelectorAll<HTMLElement>(`[${NAV_ATTR}]`));
10
15
  }
@@ -41,18 +46,40 @@ function safeItemAt(items: HTMLElement[], index: number): HTMLElement | null {
41
46
 
42
47
  function isEditableTarget(target: HTMLElement): boolean {
43
48
  const tag = target.tagName;
44
- if (tag === "INPUT" || tag === "TEXTAREA") return true;
49
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
45
50
  if (target.isContentEditable) return true;
46
51
  return false;
47
52
  }
48
53
 
54
+ function isInteractiveTarget(target: HTMLElement): boolean {
55
+ const tag = target.tagName;
56
+ if (tag === "BUTTON" || tag === "A") return true;
57
+ if (isEditableTarget(target)) return true;
58
+ return target.closest("[role='button'],[role='menuitem'],[role='option']") !== null;
59
+ }
60
+
61
+ function isInOpenOverlay(target: HTMLElement): boolean {
62
+ if (target.closest("[role='dialog'],[role='menu'],[role='listbox']") !== null) return true;
63
+ return document.querySelector("[role='dialog'],[role='menu'],[role='listbox']") !== null;
64
+ }
65
+
66
+ function hasActiveTextSelection(): boolean {
67
+ const selection = window.getSelection();
68
+ if (selection === null) return false;
69
+ return selection.type === "Range" && selection.toString().length > 0;
70
+ }
71
+
72
+ function isLetterNavigationKey(event: KeyboardEvent, key: "W" | "A" | "S" | "D"): boolean {
73
+ return event.key === key && event.shiftKey;
74
+ }
75
+
49
76
  /**
50
77
  * Enables keyboard navigation within a log-list container.
51
78
  *
52
79
  * Navigation keys:
53
- * - ArrowUp / Shift+W : previous item
54
- * - ArrowDown / Shift+S : next item
55
- * - ArrowLeft / Shift+A : collapse or move to prev item
80
+ * - ArrowUp / Shift+W : previous thread/turn/log item
81
+ * - ArrowDown / Shift+S : next thread/turn/log item
82
+ * - ArrowLeft / Shift+A : collapse or move to previous item
56
83
  * - ArrowRight / Shift+D : expand or move to next item
57
84
  * - Space : toggle expand/collapse
58
85
  *
@@ -66,17 +93,20 @@ export function useKeyboardNavigation(
66
93
  /** Ref to the outer focus-receiving wrapper (the one with tabIndex).
67
94
  * Defaults to `containerRef` when omitted. */
68
95
  wrapperRef?: React.RefObject<HTMLElement | null>,
96
+ options: KeyboardNavigationOptions = {},
69
97
  ): void {
70
98
  const rootRef = wrapperRef ?? containerRef;
99
+ const pageWide = options.pageWide === true;
71
100
 
72
101
  const handleFocusContainer = useCallback(
73
102
  (e: FocusEvent) => {
74
103
  const container = containerRef.current;
104
+ const root = rootRef.current;
75
105
  if (!container) return;
106
+ if (!root) return;
76
107
  const target = e.target;
77
108
  if (!isFocusTarget(target)) return;
78
- if (target !== rootRef.current) return;
79
- if (!container.contains(target)) return;
109
+ if (target !== root) return;
80
110
  const items = findNavItems(container);
81
111
  const first = safeItemAt(items, 0);
82
112
  if (first !== null) focusAndScroll(first);
@@ -85,17 +115,27 @@ export function useKeyboardNavigation(
85
115
  );
86
116
 
87
117
  useEffect(() => {
88
- const root = rootRef.current;
89
- const container = containerRef.current;
90
- if (!root || !container) return;
91
-
92
118
  const handleKeyDown = (e: KeyboardEvent) => {
119
+ if (e.altKey || e.ctrlKey || e.metaKey) return;
120
+
121
+ const container = containerRef.current;
122
+ if (!container) return;
123
+
93
124
  const target = e.target;
94
125
  if (!isFocusTarget(target)) return;
95
126
 
96
- if (!container.contains(target)) return;
97
-
98
127
  if (isEditableTarget(target)) return;
128
+ if (hasActiveTextSelection()) return;
129
+
130
+ const isInsideContainer = container.contains(target);
131
+ if (isInsideContainer && isInteractiveTarget(target) && !target.hasAttribute(NAV_ATTR)) {
132
+ return;
133
+ }
134
+
135
+ if (!isInsideContainer) {
136
+ if (!pageWide) return;
137
+ if (isInteractiveTarget(target) || isInOpenOverlay(target)) return;
138
+ }
99
139
 
100
140
  const items = findNavItems(container);
101
141
  if (items.length === 0) return;
@@ -108,7 +148,8 @@ export function useKeyboardNavigation(
108
148
  switch (e.key) {
109
149
  case "ArrowUp":
110
150
  case "W": {
111
- if (e.key === "W" && !e.shiftKey) break;
151
+ if (e.shiftKey && e.key === "ArrowUp") break;
152
+ if (e.key === "W" && !isLetterNavigationKey(e, "W")) break;
112
153
  e.preventDefault();
113
154
  const prevIdx =
114
155
  currentIdx > 0 ? currentIdx - 1 : currentIdx === -1 ? items.length - 1 : -1;
@@ -121,7 +162,8 @@ export function useKeyboardNavigation(
121
162
  }
122
163
  case "ArrowDown":
123
164
  case "S": {
124
- if (e.key === "S" && !e.shiftKey) break;
165
+ if (e.shiftKey && e.key === "ArrowDown") break;
166
+ if (e.key === "S" && !isLetterNavigationKey(e, "S")) break;
125
167
  e.preventDefault();
126
168
  const nextIdx =
127
169
  currentIdx < items.length - 1 ? currentIdx + 1 : currentIdx === -1 ? 0 : -1;
@@ -134,7 +176,8 @@ export function useKeyboardNavigation(
134
176
  }
135
177
  case "ArrowLeft":
136
178
  case "A": {
137
- if (e.key === "A" && !e.shiftKey) break;
179
+ if (e.shiftKey && e.key === "ArrowLeft") break;
180
+ if (e.key === "A" && !isLetterNavigationKey(e, "A")) break;
138
181
  if (current === null) break;
139
182
  e.preventDefault();
140
183
  const action = getAction(current);
@@ -149,7 +192,8 @@ export function useKeyboardNavigation(
149
192
  }
150
193
  case "ArrowRight":
151
194
  case "D": {
152
- if (e.key === "D" && !e.shiftKey) break;
195
+ if (e.shiftKey && e.key === "ArrowRight") break;
196
+ if (e.key === "D" && !isLetterNavigationKey(e, "D")) break;
153
197
  if (current === null) break;
154
198
  e.preventDefault();
155
199
  const action = getAction(current);
@@ -179,12 +223,10 @@ export function useKeyboardNavigation(
179
223
 
180
224
  document.addEventListener("keydown", handleKeyDown, { capture: true });
181
225
  return () => document.removeEventListener("keydown", handleKeyDown, { capture: true });
182
- }, [containerRef, rootRef]);
226
+ }, [containerRef, pageWide, rootRef]);
183
227
 
184
228
  useEffect(() => {
185
- const root = rootRef.current;
186
- if (!root) return;
187
- root.addEventListener("focus", handleFocusContainer);
188
- return () => root.removeEventListener("focus", handleFocusContainer);
189
- }, [handleFocusContainer, rootRef]);
229
+ document.addEventListener("focusin", handleFocusContainer);
230
+ return () => document.removeEventListener("focusin", handleFocusContainer);
231
+ }, [handleFocusContainer]);
190
232
  }
@@ -40,6 +40,7 @@ export type ToolTraceEvent = {
40
40
  index: number;
41
41
  provider: "anthropic" | "openai";
42
42
  name: string;
43
+ argumentsText: string | null;
43
44
  argumentsPreview: string | null;
44
45
  };
45
46
 
@@ -118,6 +119,13 @@ function previewValue(value: unknown): string | null {
118
119
  : normalized;
119
120
  }
120
121
 
122
+ function copyValue(value: unknown): string | null {
123
+ if (value === undefined || value === null) return null;
124
+ if (typeof value === "string") return value.length > 0 ? value : null;
125
+ const raw = JSON.stringify(value, null, 2);
126
+ return raw === undefined || raw.length === 0 ? null : raw;
127
+ }
128
+
121
129
  function extractAnthropicToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
122
130
  const parsed = parseJsonResponse(log.responseText);
123
131
  const content = safeGetOwnProperty(parsed, "content");
@@ -129,13 +137,15 @@ function extractAnthropicToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
129
137
  if (type !== "tool_use") continue;
130
138
  const name = safeGetOwnProperty(block, "name");
131
139
  if (typeof name !== "string" || name.length === 0) continue;
140
+ const input = safeGetOwnProperty(block, "input");
132
141
  events.push({
133
142
  id: `${String(log.id)}-anthropic-tool-${String(events.length)}`,
134
143
  logId: log.id,
135
144
  index: events.length,
136
145
  provider: "anthropic",
137
146
  name,
138
- argumentsPreview: previewValue(safeGetOwnProperty(block, "input")),
147
+ argumentsText: copyValue(input),
148
+ argumentsPreview: previewValue(input),
139
149
  });
140
150
  }
141
151
  return events;
@@ -155,13 +165,15 @@ function extractOpenAIToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
155
165
  const fn = safeGetOwnProperty(call, "function");
156
166
  const name = safeGetOwnProperty(fn, "name");
157
167
  if (typeof name !== "string" || name.length === 0) continue;
168
+ const args = safeGetOwnProperty(fn, "arguments");
158
169
  events.push({
159
170
  id: `${String(log.id)}-openai-tool-${String(events.length)}`,
160
171
  logId: log.id,
161
172
  index: events.length,
162
173
  provider: "openai",
163
174
  name,
164
- argumentsPreview: previewValue(safeGetOwnProperty(fn, "arguments")),
175
+ argumentsText: copyValue(args),
176
+ argumentsPreview: previewValue(args),
165
177
  });
166
178
  }
167
179
  }
@@ -380,7 +380,7 @@ export type JsonViewerProps = {
380
380
  bulkRevision?: number;
381
381
  /**
382
382
  * Set of JSON-pointer-style paths whose row should expose a
383
- * `data-anatomy-path` attribute. Used by the Anatomy view to
383
+ * `data-anatomy-path` attribute. Used by the Context view to
384
384
  * locate rows for click-to-jump. When `null` or `undefined`,
385
385
  * no `data-anatomy-path` attribute is emitted (zero cost).
386
386
  */
@@ -1,4 +1,12 @@
1
- import type { KnowledgeCandidate } from "./types";
1
+ import type { KnowledgeCandidate, KnowledgeCandidateType } from "./types";
2
+ import { redactCandidate } from "./redactor";
3
+
4
+ export type CandidateDraftUpdate = {
5
+ type?: KnowledgeCandidateType;
6
+ title?: string;
7
+ content?: string;
8
+ tags?: string[];
9
+ };
2
10
 
3
11
  const candidates = new Map<string, KnowledgeCandidate>();
4
12
 
@@ -58,6 +66,29 @@ export function markCandidateFailed(id: string, error: string): KnowledgeCandida
58
66
  return updated;
59
67
  }
60
68
 
69
+ export function updateCandidateDraft(
70
+ id: string,
71
+ updates: CandidateDraftUpdate,
72
+ ): KnowledgeCandidate | null {
73
+ const existing = candidates.get(id);
74
+ if (existing === undefined) return null;
75
+ if (existing.status === "promoted") return null;
76
+
77
+ const updated = redactCandidate({
78
+ ...existing,
79
+ type: updates.type ?? existing.type,
80
+ title: updates.title ?? existing.title,
81
+ content: updates.content ?? existing.content,
82
+ tags: updates.tags ?? existing.tags,
83
+ status: "draft",
84
+ error: null,
85
+ openClawMemoryId: null,
86
+ updatedAt: new Date().toISOString(),
87
+ });
88
+ candidates.set(id, updated);
89
+ return updated;
90
+ }
91
+
61
92
  export function _resetCandidatesForTests(): void {
62
93
  candidates.clear();
63
94
  }
@@ -0,0 +1,50 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { z } from "zod";
3
+ import { getCandidate, updateCandidateDraft } from "../../knowledge/candidateStore";
4
+ import { KnowledgeCandidateSchema, KnowledgeCandidateTypeSchema } from "../../knowledge/types";
5
+
6
+ const CandidateUpdateSchema = z
7
+ .object({
8
+ type: KnowledgeCandidateTypeSchema.optional(),
9
+ title: z.string().trim().min(1).max(160).optional(),
10
+ content: z.string().trim().min(1).max(12000).optional(),
11
+ tags: z.array(z.string().trim().min(1).max(64)).max(24).optional(),
12
+ })
13
+ .strict();
14
+
15
+ const CandidateUpdateResponseSchema = z.object({
16
+ candidate: KnowledgeCandidateSchema,
17
+ });
18
+
19
+ export type CandidateUpdateResponse = z.infer<typeof CandidateUpdateResponseSchema>;
20
+
21
+ export const Route = createFileRoute("/api/knowledge/candidates/$candidateId")({
22
+ server: {
23
+ handlers: {
24
+ PATCH: async ({ params, request }: { params: { candidateId: string }; request: Request }) => {
25
+ const existing = getCandidate(params.candidateId);
26
+ if (existing === null) {
27
+ return Response.json({ error: "Knowledge candidate not found" }, { status: 404 });
28
+ }
29
+ if (existing.status === "promoted") {
30
+ return Response.json(
31
+ { error: "Promoted knowledge candidates cannot be edited" },
32
+ { status: 409 },
33
+ );
34
+ }
35
+
36
+ const raw: unknown = await request.json().catch(() => null);
37
+ const parsed = CandidateUpdateSchema.safeParse(raw);
38
+ if (!parsed.success) {
39
+ return Response.json({ error: parsed.error.message }, { status: 400 });
40
+ }
41
+
42
+ const updated = updateCandidateDraft(existing.id, parsed.data);
43
+ if (updated === null) {
44
+ return Response.json({ error: "Knowledge candidate update failed" }, { status: 409 });
45
+ }
46
+ return Response.json({ candidate: updated } satisfies CandidateUpdateResponse);
47
+ },
48
+ },
49
+ },
50
+ });
@@ -1,13 +1,23 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
- import { getFilteredLogs } from "../../proxy/store";
2
+ import { getFilteredLogs, getLogSessionId } from "../../proxy/store";
3
3
  import { distillSessionCandidates } from "../../knowledge/distiller";
4
4
  import { saveCandidates } from "../../knowledge/candidateStore";
5
5
 
6
+ function getCandidateScopeId(log: ReturnType<typeof getFilteredLogs>[number]): string {
7
+ return getLogSessionId(log) ?? "default";
8
+ }
9
+
10
+ function getLogsForCandidateScope(scopeId: string): ReturnType<typeof getFilteredLogs> {
11
+ const direct = getFilteredLogs(scopeId);
12
+ if (direct.length > 0) return direct;
13
+ return getFilteredLogs().filter((log) => getCandidateScopeId(log) === scopeId);
14
+ }
15
+
6
16
  export const Route = createFileRoute("/api/knowledge/sessions/$sessionId/candidates")({
7
17
  server: {
8
18
  handlers: {
9
19
  POST: ({ params }: { params: { sessionId: string } }) => {
10
- const logs = getFilteredLogs(params.sessionId);
20
+ const logs = getLogsForCandidateScope(params.sessionId);
11
21
  const candidates = saveCandidates(distillSessionCandidates(params.sessionId, logs));
12
22
  return Response.json({ candidates });
13
23
  },