@kentwynn/kgraph 0.2.36 → 0.2.38
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/cli/commands/pack.js +39 -6
- package/dist/cli/commands/session.js +25 -4
- package/dist/cli/commands/visualize.js +26 -3
- package/dist/context/ranking.js +23 -2
- package/dist/integrations/adapters/claude-code.js +106 -1
- package/dist/integrations/adapters/cline.js +2 -0
- package/dist/integrations/adapters/cursor.js +2 -0
- package/dist/integrations/adapters/gemini.js +5 -2
- package/dist/integrations/adapters/windsurf.js +3 -0
- package/dist/integrations/instruction-blocks.js +1 -1
- package/dist/integrations/integration-registry.d.ts +9 -0
- package/dist/integrations/integration-store.js +42 -0
- package/dist/session/session-store.d.ts +2 -0
- package/dist/session/session-store.js +10 -0
- package/dist/types/session.d.ts +5 -0
- package/dist/visualization/html-template.d.ts +16 -1
- package/dist/visualization/html-template.js +120 -2
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { loadConfig } from '../../config/config.js';
|
|
3
3
|
import { buildContextPack } from '../../context/context-pack.js';
|
|
4
4
|
import { queryContext } from '../../context/context-query.js';
|
|
5
|
+
import { getCurrentCommit, isGitRepo } from '../../scanner/git-utils.js';
|
|
5
6
|
import { assertSessionAgent, recordSessionEvent, } from '../../session/session-store.js';
|
|
6
7
|
import { listInboxNotes } from '../../storage/cognition-store.js';
|
|
7
8
|
import { assertWorkspace } from '../../storage/kgraph-paths.js';
|
|
@@ -28,19 +29,51 @@ export function registerPackCommand(program) {
|
|
|
28
29
|
const agent = options.agent ??
|
|
29
30
|
command.getOptionValue('agent') ??
|
|
30
31
|
findCommandOption(command, 'agent');
|
|
32
|
+
const [config, maps] = await Promise.all([
|
|
33
|
+
loadConfig(workspace),
|
|
34
|
+
readMaps(workspace),
|
|
35
|
+
]);
|
|
36
|
+
const response = await queryContext(workspace, config, maps, task);
|
|
37
|
+
const pack = buildContextPack(response, budget, workspace.rootPath);
|
|
38
|
+
// P4: warn when maps are behind HEAD (user edited files without re-scanning)
|
|
39
|
+
if (await isGitRepo(workspace.rootPath)) {
|
|
40
|
+
const head = await getCurrentCommit(workspace.rootPath);
|
|
41
|
+
if (head &&
|
|
42
|
+
maps.fileMap.scannedAtCommit &&
|
|
43
|
+
maps.fileMap.scannedAtCommit !== head) {
|
|
44
|
+
pack.warnings = [
|
|
45
|
+
`Maps are behind HEAD — run \`kgraph scan\` or \`kgraph "${task}"${agent ? ` --agent ${agent}` : ''}\` for accurate atom status. Knowledge freshness may be reduced.`,
|
|
46
|
+
...pack.warnings,
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// P1: warn when needs-review or stale atoms are served in context
|
|
51
|
+
const degradedAtoms = pack.items.filter((item) => item.kind === 'atom' &&
|
|
52
|
+
(item.data.status === 'needs-review' ||
|
|
53
|
+
item.data.status === 'stale'));
|
|
54
|
+
if (degradedAtoms.length > 0) {
|
|
55
|
+
const needsReview = degradedAtoms.filter((item) => item.data.status === 'needs-review').length;
|
|
56
|
+
const stale = degradedAtoms.filter((item) => item.data.status === 'stale').length;
|
|
57
|
+
const parts = [];
|
|
58
|
+
if (needsReview > 0)
|
|
59
|
+
parts.push(`${needsReview} needs-review`);
|
|
60
|
+
if (stale > 0)
|
|
61
|
+
parts.push(`${stale} stale`);
|
|
62
|
+
pack.warnings = [
|
|
63
|
+
...pack.warnings,
|
|
64
|
+
`${parts.join(', ')} atom(s) in context may reflect outdated knowledge — run \`kgraph stale\` to inspect, or \`kgraph "${task}" --final${agent ? ` --agent ${agent}` : ''}\` to resolve.`,
|
|
65
|
+
];
|
|
66
|
+
}
|
|
31
67
|
if (agent) {
|
|
68
|
+
const omittedTokens = pack.omitted.reduce((sum, item) => sum + item.tokenEstimate, 0);
|
|
32
69
|
await recordSessionEvent(workspace, {
|
|
33
70
|
agent: assertSessionAgent(agent),
|
|
34
71
|
type: 'context',
|
|
35
72
|
captureSource: 'automatic',
|
|
73
|
+
packUsedTokens: pack.usedTokens,
|
|
74
|
+
packOmittedTokens: omittedTokens,
|
|
36
75
|
});
|
|
37
76
|
}
|
|
38
|
-
const [config, maps] = await Promise.all([
|
|
39
|
-
loadConfig(workspace),
|
|
40
|
-
readMaps(workspace),
|
|
41
|
-
]);
|
|
42
|
-
const response = await queryContext(workspace, config, maps, task);
|
|
43
|
-
const pack = buildContextPack(response, budget, workspace.rootPath);
|
|
44
77
|
const pendingInboxFiles = (await listInboxNotes(workspace)).map((file) => path.relative(workspace.rootPath, file).split(path.sep).join('/'));
|
|
45
78
|
if (pendingInboxFiles.length > 0) {
|
|
46
79
|
pack.pendingInbox = {
|
|
@@ -13,7 +13,9 @@ export function registerSessionCommand(program) {
|
|
|
13
13
|
.action((options) => runCommand(async () => {
|
|
14
14
|
const workspace = await assertWorkspace(process.cwd());
|
|
15
15
|
const report = await buildSessionReport(workspace);
|
|
16
|
-
console.log(options.json
|
|
16
|
+
console.log(options.json
|
|
17
|
+
? JSON.stringify(report, null, 2)
|
|
18
|
+
: renderSessionReport(report));
|
|
17
19
|
}));
|
|
18
20
|
session
|
|
19
21
|
.command('start')
|
|
@@ -112,10 +114,25 @@ export function renderSessionReport(report) {
|
|
|
112
114
|
lines.push(`Repeated reads: ${report.repeatedReadCount}`);
|
|
113
115
|
lines.push(`Estimated read tokens: ${report.estimatedReadTokens}`);
|
|
114
116
|
lines.push(`Estimated repeated-read tokens: ${report.estimatedRepeatedReadTokens}`);
|
|
117
|
+
lines.push('');
|
|
118
|
+
lines.push('Pack Usage');
|
|
119
|
+
lines.push(` Pack calls: ${report.packCallCount}`);
|
|
120
|
+
lines.push(` Tokens used: ${report.totalPackUsedTokens}`);
|
|
121
|
+
lines.push(` Tokens filtered: ${report.totalPackOmittedTokens}`);
|
|
122
|
+
if (report.packCallCount > 0 && report.totalPackOmittedTokens > 0) {
|
|
123
|
+
const total = report.totalPackUsedTokens + report.totalPackOmittedTokens;
|
|
124
|
+
const pct = Math.round((report.totalPackOmittedTokens / total) * 100);
|
|
125
|
+
lines.push(` Filter rate: ${pct}% of candidate tokens excluded from context`);
|
|
126
|
+
}
|
|
115
127
|
lines.push('', 'Top Repeated Reads');
|
|
116
128
|
lines.push(...formatList(report.topRepeatedReads.map((item) => `- ${item.path} read ${item.count} times (~${item.estimatedTokens} tokens)`)));
|
|
117
129
|
lines.push('', 'Recent Events');
|
|
118
|
-
lines.push(...formatList(report.recentEvents.map((event) =>
|
|
130
|
+
lines.push(...formatList(report.recentEvents.map((event) => {
|
|
131
|
+
const packInfo = event.packUsedTokens !== undefined
|
|
132
|
+
? ` [used:${event.packUsedTokens} filtered:${event.packOmittedTokens ?? 0}]`
|
|
133
|
+
: '';
|
|
134
|
+
return `- ${event.agent} ${event.type}${event.path ? ` ${event.path}` : ''}${packInfo} [${event.captureSource}]`;
|
|
135
|
+
})));
|
|
119
136
|
lines.push('', 'Recent Ledger');
|
|
120
137
|
lines.push(...formatList(report.ledger.map((entry) => `- ${entry.agent} ${entry.readCount} reads, ${entry.writeCount} writes, ${entry.repeatedReadCount} repeated`)));
|
|
121
138
|
lines.push('', 'Next');
|
|
@@ -144,7 +161,9 @@ function findCommandOption(command, name) {
|
|
|
144
161
|
return undefined;
|
|
145
162
|
}
|
|
146
163
|
function normalizeSource(value) {
|
|
147
|
-
if (value === 'automatic' ||
|
|
164
|
+
if (value === 'automatic' ||
|
|
165
|
+
value === 'agent-reported' ||
|
|
166
|
+
value === 'manual') {
|
|
148
167
|
return value;
|
|
149
168
|
}
|
|
150
169
|
throw new KGraphError('--source must be automatic, agent-reported, or manual.');
|
|
@@ -153,7 +172,9 @@ function formatList(items) {
|
|
|
153
172
|
return items.length > 0 ? items : ['- None'];
|
|
154
173
|
}
|
|
155
174
|
function sessionNextActions(report) {
|
|
156
|
-
if (report.
|
|
175
|
+
if (report.packCallCount === 0 &&
|
|
176
|
+
report.readCount === 0 &&
|
|
177
|
+
report.writeCount === 0) {
|
|
157
178
|
return [
|
|
158
179
|
'- Start tracking with `kgraph session start --agent <name>`.',
|
|
159
180
|
'- Record meaningful reads/writes with `kgraph session read <path> --agent <name>` and `kgraph session write <path> --agent <name>`.',
|
|
@@ -2,10 +2,11 @@ import { exec } from 'node:child_process';
|
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
3
|
import { loadConfig } from '../../config/config.js';
|
|
4
4
|
import { refreshKnowledgeAtomStatuses } from '../../knowledge/atom-store.js';
|
|
5
|
+
import { readSessionState } from '../../session/session-store.js';
|
|
5
6
|
import { assertWorkspace } from '../../storage/kgraph-paths.js';
|
|
6
7
|
import { mapsExist, readMaps } from '../../storage/map-store.js';
|
|
7
8
|
import { buildGraph } from '../../visualization/graph-builder.js';
|
|
8
|
-
import { renderHtml } from '../../visualization/html-template.js';
|
|
9
|
+
import { renderHtml, } from '../../visualization/html-template.js';
|
|
9
10
|
import { KGraphError, runCommand } from '../errors.js';
|
|
10
11
|
export function registerVisualizeCommand(program) {
|
|
11
12
|
program
|
|
@@ -22,14 +23,36 @@ export function registerVisualizeCommand(program) {
|
|
|
22
23
|
if (!(await mapsExist(workspace))) {
|
|
23
24
|
throw new KGraphError('KGraph maps are missing. Run `kgraph scan` first.');
|
|
24
25
|
}
|
|
25
|
-
const maps = await
|
|
26
|
+
const [maps, sessionState] = await Promise.all([
|
|
27
|
+
readMaps(workspace),
|
|
28
|
+
readSessionState(workspace),
|
|
29
|
+
]);
|
|
26
30
|
const { atoms } = await refreshKnowledgeAtomStatuses(workspace, {
|
|
27
31
|
fileMap: maps.fileMap,
|
|
28
32
|
symbolMap: maps.symbolMap,
|
|
29
33
|
});
|
|
30
34
|
await loadConfig(workspace); // ensure workspace is valid
|
|
35
|
+
const contextEvents = sessionState.events.filter((e) => e.type === 'context');
|
|
36
|
+
const sessionVizData = sessionState.events.length > 0
|
|
37
|
+
? {
|
|
38
|
+
activeAgents: Object.values(sessionState.active).map((a) => a.agent),
|
|
39
|
+
packCallCount: contextEvents.length,
|
|
40
|
+
totalPackUsedTokens: contextEvents.reduce((sum, e) => sum + (e.packUsedTokens ?? 0), 0),
|
|
41
|
+
totalPackOmittedTokens: contextEvents.reduce((sum, e) => sum + (e.packOmittedTokens ?? 0), 0),
|
|
42
|
+
readCount: sessionState.events.filter((e) => e.type === 'read')
|
|
43
|
+
.length,
|
|
44
|
+
writeCount: sessionState.events.filter((e) => e.type === 'write').length,
|
|
45
|
+
contextEvents: contextEvents.map((e) => ({
|
|
46
|
+
agent: e.agent,
|
|
47
|
+
packUsedTokens: e.packUsedTokens ?? 0,
|
|
48
|
+
packOmittedTokens: e.packOmittedTokens ?? 0,
|
|
49
|
+
timestamp: e.timestamp,
|
|
50
|
+
captureSource: e.captureSource,
|
|
51
|
+
})),
|
|
52
|
+
}
|
|
53
|
+
: undefined;
|
|
31
54
|
const graphData = buildGraph(maps.fileMap, maps.symbolMap, maps.dependencyMap, maps.relationshipMap, atoms);
|
|
32
|
-
const html = renderHtml(graphData, workspace.rootPath);
|
|
55
|
+
const html = renderHtml(graphData, workspace.rootPath, sessionVizData);
|
|
33
56
|
await serveGraph(html, port, options.open);
|
|
34
57
|
}));
|
|
35
58
|
}
|
package/dist/context/ranking.js
CHANGED
|
@@ -14,7 +14,10 @@ export function rankByFields(query, items, fields) {
|
|
|
14
14
|
for (const field of fields) {
|
|
15
15
|
const value = field.value(item);
|
|
16
16
|
const values = Array.isArray(value) ? value : value ? [value] : [];
|
|
17
|
-
const haystack = values
|
|
17
|
+
const haystack = values
|
|
18
|
+
.flatMap((value) => [value, splitIdentifier(value).join(' ')])
|
|
19
|
+
.join(' ')
|
|
20
|
+
.toLowerCase();
|
|
18
21
|
for (const token of tokens) {
|
|
19
22
|
if (haystack.includes(token)) {
|
|
20
23
|
const baseScore = field.name === 'path' || field.name === 'name'
|
|
@@ -36,7 +39,25 @@ export function rankByFields(query, items, fields) {
|
|
|
36
39
|
.sort((a, b) => b.score - a.score);
|
|
37
40
|
}
|
|
38
41
|
function expandTokens(tokens) {
|
|
39
|
-
return [
|
|
42
|
+
return [
|
|
43
|
+
...new Set(tokens.flatMap((token) => {
|
|
44
|
+
const parts = [token, ...splitIdentifier(token)];
|
|
45
|
+
// Add prefix stems so "authentication" also matches "auth",
|
|
46
|
+
// "configuration" matches "config", etc. Only for longer tokens
|
|
47
|
+
// to avoid over-matching short words.
|
|
48
|
+
// Use a short 4-char stem (catches root prefixes like "auth", "conf", "init")
|
|
49
|
+
// and a half-length stem (catches "config" from "configuration", "authent" from
|
|
50
|
+
// "authentication"). Both are narrower than the full token so they find real matches
|
|
51
|
+
// without over-fetching.
|
|
52
|
+
if (token.length >= 8) {
|
|
53
|
+
parts.push(token.slice(0, 4));
|
|
54
|
+
const half = Math.floor(token.length * 0.5);
|
|
55
|
+
if (half > 4)
|
|
56
|
+
parts.push(token.slice(0, half));
|
|
57
|
+
}
|
|
58
|
+
return parts;
|
|
59
|
+
})),
|
|
60
|
+
];
|
|
40
61
|
}
|
|
41
62
|
function splitIdentifier(value) {
|
|
42
63
|
return value
|
|
@@ -6,9 +6,16 @@ export const claudeCodeAdapter = {
|
|
|
6
6
|
instructions: `## KGraph Workflow
|
|
7
7
|
|
|
8
8
|
${numberedWorkflow('claude-code', {
|
|
9
|
-
sessionQualifier: 'native hooks also report session activity
|
|
9
|
+
sessionQualifier: 'native hooks also report session activity automatically',
|
|
10
10
|
})}
|
|
11
11
|
`,
|
|
12
|
+
configFiles: [
|
|
13
|
+
{
|
|
14
|
+
path: '.claude/settings.json',
|
|
15
|
+
merge: mergeClaudeHooks,
|
|
16
|
+
remove: removeClaudeHooks,
|
|
17
|
+
},
|
|
18
|
+
],
|
|
12
19
|
commandFiles: [
|
|
13
20
|
{
|
|
14
21
|
path: '.claude/commands/kgraph.md',
|
|
@@ -106,6 +113,88 @@ ${numberedWorkflow('claude-code')}
|
|
|
106
113
|
],
|
|
107
114
|
obsoleteCommandFiles: [],
|
|
108
115
|
};
|
|
116
|
+
const KGRAPH_HOOK_MARKER = 'kgraph-session';
|
|
117
|
+
const CLAUDE_HOOK_REGISTRATIONS = [
|
|
118
|
+
{
|
|
119
|
+
event: 'UserPromptSubmit',
|
|
120
|
+
entry: {
|
|
121
|
+
hooks: [
|
|
122
|
+
{
|
|
123
|
+
type: 'command',
|
|
124
|
+
command: 'node .claude/hooks/kgraph-session-start.cjs',
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
event: 'PreToolUse',
|
|
131
|
+
entry: {
|
|
132
|
+
matcher: 'Read',
|
|
133
|
+
hooks: [
|
|
134
|
+
{
|
|
135
|
+
type: 'command',
|
|
136
|
+
command: 'node .claude/hooks/kgraph-session-pre-read.cjs',
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
event: 'PostToolUse',
|
|
143
|
+
entry: {
|
|
144
|
+
matcher: 'Write|Edit|MultiEdit',
|
|
145
|
+
hooks: [
|
|
146
|
+
{
|
|
147
|
+
type: 'command',
|
|
148
|
+
command: 'node .claude/hooks/kgraph-session-post-write.cjs',
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
event: 'Stop',
|
|
155
|
+
entry: {
|
|
156
|
+
hooks: [
|
|
157
|
+
{
|
|
158
|
+
type: 'command',
|
|
159
|
+
command: 'node .claude/hooks/kgraph-session-stop.cjs',
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
function mergeClaudeHooks(existing) {
|
|
166
|
+
const settings = { ...existing };
|
|
167
|
+
const hooks = typeof settings.hooks === 'object' && settings.hooks !== null
|
|
168
|
+
? { ...settings.hooks }
|
|
169
|
+
: {};
|
|
170
|
+
for (const { event, entry } of CLAUDE_HOOK_REGISTRATIONS) {
|
|
171
|
+
const entries = (hooks[event] ?? []);
|
|
172
|
+
const alreadyPresent = entries.some((e) => e.hooks.some((h) => h.command.includes(KGRAPH_HOOK_MARKER)));
|
|
173
|
+
if (!alreadyPresent) {
|
|
174
|
+
hooks[event] = [...entries, entry];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
settings.hooks = hooks;
|
|
178
|
+
return settings;
|
|
179
|
+
}
|
|
180
|
+
function removeClaudeHooks(existing) {
|
|
181
|
+
if (typeof existing.hooks !== 'object' || existing.hooks === null)
|
|
182
|
+
return existing;
|
|
183
|
+
const settings = { ...existing };
|
|
184
|
+
const hooks = { ...settings.hooks };
|
|
185
|
+
for (const event of Object.keys(hooks)) {
|
|
186
|
+
hooks[event] = hooks[event].filter((entry) => !entry.hooks.some((h) => h.command.includes(KGRAPH_HOOK_MARKER)));
|
|
187
|
+
if (hooks[event].length === 0) {
|
|
188
|
+
delete hooks[event];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (Object.keys(hooks).length === 0) {
|
|
192
|
+
const { hooks: _removed, ...rest } = settings;
|
|
193
|
+
return rest;
|
|
194
|
+
}
|
|
195
|
+
settings.hooks = hooks;
|
|
196
|
+
return settings;
|
|
197
|
+
}
|
|
109
198
|
function hookScript(event) {
|
|
110
199
|
const pathArg = event === 'read' || event === 'write'
|
|
111
200
|
? `const payload = readPayload();
|
|
@@ -113,6 +202,22 @@ const filePath = payload?.tool_input?.file_path || payload?.toolInput?.file_path
|
|
|
113
202
|
if (!filePath) process.exit(0);
|
|
114
203
|
args.push(filePath);`
|
|
115
204
|
: '';
|
|
205
|
+
// On session end: run stale check first (refreshes atom statuses against current
|
|
206
|
+
// maps without needing a topic), then end the session with --conclude so any
|
|
207
|
+
// pending knowledge is captured. Both run regardless of each other's exit code.
|
|
208
|
+
if (event === 'end') {
|
|
209
|
+
return `#!/usr/bin/env node
|
|
210
|
+
const { spawnSync } = require('node:child_process');
|
|
211
|
+
|
|
212
|
+
// P3: refresh atom statuses against current maps so needs-review/stale atoms
|
|
213
|
+
// are up to date before the session ends. Output ignored — hook is silent.
|
|
214
|
+
spawnSync('kgraph', ['stale', '--json'], { stdio: 'ignore' });
|
|
215
|
+
|
|
216
|
+
// P2: end the session and trigger conclude so durable knowledge is captured.
|
|
217
|
+
const result = spawnSync('kgraph', ['session', 'end', '--agent', 'claude-code', '--conclude', '--source', 'automatic'], { stdio: 'ignore' });
|
|
218
|
+
process.exit(result.status || 0);
|
|
219
|
+
`;
|
|
220
|
+
}
|
|
116
221
|
return `#!/usr/bin/env node
|
|
117
222
|
const { spawnSync } = require('node:child_process');
|
|
118
223
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { agentSkillFiles } from '../agent-skills.js';
|
|
1
2
|
import { bulletWorkflow } from '../workflow-steps.js';
|
|
2
3
|
export const clineAdapter = {
|
|
3
4
|
name: 'cline',
|
|
@@ -7,4 +8,5 @@ export const clineAdapter = {
|
|
|
7
8
|
|
|
8
9
|
${bulletWorkflow('cline')}
|
|
9
10
|
`,
|
|
11
|
+
commandFiles: agentSkillFiles('cline'),
|
|
10
12
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { agentSkillFiles } from '../agent-skills.js';
|
|
1
2
|
import { bulletWorkflow } from '../workflow-steps.js';
|
|
2
3
|
export const cursorAdapter = {
|
|
3
4
|
name: 'cursor',
|
|
@@ -12,5 +13,6 @@ alwaysApply: true
|
|
|
12
13
|
|
|
13
14
|
${bulletWorkflow('cursor')}
|
|
14
15
|
`,
|
|
16
|
+
commandFiles: agentSkillFiles('cursor'),
|
|
15
17
|
obsoleteCommandFiles: ['.cursor/rules/kgraph-commands.mdc'],
|
|
16
18
|
};
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { agentSkillFiles } from '../agent-skills.js';
|
|
2
|
+
import { numberedWorkflow } from '../workflow-steps.js';
|
|
2
3
|
export const geminiAdapter = {
|
|
3
4
|
name: 'gemini',
|
|
4
5
|
label: 'Gemini CLI',
|
|
5
6
|
targetPath: 'GEMINI.md',
|
|
6
7
|
instructions: `## KGraph Workflow
|
|
7
8
|
|
|
8
|
-
${
|
|
9
|
+
${numberedWorkflow('gemini')}
|
|
9
10
|
`,
|
|
11
|
+
commandFiles: agentSkillFiles('gemini'),
|
|
12
|
+
obsoleteCommandFiles: [],
|
|
10
13
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { agentSkillFiles } from '../agent-skills.js';
|
|
1
2
|
import { bulletWorkflow } from '../workflow-steps.js';
|
|
2
3
|
export const windsurfAdapter = {
|
|
3
4
|
name: 'windsurf',
|
|
@@ -7,4 +8,6 @@ export const windsurfAdapter = {
|
|
|
7
8
|
|
|
8
9
|
${bulletWorkflow('windsurf')}
|
|
9
10
|
`,
|
|
11
|
+
commandFiles: agentSkillFiles('windsurf'),
|
|
12
|
+
obsoleteCommandFiles: [],
|
|
10
13
|
};
|
|
@@ -54,7 +54,7 @@ export function renderContextPolicy(mode, agentName) {
|
|
|
54
54
|
case 'always':
|
|
55
55
|
return `Every chat in this repository must use the correct KGraph command before answering or exploring files. ${routing} For normal repo context, code navigation, debugging, review, or edits, run \`${packCommand}\`. If that pack reports pending inbox notes, run \`${rootCommand}\` or \`kgraph update\` before relying on history or newly captured atoms. Infer the topic from the user's message. This records a lightweight KGraph session context event for the agent. ${useResultBoundary}`;
|
|
56
56
|
case 'manual':
|
|
57
|
-
return
|
|
57
|
+
return `Do not run KGraph automatically. Run \`${packCommand}\` only when the user explicitly asks for KGraph context, invokes KGraph, or needs a machine-readable repo-memory pack. If the user explicitly asks for KGraph history, inbox/update, knowledge, or doctor, run that specific KGraph command instead of pack.`;
|
|
58
58
|
case 'off':
|
|
59
59
|
return 'KGraph is disabled for this integration.';
|
|
60
60
|
case 'smart':
|
|
@@ -5,12 +5,21 @@ export interface IntegrationAdapter {
|
|
|
5
5
|
targetPath: string;
|
|
6
6
|
instructions: string;
|
|
7
7
|
commandFiles?: IntegrationCommandFile[];
|
|
8
|
+
configFiles?: IntegrationConfigFile[];
|
|
8
9
|
obsoleteCommandFiles?: string[];
|
|
9
10
|
}
|
|
10
11
|
export interface IntegrationCommandFile {
|
|
11
12
|
path: string;
|
|
12
13
|
content: string;
|
|
13
14
|
}
|
|
15
|
+
export interface IntegrationConfigFile {
|
|
16
|
+
/** Repo-relative path to a JSON config file (e.g. `.claude/settings.json`). */
|
|
17
|
+
path: string;
|
|
18
|
+
/** Merge KGraph entries into the existing parsed JSON object and return the result. */
|
|
19
|
+
merge: (existing: Record<string, unknown>) => Record<string, unknown>;
|
|
20
|
+
/** Remove KGraph entries from the existing parsed JSON and return the result. */
|
|
21
|
+
remove: (existing: Record<string, unknown>) => Record<string, unknown>;
|
|
22
|
+
}
|
|
14
23
|
export declare function listIntegrationAdapters(): IntegrationAdapter[];
|
|
15
24
|
export declare function getIntegrationAdapter(name: string): IntegrationAdapter;
|
|
16
25
|
export declare function normalizeIntegrationNames(values: string[] | undefined): IntegrationName[];
|
|
@@ -32,6 +32,7 @@ export async function addIntegrations(workspace, names, mode = 'always') {
|
|
|
32
32
|
if (mode === 'off') {
|
|
33
33
|
await removeIntegrationInstructions(workspace.rootPath, adapter.targetPath, adapter.name);
|
|
34
34
|
await removeIntegrationCommandFiles(workspace.rootPath, adapter.commandFiles ?? []);
|
|
35
|
+
await removeIntegrationConfigFiles(workspace.rootPath, adapter.configFiles ?? []);
|
|
35
36
|
}
|
|
36
37
|
else {
|
|
37
38
|
await writeIntegrationInstructions(workspace.rootPath, adapter.targetPath, adapter.name, applyContextPolicy(adapter.instructions, mode, adapter.name));
|
|
@@ -43,6 +44,7 @@ export async function addIntegrations(workspace, names, mode = 'always') {
|
|
|
43
44
|
for (const file of adapter.commandFiles ?? []) {
|
|
44
45
|
writtenCommandFiles.add(file.path);
|
|
45
46
|
}
|
|
47
|
+
await upsertIntegrationConfigFiles(workspace.rootPath, adapter.configFiles ?? []);
|
|
46
48
|
}
|
|
47
49
|
await removeIntegrationCommandFiles(workspace.rootPath, adapter.obsoleteCommandFiles ?? []);
|
|
48
50
|
changed.push(next);
|
|
@@ -74,6 +76,7 @@ export async function removeIntegrations(workspace, names) {
|
|
|
74
76
|
await removeIntegrationInstructions(workspace.rootPath, adapter.targetPath, adapter.name);
|
|
75
77
|
await removeIntegrationCommandFiles(workspace.rootPath, (adapter.commandFiles ?? []).filter((file) => !retainedPaths.has(file.path)));
|
|
76
78
|
await removeIntegrationCommandFiles(workspace.rootPath, adapter.obsoleteCommandFiles ?? []);
|
|
79
|
+
await removeIntegrationConfigFiles(workspace.rootPath, adapter.configFiles ?? []);
|
|
77
80
|
removed.push(adapter.name);
|
|
78
81
|
}
|
|
79
82
|
config.integrations = config.integrations.filter((integration) => !removeNames.has(integration.name));
|
|
@@ -133,3 +136,42 @@ async function pruneEmptyParents(rootPath, startDir) {
|
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
138
|
}
|
|
139
|
+
async function upsertIntegrationConfigFiles(rootPath, configFiles) {
|
|
140
|
+
for (const configFile of configFiles) {
|
|
141
|
+
const fullPath = path.join(rootPath, configFile.path);
|
|
142
|
+
let existing = {};
|
|
143
|
+
if (await pathExists(fullPath)) {
|
|
144
|
+
try {
|
|
145
|
+
existing = JSON.parse(await readFile(fullPath, 'utf8'));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
existing = {};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const next = configFile.merge(existing);
|
|
152
|
+
await mkdir(path.dirname(fullPath), { recursive: true });
|
|
153
|
+
await writeFile(fullPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function removeIntegrationConfigFiles(rootPath, configFiles) {
|
|
157
|
+
for (const configFile of configFiles) {
|
|
158
|
+
const fullPath = path.join(rootPath, configFile.path);
|
|
159
|
+
if (!(await pathExists(fullPath)))
|
|
160
|
+
continue;
|
|
161
|
+
let existing = {};
|
|
162
|
+
try {
|
|
163
|
+
existing = JSON.parse(await readFile(fullPath, 'utf8'));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const next = configFile.remove(existing);
|
|
169
|
+
if (Object.keys(next).length === 0) {
|
|
170
|
+
await rm(fullPath, { force: true });
|
|
171
|
+
await pruneEmptyParents(rootPath, path.dirname(fullPath));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
await writeFile(fullPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -11,5 +11,7 @@ export declare function recordSessionEvent(workspace: KGraphWorkspace, input: {
|
|
|
11
11
|
path?: string;
|
|
12
12
|
captureSource: SessionCaptureSource;
|
|
13
13
|
fileMap?: FileMap;
|
|
14
|
+
packUsedTokens?: number;
|
|
15
|
+
packOmittedTokens?: number;
|
|
14
16
|
}): Promise<SessionEvent>;
|
|
15
17
|
export declare function buildSessionReport(workspace: KGraphWorkspace): Promise<SessionReport>;
|
|
@@ -63,6 +63,12 @@ export async function recordSessionEvent(workspace, input) {
|
|
|
63
63
|
...(normalizedPath ? { path: normalizedPath } : {}),
|
|
64
64
|
...(tokenEstimate !== undefined ? { tokenEstimate } : {}),
|
|
65
65
|
...(repeated !== undefined ? { repeated } : {}),
|
|
66
|
+
...(input.type === 'context' && input.packUsedTokens !== undefined
|
|
67
|
+
? { packUsedTokens: input.packUsedTokens }
|
|
68
|
+
: {}),
|
|
69
|
+
...(input.type === 'context' && input.packOmittedTokens !== undefined
|
|
70
|
+
? { packOmittedTokens: input.packOmittedTokens }
|
|
71
|
+
: {}),
|
|
66
72
|
captureSource: input.captureSource,
|
|
67
73
|
timestamp: now,
|
|
68
74
|
};
|
|
@@ -90,6 +96,7 @@ export async function buildSessionReport(workspace) {
|
|
|
90
96
|
const readEvents = state.events.filter((event) => event.type === 'read');
|
|
91
97
|
const writeEvents = state.events.filter((event) => event.type === 'write');
|
|
92
98
|
const repeatedReads = readEvents.filter((event) => event.repeated);
|
|
99
|
+
const contextEvents = state.events.filter((event) => event.type === 'context');
|
|
93
100
|
return {
|
|
94
101
|
activeAgents: Object.values(state.active),
|
|
95
102
|
readCount: readEvents.length,
|
|
@@ -97,6 +104,9 @@ export async function buildSessionReport(workspace) {
|
|
|
97
104
|
repeatedReadCount: repeatedReads.length,
|
|
98
105
|
estimatedReadTokens: sumTokens(readEvents),
|
|
99
106
|
estimatedRepeatedReadTokens: sumTokens(repeatedReads),
|
|
107
|
+
packCallCount: contextEvents.length,
|
|
108
|
+
totalPackUsedTokens: contextEvents.reduce((sum, e) => sum + (e.packUsedTokens ?? 0), 0),
|
|
109
|
+
totalPackOmittedTokens: contextEvents.reduce((sum, e) => sum + (e.packOmittedTokens ?? 0), 0),
|
|
100
110
|
topRepeatedReads: topRepeatedReads(readEvents),
|
|
101
111
|
recentEvents: state.events.slice(-10),
|
|
102
112
|
ledger: ledger.slice(-10),
|
package/dist/types/session.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface SessionEvent {
|
|
|
9
9
|
path?: string;
|
|
10
10
|
tokenEstimate?: number;
|
|
11
11
|
repeated?: boolean;
|
|
12
|
+
packUsedTokens?: number;
|
|
13
|
+
packOmittedTokens?: number;
|
|
12
14
|
captureSource: SessionCaptureSource;
|
|
13
15
|
timestamp: string;
|
|
14
16
|
}
|
|
@@ -41,6 +43,9 @@ export interface SessionReport {
|
|
|
41
43
|
repeatedReadCount: number;
|
|
42
44
|
estimatedReadTokens: number;
|
|
43
45
|
estimatedRepeatedReadTokens: number;
|
|
46
|
+
packCallCount: number;
|
|
47
|
+
totalPackUsedTokens: number;
|
|
48
|
+
totalPackOmittedTokens: number;
|
|
44
49
|
topRepeatedReads: Array<{
|
|
45
50
|
path: string;
|
|
46
51
|
count: number;
|
|
@@ -1,2 +1,17 @@
|
|
|
1
1
|
import type { GraphData } from './graph-builder.js';
|
|
2
|
-
export
|
|
2
|
+
export interface SessionVizData {
|
|
3
|
+
activeAgents: string[];
|
|
4
|
+
packCallCount: number;
|
|
5
|
+
totalPackUsedTokens: number;
|
|
6
|
+
totalPackOmittedTokens: number;
|
|
7
|
+
readCount: number;
|
|
8
|
+
writeCount: number;
|
|
9
|
+
contextEvents: Array<{
|
|
10
|
+
agent: string;
|
|
11
|
+
packUsedTokens: number;
|
|
12
|
+
packOmittedTokens: number;
|
|
13
|
+
timestamp: string;
|
|
14
|
+
captureSource: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export declare function renderHtml(graphData: GraphData, rootPath: string, sessionData?: SessionVizData): string;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
export function renderHtml(graphData, rootPath) {
|
|
1
|
+
export function renderHtml(graphData, rootPath, sessionData) {
|
|
2
2
|
const repoName = escAttr(rootPath.split(/[\\/]/).pop() ?? 'Repository');
|
|
3
3
|
const { meta } = graphData;
|
|
4
4
|
// Prevent </script> tag injection from embedded JSON
|
|
5
|
-
const safeData = JSON.stringify(graphData).replace(/<\/script>/gi, '
|
|
5
|
+
const safeData = JSON.stringify(graphData).replace(/<\/script>/gi, '<\/script>');
|
|
6
|
+
const safeSessionData = sessionData
|
|
7
|
+
? JSON.stringify(sessionData).replace(/<\/script>/gi, '<\/script>')
|
|
8
|
+
: null;
|
|
6
9
|
return `<!DOCTYPE html>
|
|
7
10
|
<html lang="en">
|
|
8
11
|
<head>
|
|
@@ -46,6 +49,31 @@ select:hover,button:hover{background:#475569}
|
|
|
46
49
|
.li-dia{width:10px;height:10px;transform:rotate(45deg);flex-shrink:0;display:inline-block}
|
|
47
50
|
.li-sep{width:1px;height:14px;background:#334155;flex-shrink:0}
|
|
48
51
|
.li-head{font-size:11px;color:#475569;font-weight:700;letter-spacing:.04em}
|
|
52
|
+
#session-panel{width:310px;background:#1e293b;border-left:1px solid #334155;display:none;flex-direction:column;overflow:hidden;flex-shrink:0}
|
|
53
|
+
#session-panel.open{display:flex}
|
|
54
|
+
#btn-session{color:#7dd3fc;border-color:#3b82f6}
|
|
55
|
+
#btn-session.active,#btn-session:hover{background:#1e3a5f!important;color:#7dd3fc;border-color:#3b82f6}
|
|
56
|
+
#sp-head{display:flex;align-items:center;padding:12px 14px;border-bottom:1px solid #334155;gap:8px;flex-shrink:0}
|
|
57
|
+
#sp-title{font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:#7dd3fc;flex-shrink:0}
|
|
58
|
+
#sp-agents{color:#475569;font-size:11px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
59
|
+
#sp-close2{background:none!important;border:none!important;color:#64748b;font-size:17px;line-height:1;cursor:pointer;padding:0 2px;flex-shrink:0;box-shadow:none}
|
|
60
|
+
#sp-close2:hover{color:#e2e8f0}
|
|
61
|
+
#sp-body{padding:14px;overflow-y:auto;flex:1;display:flex;flex-direction:column;gap:16px}
|
|
62
|
+
.sp-summary{display:grid;grid-template-columns:1fr 1fr;gap:6px}
|
|
63
|
+
.sp-metric{background:#0f172a;border:1px solid #334155;border-radius:5px;padding:8px 10px}
|
|
64
|
+
.sp-metric-val{font-size:18px;font-weight:700;color:#f1f5f9;line-height:1;font-variant-numeric:tabular-nums}
|
|
65
|
+
.sp-metric-val.accent{color:#7dd3fc}
|
|
66
|
+
.sp-metric-lbl{font-size:10px;color:#64748b;margin-top:3px;text-transform:uppercase;letter-spacing:.05em}
|
|
67
|
+
.sp-group-lbl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:#475569;padding-bottom:6px;margin-bottom:2px;border-bottom:1px solid #1e293b}
|
|
68
|
+
.sp-call-row{display:flex;align-items:center;gap:8px;padding:5px 0;border-bottom:1px solid #0f172a}
|
|
69
|
+
.sp-call-row:last-child{border-bottom:none}
|
|
70
|
+
.sp-call-time{font-size:10px;color:#475569;flex-shrink:0;width:44px}
|
|
71
|
+
.sp-call-bar-wrap{flex:1;background:#0f172a;border-radius:2px;height:6px;overflow:hidden}
|
|
72
|
+
.sp-call-bar{background:#3b82f6;height:100%;border-radius:2px;min-width:2px}
|
|
73
|
+
.sp-call-toks{font-size:11px;color:#94a3b8;flex-shrink:0;width:60px;text-align:right}
|
|
74
|
+
.sp-io-row{display:flex;gap:6px}
|
|
75
|
+
.sp-io-box{flex:1;background:#0f172a;border:1px solid #334155;border-radius:5px;padding:8px 10px;text-align:center}
|
|
76
|
+
.sp-io-val{font-size:18px;font-weight:700;color:#f1f5f9}.sp-io-lbl{font-size:10px;color:#64748b;margin-top:2px;text-transform:uppercase;letter-spacing:.05em}
|
|
49
77
|
</style>
|
|
50
78
|
</head>
|
|
51
79
|
<body>
|
|
@@ -63,6 +91,7 @@ select:hover,button:hover{background:#475569}
|
|
|
63
91
|
</select>
|
|
64
92
|
<button id="btn-fit" title="Fit graph to viewport">\u229f Fit</button>
|
|
65
93
|
<button id="btn-png" title="Download as PNG">\u2193 PNG</button>
|
|
94
|
+
${sessionData ? ` <button id="btn-session" title="View session stats">\u26a1 Session</button>` : ''}
|
|
66
95
|
</div>
|
|
67
96
|
</div>
|
|
68
97
|
<div id="main">
|
|
@@ -74,6 +103,16 @@ select:hover,button:hover{background:#475569}
|
|
|
74
103
|
</div>
|
|
75
104
|
<div id="sb-body"></div>
|
|
76
105
|
</div>
|
|
106
|
+
${sessionData
|
|
107
|
+
? ` <div id="session-panel">
|
|
108
|
+
<div id="sp-head">
|
|
109
|
+
<span id="sp-title">\u26a1 Session</span>
|
|
110
|
+
<span id="sp-agents">${sessionData.activeAgents.length ? sessionData.activeAgents.join(' \xb7 ') : 'session recorded'}</span>
|
|
111
|
+
<button id="sp-close2" title="Close">\u00d7</button>
|
|
112
|
+
</div>
|
|
113
|
+
<div id="sp-body"></div>
|
|
114
|
+
</div>`
|
|
115
|
+
: ''}
|
|
77
116
|
</div>
|
|
78
117
|
<div id="legend">
|
|
79
118
|
<span class="li-head">Files</span>
|
|
@@ -294,6 +333,8 @@ select:hover,button:hover{background:#475569}
|
|
|
294
333
|
document.getElementById('sb-type').textContent = d.type === 'atom' ? 'Knowledge Atom' : 'File';
|
|
295
334
|
document.getElementById('sb-body').innerHTML = d.type === 'atom' ? renderAtomPanel(d) : renderFilePanel(d);
|
|
296
335
|
document.getElementById('sidebar').classList.add('open');
|
|
336
|
+
var sp = document.getElementById('session-panel');
|
|
337
|
+
if (sp) { sp.classList.remove('open'); var sb = document.getElementById('btn-session'); if (sb) { sb.classList.remove('active'); sessionPanelOpen = false; } }
|
|
297
338
|
});
|
|
298
339
|
|
|
299
340
|
cy.on('tap', function (evt) {
|
|
@@ -337,6 +378,83 @@ select:hover,button:hover{background:#475569}
|
|
|
337
378
|
a.click();
|
|
338
379
|
document.body.removeChild(a);
|
|
339
380
|
});
|
|
381
|
+
${safeSessionData
|
|
382
|
+
? `
|
|
383
|
+
var SESSION_DATA = ${safeSessionData};
|
|
384
|
+
var sessionRendered = false;
|
|
385
|
+
var sessionPanelOpen = false;
|
|
386
|
+
|
|
387
|
+
function fmt(n) {
|
|
388
|
+
return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function renderSessionPanel() {
|
|
392
|
+
var d = SESSION_DATA;
|
|
393
|
+
var total = d.totalPackUsedTokens + d.totalPackOmittedTokens;
|
|
394
|
+
var filterRate = total > 0 ? Math.round((d.totalPackOmittedTokens / total) * 100) : 0;
|
|
395
|
+
var avgToks = d.packCallCount > 0 ? Math.round(d.totalPackUsedTokens / d.packCallCount) : 0;
|
|
396
|
+
var filterCell = filterRate > 0
|
|
397
|
+
? '<div class="sp-metric"><div class="sp-metric-val accent">' + filterRate + '%</div><div class="sp-metric-lbl">Filtered</div></div>'
|
|
398
|
+
: '<div class="sp-metric"><div class="sp-metric-val" style="font-size:11px;color:#475569;line-height:1.4">None</div><div class="sp-metric-lbl">Filtered</div></div>';
|
|
399
|
+
var summaryHtml =
|
|
400
|
+
'<div class="sp-summary">' +
|
|
401
|
+
'<div class="sp-metric"><div class="sp-metric-val">' + d.packCallCount + '</div><div class="sp-metric-lbl">Pack calls</div></div>' +
|
|
402
|
+
'<div class="sp-metric"><div class="sp-metric-val accent">' + fmt(d.totalPackUsedTokens) + '</div><div class="sp-metric-lbl">Tokens used</div></div>' +
|
|
403
|
+
'<div class="sp-metric"><div class="sp-metric-val">' + fmt(avgToks) + '</div><div class="sp-metric-lbl">Avg / call</div></div>' +
|
|
404
|
+
filterCell +
|
|
405
|
+
'</div>';
|
|
406
|
+
var maxUsed = 0;
|
|
407
|
+
d.contextEvents.forEach(function (e) { if ((e.packUsedTokens || 0) > maxUsed) maxUsed = e.packUsedTokens || 0; });
|
|
408
|
+
var callRows = '';
|
|
409
|
+
if (d.contextEvents.length > 0) {
|
|
410
|
+
callRows = '<div><div class="sp-group-lbl">Pack calls (' + d.contextEvents.length + ')</div>';
|
|
411
|
+
d.contextEvents.forEach(function (e) {
|
|
412
|
+
var used = e.packUsedTokens || 0;
|
|
413
|
+
var filt = e.packOmittedTokens || 0;
|
|
414
|
+
var barPct = maxUsed > 0 ? Math.round(used / maxUsed * 100) : 100;
|
|
415
|
+
var ts = e.timestamp ? new Date(e.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
|
|
416
|
+
var filtLabel = filt > 0 ? ' <span style="color:#ef4444">-' + fmt(filt) + '</span>' : '';
|
|
417
|
+
callRows +=
|
|
418
|
+
'<div class="sp-call-row">' +
|
|
419
|
+
'<span class="sp-call-time">' + esc(ts) + '</span>' +
|
|
420
|
+
'<div class="sp-call-bar-wrap"><div class="sp-call-bar" style="width:' + barPct + '%"></div></div>' +
|
|
421
|
+
'<span class="sp-call-toks">' + fmt(used) + filtLabel + '</span>' +
|
|
422
|
+
'</div>';
|
|
423
|
+
});
|
|
424
|
+
callRows += '</div>';
|
|
425
|
+
}
|
|
426
|
+
var ioHtml =
|
|
427
|
+
'<div><div class="sp-group-lbl">File access</div>' +
|
|
428
|
+
'<div class="sp-io-row">' +
|
|
429
|
+
'<div class="sp-io-box"><div class="sp-io-val">' + d.readCount + '</div><div class="sp-io-lbl">Reads</div></div>' +
|
|
430
|
+
'<div class="sp-io-box"><div class="sp-io-val">' + d.writeCount + '</div><div class="sp-io-lbl">Writes</div></div>' +
|
|
431
|
+
'</div></div>';
|
|
432
|
+
document.getElementById('sp-body').innerHTML = summaryHtml + callRows + ioHtml;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
document.getElementById('btn-session').addEventListener('click', function () {
|
|
436
|
+
var panel = document.getElementById('session-panel');
|
|
437
|
+
sessionPanelOpen = !sessionPanelOpen;
|
|
438
|
+
if (sessionPanelOpen) {
|
|
439
|
+
panel.classList.add('open');
|
|
440
|
+
this.classList.add('active');
|
|
441
|
+
if (!sessionRendered) {
|
|
442
|
+
renderSessionPanel();
|
|
443
|
+
sessionRendered = true;
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
panel.classList.remove('open');
|
|
447
|
+
this.classList.remove('active');
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
document.getElementById('sp-close2').addEventListener('click', function () {
|
|
452
|
+
document.getElementById('session-panel').classList.remove('open');
|
|
453
|
+
document.getElementById('btn-session').classList.remove('active');
|
|
454
|
+
sessionPanelOpen = false;
|
|
455
|
+
});
|
|
456
|
+
`
|
|
457
|
+
: ''}
|
|
340
458
|
})();
|
|
341
459
|
</script>
|
|
342
460
|
</body>
|