@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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-BCH_fsLm.js → CompareDrawer-DDmqSAfl.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-Cxpdziwd.js +101 -0
- package/.output/public/assets/ReplayDialog-Bt5DGzlh.js +1 -0
- package/.output/public/assets/RequestAnatomy-BxX3_N9S.js +1 -0
- package/.output/public/assets/ResponseView-Bl_5S9gZ.js +1 -0
- package/.output/public/assets/StreamingChunkSequence-RJMwNf6F.js +1 -0
- package/.output/public/assets/_sessionId-b4isaoDp.js +1 -0
- package/.output/public/assets/index-BZ4x5UI6.js +1 -0
- package/.output/public/assets/{index-CobXD0yH.css → index-C624DUk9.css} +1 -1
- package/.output/public/assets/{json-viewer-BrzjD7qI.js → json-viewer-CRL_gWEZ.js} +1 -1
- package/.output/public/assets/{main-mgxeUdZQ.js → main-CKnTJ4-O.js} +6 -6
- package/.output/server/_libs/lucide-react.mjs +181 -114
- package/.output/server/{_sessionId-C4xsxIWm.mjs → _sessionId-B-x9fRY3.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-DuWEpqQ7.mjs → CompareDrawer-BQVNsAY2.mjs} +6 -6
- package/.output/server/_ssr/{ProxyViewerContainer-Cckz5qKu.mjs → ProxyViewerContainer-CYm2Dw19.mjs} +766 -122
- package/.output/server/_ssr/{ReplayDialog-BDRcr8E5.mjs → ReplayDialog-CaMQBc79.mjs} +240 -14
- package/.output/server/_ssr/{RequestAnatomy-BoO2_Ij0.mjs → RequestAnatomy--P5arRH2.mjs} +236 -66
- package/.output/server/_ssr/{ResponseView-DZiPBxvO.mjs → ResponseView-RtFwNvgD.mjs} +8 -8
- package/.output/server/_ssr/{StreamingChunkSequence-D-be7KEL.mjs → StreamingChunkSequence-B5HPkzab.mjs} +3 -3
- package/.output/server/_ssr/{index-5RImHKfu.mjs → index-CZIKZU43.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-aJhb93ZK.mjs → json-viewer-d4obyRaA.mjs} +3 -3
- package/.output/server/_ssr/{router-Dgkv5nKP.mjs → router-DGPt3MUc.mjs} +145 -71
- package/.output/server/_tanstack-start-manifest_v-BzH4pNaI.mjs +4 -0
- package/.output/server/index.mjs +64 -64
- package/package.json +1 -1
- package/src/components/OnboardingBanner.tsx +11 -19
- package/src/components/ProxyViewer.tsx +1 -1
- package/src/components/providers/ProviderCard.tsx +6 -20
- package/src/components/providers/SettingsDialog.tsx +95 -2
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +639 -38
- package/src/components/proxy-viewer/CompareDrawer.tsx +4 -2
- package/src/components/proxy-viewer/LogEntry.tsx +4 -4
- package/src/components/proxy-viewer/LogEntryHeader.tsx +15 -25
- package/src/components/proxy-viewer/ReplayDialog.tsx +190 -8
- package/src/components/proxy-viewer/ResponseView.tsx +2 -2
- package/src/components/proxy-viewer/ToolTraceEvents.tsx +37 -16
- package/src/components/proxy-viewer/TurnGroup.tsx +14 -2
- package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +196 -45
- package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +92 -67
- package/src/components/proxy-viewer/anatomy/types.ts +15 -13
- package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +2 -2
- package/src/components/proxy-viewer/log-formats/anthropic.ts +1 -1
- package/src/components/proxy-viewer/log-formats/openai.ts +1 -1
- package/src/components/proxy-viewer/log-formats/types.ts +1 -1
- package/src/components/proxy-viewer/replayComparison.ts +131 -0
- package/src/components/proxy-viewer/useKeyboardNavigation.ts +64 -22
- package/src/components/proxy-viewer/viewerState.ts +14 -2
- package/src/components/ui/json-viewer.tsx +1 -1
- package/src/knowledge/candidateStore.ts +32 -1
- package/src/routes/api/knowledge.candidates.$candidateId.ts +50 -0
- package/src/routes/api/knowledge.sessions.$sessionId.candidates.ts +12 -2
- package/.output/public/assets/ProxyViewerContainer-D85_UANk.js +0 -101
- package/.output/public/assets/ReplayDialog-DTeaHHit.js +0 -1
- package/.output/public/assets/RequestAnatomy-DZ8grAih.js +0 -1
- package/.output/public/assets/ResponseView-Cldm6RCi.js +0 -1
- package/.output/public/assets/StreamingChunkSequence-3x4p-yT7.js +0 -1
- package/.output/public/assets/_sessionId-YqWFBu6d.js +0 -1
- package/.output/public/assets/index-BIw2H6jO.js +0 -1
- 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
|
|
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 !==
|
|
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 === "
|
|
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 === "
|
|
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 === "
|
|
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 === "
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
20
|
+
const logs = getLogsForCandidateScope(params.sessionId);
|
|
11
21
|
const candidates = saveCandidates(distillSessionCandidates(params.sessionId, logs));
|
|
12
22
|
return Response.json({ candidates });
|
|
13
23
|
},
|