@bahulam/code 0.1.6 → 0.1.7
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/LICENSE +1 -1
- package/NOTICE +6 -6
- package/README.md +10 -17
- package/package.json +9 -9
- package/src/agents/loader.mjs +1 -1
- package/src/agents/workflow_loader.mjs +2 -2
- package/src/auth/tarang-auth.mjs +3 -2
- package/src/config/model-catalog-default.json +4 -3
- package/src/config/model-catalog.mjs +2 -0
- package/src/context/prose-chunker.mjs +162 -5
- package/src/core/attachments.mjs +65 -3
- package/src/core/bundled-runtime.mjs +1 -1
- package/src/core/headless.mjs +14 -0
- package/src/core/jsonl-writer.mjs +3 -3
- package/src/core/lint-resolver.mjs +472 -0
- package/src/core/local-agent.mjs +15 -4
- package/src/core/local-store.mjs +4 -0
- package/src/core/output-filter.mjs +21 -25
- package/src/core/pricing.mjs +10 -3
- package/src/core/stream-client.mjs +9 -4
- package/src/core/tool-executor.mjs +105 -29
- package/src/daemon/http-dashboard.mjs +400 -0
- package/src/daemon/session-core.mjs +1 -0
- package/src/local-service/agent-relay.mjs +868 -0
- package/src/local-service/approval-bridge.mjs +222 -0
- package/src/local-service/browser.mjs +24 -0
- package/src/local-service/command.mjs +155 -0
- package/src/local-service/file-access.mjs +393 -0
- package/src/local-service/machine.mjs +86 -0
- package/src/local-service/paths.mjs +29 -0
- package/src/local-service/preview-converters.mjs +260 -0
- package/src/local-service/server.mjs +2644 -0
- package/src/local-service/session-store.mjs +221 -0
- package/src/onboarding/preflight.mjs +1 -1
- package/src/terminal/analytics.mjs +2 -6
- package/src/terminal/ansi.mjs +11 -3
- package/src/terminal/main.mjs +37 -11
- package/src/terminal/model-catalog-display.mjs +111 -0
- package/src/terminal/repl-format.mjs +4 -1
- package/src/terminal/repl-model-form.mjs +81 -14
- package/src/terminal/repl-render.mjs +35 -5
- package/src/terminal/repl-state.mjs +12 -0
- package/src/terminal/repl.mjs +231 -53
- package/src/terminal/tool-display.mjs +32 -0
- package/src/terminal/watch-state.mjs +118 -0
- package/src/tools/analyze-image.mjs +158 -0
- package/src/tools/bash.mjs +11 -9
- package/src/tools/generate-image.mjs +411 -0
- package/src/tools/lint.mjs +24 -43
- package/src/tools/registry.mjs +4 -0
- package/src/ui/icons.mjs +2 -0
- package/src/ui/palette.mjs +1 -1
- package/src/ui/slash-commands.mjs +4 -6
- package/src/ui/tool-card.mjs +7 -3
- package/pulse/app/activity/page.tsx +0 -190
- package/pulse/app/api/activity/route.ts +0 -138
- package/pulse/app/api/benchmark/route.ts +0 -113
- package/pulse/app/api/benchmarks/route.ts +0 -195
- package/pulse/app/api/costs/route.ts +0 -88
- package/pulse/app/api/export/route.ts +0 -77
- package/pulse/app/api/history/route.ts +0 -11
- package/pulse/app/api/import/route.ts +0 -31
- package/pulse/app/api/memory/route.ts +0 -50
- package/pulse/app/api/plans/route.ts +0 -9
- package/pulse/app/api/projects/[slug]/route.ts +0 -96
- package/pulse/app/api/projects/route.ts +0 -121
- package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
- package/pulse/app/api/sessions/[id]/route.ts +0 -31
- package/pulse/app/api/sessions/route.ts +0 -112
- package/pulse/app/api/settings/route.ts +0 -14
- package/pulse/app/api/stats/route.ts +0 -143
- package/pulse/app/api/todos/route.ts +0 -9
- package/pulse/app/api/tools/route.ts +0 -160
- package/pulse/app/benchmarks/page.tsx +0 -224
- package/pulse/app/costs/page.tsx +0 -179
- package/pulse/app/export/page.tsx +0 -465
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +0 -263
- package/pulse/app/help/page.tsx +0 -143
- package/pulse/app/history/page.tsx +0 -157
- package/pulse/app/layout.tsx +0 -46
- package/pulse/app/memory/page.tsx +0 -365
- package/pulse/app/overview-client.tsx +0 -393
- package/pulse/app/page.tsx +0 -14
- package/pulse/app/plans/page.tsx +0 -308
- package/pulse/app/projects/[slug]/page.tsx +0 -390
- package/pulse/app/projects/page.tsx +0 -110
- package/pulse/app/sessions/[id]/page.tsx +0 -243
- package/pulse/app/sessions/page.tsx +0 -39
- package/pulse/app/settings/page.tsx +0 -188
- package/pulse/app/todos/page.tsx +0 -211
- package/pulse/app/tools/page.tsx +0 -249
- package/pulse/cli.js +0 -164
- package/pulse/components/activity/day-of-week-chart.tsx +0 -35
- package/pulse/components/activity/streak-card.tsx +0 -36
- package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
- package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
- package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
- package/pulse/components/costs/model-token-table.tsx +0 -60
- package/pulse/components/global-search.tsx +0 -193
- package/pulse/components/keyboard-nav-provider.tsx +0 -23
- package/pulse/components/layout/bottom-nav.tsx +0 -53
- package/pulse/components/layout/client-layout.tsx +0 -31
- package/pulse/components/layout/sidebar-context.tsx +0 -50
- package/pulse/components/layout/sidebar.tsx +0 -183
- package/pulse/components/layout/top-bar.tsx +0 -121
- package/pulse/components/overview/activity-heatmap.tsx +0 -107
- package/pulse/components/overview/conversation-table.tsx +0 -148
- package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
- package/pulse/components/overview/peak-hours-chart.tsx +0 -87
- package/pulse/components/overview/project-activity-donut.tsx +0 -96
- package/pulse/components/overview/stat-card.tsx +0 -102
- package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
- package/pulse/components/projects/project-card.tsx +0 -175
- package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
- package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
- package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
- package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
- package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
- package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
- package/pulse/components/sessions/session-badges.tsx +0 -49
- package/pulse/components/sessions/session-table.tsx +0 -299
- package/pulse/components/theme-provider.tsx +0 -44
- package/pulse/components/tools/feature-adoption-table.tsx +0 -58
- package/pulse/components/tools/mcp-server-panel.tsx +0 -45
- package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
- package/pulse/components/tools/version-history-table.tsx +0 -32
- package/pulse/components/ui/alert.tsx +0 -66
- package/pulse/components/ui/badge.tsx +0 -48
- package/pulse/components/ui/breadcrumb.tsx +0 -109
- package/pulse/components/ui/button.tsx +0 -64
- package/pulse/components/ui/calendar.tsx +0 -220
- package/pulse/components/ui/card.tsx +0 -92
- package/pulse/components/ui/command.tsx +0 -158
- package/pulse/components/ui/dialog.tsx +0 -158
- package/pulse/components/ui/input.tsx +0 -21
- package/pulse/components/ui/popover.tsx +0 -89
- package/pulse/components/ui/progress.tsx +0 -31
- package/pulse/components/ui/select.tsx +0 -190
- package/pulse/components/ui/separator.tsx +0 -28
- package/pulse/components/ui/sheet.tsx +0 -143
- package/pulse/components/ui/skeleton.tsx +0 -13
- package/pulse/components/ui/table.tsx +0 -116
- package/pulse/components/ui/tabs.tsx +0 -91
- package/pulse/components/ui/tooltip.tsx +0 -57
- package/pulse/components/use-global-keyboard-nav.ts +0 -79
- package/pulse/components.json +0 -23
- package/pulse/eslint.config.mjs +0 -18
- package/pulse/lib/bahulam-paths.ts +0 -23
- package/pulse/lib/claude-reader.ts +0 -592
- package/pulse/lib/decode.ts +0 -129
- package/pulse/lib/pricing.ts +0 -102
- package/pulse/lib/replay-parser.ts +0 -165
- package/pulse/lib/tool-categories.ts +0 -140
- package/pulse/lib/utils.ts +0 -6
- package/pulse/next-env.d.ts +0 -6
- package/pulse/next.config.ts +0 -16
- package/pulse/package.json +0 -45
- package/pulse/postcss.config.mjs +0 -7
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +0 -1
- package/pulse/public/globe.svg +0 -1
- package/pulse/public/next.svg +0 -1
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +0 -1
- package/pulse/public/window.svg +0 -1
- package/pulse/tsconfig.json +0 -34
- package/pulse/types/claude.ts +0 -294
|
@@ -820,7 +820,7 @@ export class TarangStreamClient {
|
|
|
820
820
|
tool,
|
|
821
821
|
args || {},
|
|
822
822
|
true,
|
|
823
|
-
{ risk, reason },
|
|
823
|
+
{ risk, reason, tool_id, call_id: tool_id, request_id: tool_id },
|
|
824
824
|
);
|
|
825
825
|
|
|
826
826
|
// Map ApprovalManager decision to framework scope
|
|
@@ -930,9 +930,10 @@ export class TarangStreamClient {
|
|
|
930
930
|
}
|
|
931
931
|
|
|
932
932
|
/**
|
|
933
|
-
* Submit a live-steering follow-up on the current running task
|
|
934
|
-
* Unlike resume(), this does NOT pause/unpause — the text
|
|
935
|
-
* delivered at the next tool boundary
|
|
933
|
+
* Submit a live-steering user follow-up on the current running task
|
|
934
|
+
* (PRD-081 §5.2). Unlike resume(), this does NOT pause/unpause — the text
|
|
935
|
+
* is queued and delivered at the next tool boundary as a high-priority user
|
|
936
|
+
* intervention, never as a tool call.
|
|
936
937
|
*
|
|
937
938
|
* Returns a status object; callers should NOT swallow errors:
|
|
938
939
|
* { status: 'accepted', interventionId } — queued on backend, SSE ack forthcoming
|
|
@@ -946,6 +947,7 @@ export class TarangStreamClient {
|
|
|
946
947
|
* @param {string} [opts.idempotencyKey] Optional client-generated id.
|
|
947
948
|
* Auto-generated when omitted; pass the same key
|
|
948
949
|
* for retries to stay idempotent.
|
|
950
|
+
* @param {string} [opts.priority] User-message priority metadata.
|
|
949
951
|
*/
|
|
950
952
|
async sendIntervention(instruction, opts = {}) {
|
|
951
953
|
if (!this.currentTaskId) {
|
|
@@ -967,6 +969,9 @@ export class TarangStreamClient {
|
|
|
967
969
|
body: JSON.stringify({
|
|
968
970
|
instruction: text,
|
|
969
971
|
intervention_id: interventionId,
|
|
972
|
+
role: 'user',
|
|
973
|
+
message_type: 'user_intervention',
|
|
974
|
+
priority: opts.priority || 'high',
|
|
970
975
|
}),
|
|
971
976
|
},
|
|
972
977
|
);
|
|
@@ -19,12 +19,14 @@ import { SkillsLoader } from '../skills/loader.mjs';
|
|
|
19
19
|
import { createAgentFile, listLocalAgents, syncAgentsToBackend } from '../agents/scaffold.mjs';
|
|
20
20
|
import { createWorkflowFile, listLocalWorkflows, WORKFLOW_SYNC_ENDPOINT, slugifyWorkflowName } from '../agents/workflow_scaffold.mjs';
|
|
21
21
|
import { TarangAuth } from '../auth/tarang-auth.mjs';
|
|
22
|
+
import { detectImageFile } from './attachments.mjs';
|
|
22
23
|
import { streamResponse } from './streaming.mjs';
|
|
23
24
|
import { sendApprovalDecision, sendCallback } from './callback-client.mjs';
|
|
24
25
|
import { HookRunner } from '../config/hook-runner.mjs';
|
|
25
26
|
import { buildFileDiff } from './file-diff.mjs';
|
|
26
27
|
import { buildWorkScope } from './work-scope.mjs';
|
|
27
28
|
import { loadDiskMemory, ensureBahulamDir, globalMemoryPath, projectMemoryPath } from './memory-disk.mjs';
|
|
29
|
+
import { resolveLintCommand } from './lint-resolver.mjs';
|
|
28
30
|
import * as fs from 'node:fs';
|
|
29
31
|
import * as os from 'node:os';
|
|
30
32
|
import * as path from 'node:path';
|
|
@@ -487,15 +489,6 @@ export function createToolExecutor({
|
|
|
487
489
|
|
|
488
490
|
// ── Auto-lint after file writes ────────────────────────────
|
|
489
491
|
|
|
490
|
-
const LINT_COMMANDS = {
|
|
491
|
-
'.py': (file) => `python3 -m py_compile "${file}" 2>&1`,
|
|
492
|
-
'.js': (file) => `npx eslint --no-eslintrc --rule '{}' "${file}" 2>&1 || true`,
|
|
493
|
-
'.ts': (file) => `npx tsc --noEmit --pretty "${file}" 2>&1 || true`,
|
|
494
|
-
'.tsx': (file) => `npx tsc --noEmit --pretty "${file}" 2>&1 || true`,
|
|
495
|
-
'.go': (file) => `go vet "${file}" 2>&1`,
|
|
496
|
-
'.rs': (file) => `rustfmt --check "${file}" 2>&1`,
|
|
497
|
-
};
|
|
498
|
-
|
|
499
492
|
// tsc --pretty and eslint emit ANSI codes (including background-red
|
|
500
493
|
// highlights) which bleed when our renderer slices the first 80 chars.
|
|
501
494
|
// Strip color codes so the stored lint string is always plain text.
|
|
@@ -503,15 +496,19 @@ export function createToolExecutor({
|
|
|
503
496
|
function stripAnsi(s) { return String(s || '').replace(ANSI_RE, ''); }
|
|
504
497
|
|
|
505
498
|
function autoLint(filePath) {
|
|
506
|
-
const
|
|
507
|
-
const
|
|
508
|
-
|
|
499
|
+
const project = projectRegistry.projectForPath(filePath);
|
|
500
|
+
const lint = resolveLintCommand(filePath, {
|
|
501
|
+
projectRoot: project?.resource?.root || projectRootFor(filePath),
|
|
502
|
+
projectCommands: project?.resource?.commands || {},
|
|
503
|
+
allowProjectScript: false,
|
|
504
|
+
});
|
|
505
|
+
if (!lint?.command) return null;
|
|
509
506
|
|
|
510
507
|
try {
|
|
511
|
-
const output = execSync(
|
|
508
|
+
const output = execSync(lint.command, {
|
|
512
509
|
encoding: 'utf-8',
|
|
513
510
|
timeout: 15_000,
|
|
514
|
-
cwd:
|
|
511
|
+
cwd: lint.cwd,
|
|
515
512
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
516
513
|
env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1', TERM: 'dumb' },
|
|
517
514
|
});
|
|
@@ -695,6 +692,52 @@ export function createToolExecutor({
|
|
|
695
692
|
};
|
|
696
693
|
},
|
|
697
694
|
|
|
695
|
+
analyze_image: async (args, options = {}) => {
|
|
696
|
+
throwIfAborted(options.signal);
|
|
697
|
+
const tool = occRegistry.get('analyze_image');
|
|
698
|
+
if (!tool) {
|
|
699
|
+
return { success: false, output: 'analyze_image tool is not registered in the CLI.', _tool: 'analyze_image' };
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
const result = await tool.call(args || {});
|
|
703
|
+
throwIfAborted(options.signal);
|
|
704
|
+
if (typeof result === 'string') {
|
|
705
|
+
return { success: !/^error\b/i.test(result), output: result, _tool: 'analyze_image' };
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
success: result?.success !== false,
|
|
709
|
+
output: result?.output || result?.summary || JSON.stringify(result),
|
|
710
|
+
...result,
|
|
711
|
+
_tool: 'analyze_image',
|
|
712
|
+
};
|
|
713
|
+
} catch (err) {
|
|
714
|
+
return { success: false, output: String(err?.message || err), _tool: 'analyze_image' };
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
|
|
718
|
+
generate_image: async (args, options = {}) => {
|
|
719
|
+
throwIfAborted(options.signal);
|
|
720
|
+
const tool = occRegistry.get('generate_image');
|
|
721
|
+
if (!tool) {
|
|
722
|
+
return { success: false, output: 'generate_image tool is not registered in the CLI.', _tool: 'generate_image' };
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
const result = await tool.call(args || {});
|
|
726
|
+
throwIfAborted(options.signal);
|
|
727
|
+
if (typeof result === 'string') {
|
|
728
|
+
return { success: !/^error\b/i.test(result), output: result, _tool: 'generate_image' };
|
|
729
|
+
}
|
|
730
|
+
return {
|
|
731
|
+
success: result?.success !== false,
|
|
732
|
+
output: result?.output || JSON.stringify(result),
|
|
733
|
+
...result,
|
|
734
|
+
_tool: 'generate_image',
|
|
735
|
+
};
|
|
736
|
+
} catch (err) {
|
|
737
|
+
return { success: false, output: String(err?.message || err), _tool: 'generate_image' };
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
|
|
698
741
|
// 0b. read_attachment → chunked text extraction from a local document.
|
|
699
742
|
// Backend registers the schema; execution happens client-side because
|
|
700
743
|
// only the CLI has the user's filesystem. Supports the `path` mode
|
|
@@ -734,6 +777,17 @@ export function createToolExecutor({
|
|
|
734
777
|
return { success: false, output: String(err?.message || err), _tool: 'read_attachment' };
|
|
735
778
|
}
|
|
736
779
|
|
|
780
|
+
const imageInfo = detectImageFile(abs);
|
|
781
|
+
if (imageInfo) {
|
|
782
|
+
return {
|
|
783
|
+
success: false,
|
|
784
|
+
output: `This file is an image (${imageInfo.mime_type}). read_attachment only extracts text documents. Use analyze_image with a specific question. If analyze_image already failed, report that vision error directly instead of retrying this image with read_attachment.`,
|
|
785
|
+
_tool: 'read_attachment',
|
|
786
|
+
_path: abs,
|
|
787
|
+
_mime: imageInfo.mime_type,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
737
791
|
const { extractFromPath } = await import('../context/prose-chunker.mjs');
|
|
738
792
|
let mime, chunks;
|
|
739
793
|
try {
|
|
@@ -747,7 +801,7 @@ export function createToolExecutor({
|
|
|
747
801
|
if (!chunks.length) {
|
|
748
802
|
return {
|
|
749
803
|
success: false,
|
|
750
|
-
output: `Unsupported or empty file (mime=${mime}). Supported: pdf, txt, md/mdx, csv, tsv, json, yaml, toml, html, log, rst. For CSV/Excel analysis use read_table; for images use analyze_image.`,
|
|
804
|
+
output: `Unsupported or empty file (mime=${mime}). Supported text: pdf, txt, md/mdx, ipynb, csv, tsv, json, yaml, toml, xml, html, log, rst, sql, sh, .env, .ini, Dockerfile, .gitignore — plus any file whose first 8KB is valid UTF-8 (auto-sniffed). For CSV/Excel analysis use read_table; for images use analyze_image. If analyze_image already failed for this file, report that vision error directly instead of retrying with read_attachment.`,
|
|
751
805
|
_tool: 'read_attachment',
|
|
752
806
|
};
|
|
753
807
|
}
|
|
@@ -966,10 +1020,12 @@ export function createToolExecutor({
|
|
|
966
1020
|
filteredLines: rawOutput.split('\n').length,
|
|
967
1021
|
}
|
|
968
1022
|
: filterOutput(rawOutput, args.command, success);
|
|
1023
|
+
const agentOutput = observationTimeout && timedOut ? filtered.output : rawOutput;
|
|
969
1024
|
|
|
970
1025
|
return {
|
|
971
1026
|
success,
|
|
972
|
-
output:
|
|
1027
|
+
output: agentOutput,
|
|
1028
|
+
output_preview: filtered.output,
|
|
973
1029
|
exit_code: exitCode,
|
|
974
1030
|
_tool: 'shell',
|
|
975
1031
|
_classification: args._classification,
|
|
@@ -1608,24 +1664,44 @@ print('OK: replaced')
|
|
|
1608
1664
|
try {
|
|
1609
1665
|
throwIfAborted(options.signal);
|
|
1610
1666
|
const filePath = await resolvePath(args.file_path || args.path, args);
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1667
|
+
const project = projectRegistry.projectForPath(filePath);
|
|
1668
|
+
const lint = resolveLintCommand(filePath, {
|
|
1669
|
+
projectRoot: project?.resource?.root || projectRootFor(filePath),
|
|
1670
|
+
projectCommands: project?.resource?.commands || {},
|
|
1671
|
+
allowProjectScript: true,
|
|
1672
|
+
});
|
|
1673
|
+
if (!lint?.command) {
|
|
1674
|
+
return {
|
|
1675
|
+
success: true,
|
|
1676
|
+
issues: [],
|
|
1677
|
+
message: 'No project-aware linter for this path or file type',
|
|
1678
|
+
_tool: 'lint_check',
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1616
1681
|
|
|
1617
|
-
const
|
|
1618
|
-
command:
|
|
1682
|
+
const shellResult = await executeToolWithHooks('shell', {
|
|
1683
|
+
command: lint.command,
|
|
1619
1684
|
timeout: 30_000,
|
|
1620
|
-
description: `Lint: ${path.basename(filePath)}`,
|
|
1621
|
-
cwd:
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1685
|
+
description: `Lint: ${path.basename(filePath)} (${lint.source})`,
|
|
1686
|
+
cwd: lint.cwd,
|
|
1687
|
+
}, options);
|
|
1688
|
+
const rawOutput = typeof shellResult?.output === 'string'
|
|
1689
|
+
? shellResult.output
|
|
1690
|
+
: (typeof shellResult === 'string' ? shellResult : String(shellResult));
|
|
1625
1691
|
if (/^Error:\s*Command cancelled by user/i.test(rawOutput)) {
|
|
1626
1692
|
return { success: false, output: 'Cancelled by user', _cancelled: true, _tool: 'lint_check' };
|
|
1627
1693
|
}
|
|
1628
|
-
|
|
1694
|
+
const normalizedOutput = rawOutput === '(no output)' ? '' : rawOutput;
|
|
1695
|
+
return {
|
|
1696
|
+
success: true,
|
|
1697
|
+
output: normalizedOutput || 'No lint issues found.',
|
|
1698
|
+
command: lint.command,
|
|
1699
|
+
cwd: lint.cwd,
|
|
1700
|
+
language: lint.language,
|
|
1701
|
+
lint_source: lint.source,
|
|
1702
|
+
issues: normalizedOutput.split('\n').filter(Boolean),
|
|
1703
|
+
_tool: 'lint_check',
|
|
1704
|
+
};
|
|
1629
1705
|
} catch (err) {
|
|
1630
1706
|
if (isAbortError(err) || options.signal?.aborted) return cancelledToolResult('lint_check');
|
|
1631
1707
|
return { success: false, output: err.message, _tool: 'lint_check' };
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP + WebSocket dashboard — mirrors the CLI session in a browser.
|
|
3
|
+
*
|
|
4
|
+
* Uses ONLY Node built-ins (http, crypto, buffer). No npm dependencies.
|
|
5
|
+
* Serves an inline single-page HTML dashboard with a WebSocket-backed event
|
|
6
|
+
* stream showing agent/sub-agent activity, tool calls, handoffs, spawns.
|
|
7
|
+
*
|
|
8
|
+
* Architecture (matching socket-server.mjs):
|
|
9
|
+
* startHttpDashboard({ sessionId, port }) -> { url, broadcastEvent, close }
|
|
10
|
+
* - Registers itself via registerBroadcaster(fn) from event-tap.mjs
|
|
11
|
+
* - Binds to 127.0.0.1 only
|
|
12
|
+
* - WebSocket at /ws (RFC 6455 handshake)
|
|
13
|
+
* - Dashboard HTML served at /
|
|
14
|
+
* - Auth token in URL path for basic access control
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
import http from 'node:http';
|
|
19
|
+
import crypto from 'node:crypto';
|
|
20
|
+
import { registerBroadcaster } from './event-tap.mjs';
|
|
21
|
+
|
|
22
|
+
const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {object} opts
|
|
26
|
+
* @param {string} opts.sessionId
|
|
27
|
+
* @param {number} [opts.port=0] — 0 = OS-assigned
|
|
28
|
+
* @param {string} [opts.secret] — random token for URL path
|
|
29
|
+
* @returns {Promise<{url:string, port:number, broadcastEvent:(obj:any)=>void, close:()=>void}>}
|
|
30
|
+
*/
|
|
31
|
+
export async function startHttpDashboard({ sessionId, port = 0, secret } = {}) {
|
|
32
|
+
const token = secret || crypto.randomBytes(12).toString('hex');
|
|
33
|
+
const pathPrefix = `/dashboard/${token}`;
|
|
34
|
+
|
|
35
|
+
/** @type {Set<import('net').Socket>} */
|
|
36
|
+
const wsClients = new Set();
|
|
37
|
+
|
|
38
|
+
const server = http.createServer((req, res) => {
|
|
39
|
+
// WebSocket upgrade
|
|
40
|
+
if (req.headers.upgrade?.toLowerCase() === 'websocket') {
|
|
41
|
+
return handleWebSocketUpgrade(req, res, wsClients);
|
|
42
|
+
}
|
|
43
|
+
// Serve the HTML dashboard
|
|
44
|
+
if (req.url.startsWith(pathPrefix) || req.url === '/') {
|
|
45
|
+
res.writeHead(200, {
|
|
46
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
47
|
+
'Cache-Control': 'no-store',
|
|
48
|
+
});
|
|
49
|
+
res.end(DASHBOARD_HTML(token));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Health endpoint
|
|
53
|
+
if (req.url === '/health') {
|
|
54
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
55
|
+
res.end(JSON.stringify({ ok: true, sessionId, clients: wsClients.size }));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
res.writeHead(404);
|
|
59
|
+
res.end('Not found');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Bind to localhost only
|
|
63
|
+
await new Promise((resolve, reject) => {
|
|
64
|
+
server.listen(port, '127.0.0.1', () => {
|
|
65
|
+
const addr = server.address();
|
|
66
|
+
resolve(addr);
|
|
67
|
+
});
|
|
68
|
+
server.once('error', reject);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const addr = server.address();
|
|
72
|
+
const actualPort = addr.port;
|
|
73
|
+
const url = `http://127.0.0.1:${actualPort}/${pathPrefix}`;
|
|
74
|
+
|
|
75
|
+
// Register as event broadcaster so tapSseEvent pushes to us
|
|
76
|
+
const unregister = registerBroadcaster((event) => {
|
|
77
|
+
broadcastEvent(event);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
function broadcastEvent(obj) {
|
|
81
|
+
if (wsClients.size === 0) return;
|
|
82
|
+
const frame = encodeWSFrame(JSON.stringify(obj));
|
|
83
|
+
for (const sock of wsClients) {
|
|
84
|
+
try { sock.write(frame); } catch { /* single-client failure doesn't cascade */ }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function close() {
|
|
89
|
+
unregister();
|
|
90
|
+
for (const sock of wsClients) {
|
|
91
|
+
try { sock.end(); } catch { /* no-op */ }
|
|
92
|
+
}
|
|
93
|
+
wsClients.clear();
|
|
94
|
+
server.close();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { url, port: actualPort, broadcastEvent, close };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── WebSocket helpers (RFC 6455, text frames only) ──
|
|
101
|
+
|
|
102
|
+
function handleWebSocketUpgrade(req, sock, clients) {
|
|
103
|
+
const key = req.headers['sec-websocket-key'];
|
|
104
|
+
if (!key) {
|
|
105
|
+
sock.destroy();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const accept = crypto.createHash('sha1').update(key + WS_GUID).digest('base64');
|
|
109
|
+
sock.write(
|
|
110
|
+
'HTTP/1.1 101 Switching Protocols\r\n' +
|
|
111
|
+
'Upgrade: websocket\r\n' +
|
|
112
|
+
'Connection: Upgrade\r\n' +
|
|
113
|
+
`Sec-WebSocket-Accept: ${accept}\r\n` +
|
|
114
|
+
'\r\n'
|
|
115
|
+
);
|
|
116
|
+
clients.add(sock);
|
|
117
|
+
sock.once('close', () => clients.delete(sock));
|
|
118
|
+
sock.once('error', () => clients.delete(sock));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function encodeWSFrame(payload) {
|
|
122
|
+
const data = Buffer.from(payload, 'utf-8');
|
|
123
|
+
const len = data.length;
|
|
124
|
+
let header;
|
|
125
|
+
if (len < 126) {
|
|
126
|
+
header = Buffer.alloc(2);
|
|
127
|
+
header[0] = 0x81; // FIN + text opcode
|
|
128
|
+
header[1] = len;
|
|
129
|
+
} else if (len < 65536) {
|
|
130
|
+
header = Buffer.alloc(4);
|
|
131
|
+
header[0] = 0x81;
|
|
132
|
+
header[1] = 126;
|
|
133
|
+
header.writeUInt16BE(len, 2);
|
|
134
|
+
} else {
|
|
135
|
+
header = Buffer.alloc(10);
|
|
136
|
+
header[0] = 0x81;
|
|
137
|
+
header[1] = 127;
|
|
138
|
+
header.writeBigUInt64BE(BigInt(len), 2);
|
|
139
|
+
}
|
|
140
|
+
return Buffer.concat([header, data]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Inline Dashboard HTML ──
|
|
144
|
+
|
|
145
|
+
function DASHBOARD_HTML(token) {
|
|
146
|
+
return `<!DOCTYPE html>
|
|
147
|
+
<html lang="en">
|
|
148
|
+
<head>
|
|
149
|
+
<meta charset="UTF-8">
|
|
150
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
151
|
+
<title>Bahulam Agent Dashboard</title>
|
|
152
|
+
<style>
|
|
153
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
154
|
+
:root{--bg:#0d1117;--surface:#161b22;--border:#30363d;--text:#c9d1d9;
|
|
155
|
+
--dim:#8b949e;--green:#3fb950;--cyan:#58a6ff;--yellow:#d29922;
|
|
156
|
+
--red:#f85149;--brand:#58a6ff}
|
|
157
|
+
body{background:var(--bg);color:var(--text);font-family:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;
|
|
158
|
+
font-size:13px;line-height:1.5;padding:16px;overflow-y:scroll}
|
|
159
|
+
h1{font-size:16px;font-weight:600;color:var(--brand);margin-bottom:8px;display:flex;align-items:center;gap:8px}
|
|
160
|
+
h1 small{font-size:11px;color:var(--dim);font-weight:400}
|
|
161
|
+
#status{display:flex;gap:16px;margin-bottom:12px;flex-wrap:wrap}
|
|
162
|
+
.stat{background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:6px 12px;font-size:12px}
|
|
163
|
+
.stat .num{color:var(--brand);font-weight:600}
|
|
164
|
+
.stat .lbl{color:var(--dim)}
|
|
165
|
+
.toolbar{margin-bottom:8px;display:flex;gap:6px;flex-wrap:wrap}
|
|
166
|
+
.toolbar button{background:var(--surface);border:1px solid var(--border);color:var(--text);
|
|
167
|
+
padding:3px 10px;border-radius:4px;cursor:pointer;font-size:11px;font-family:inherit}
|
|
168
|
+
.toolbar button.active{background:var(--brand);color:#fff;border-color:var(--brand)}
|
|
169
|
+
.toolbar button:hover{background:var(--border)}
|
|
170
|
+
.split{display:flex;gap:12px;height:calc(100vh - 160px)}
|
|
171
|
+
.panel{flex:1;background:var(--surface);border:1px solid var(--border);border-radius:6px;
|
|
172
|
+
overflow-y:auto;padding:4px 0;min-width:0}
|
|
173
|
+
.panel h2{font-size:11px;font-weight:600;color:var(--dim);text-transform:uppercase;
|
|
174
|
+
letter-spacing:0.5px;padding:6px 10px;border-bottom:1px solid var(--border);position:sticky;top:0;
|
|
175
|
+
background:var(--surface);z-index:1}
|
|
176
|
+
.event-line{padding:2px 10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:12px;border-bottom:1px solid #1c2128}
|
|
177
|
+
.event-line:hover{background:#1c2128}
|
|
178
|
+
.tag{display:inline-block;padding:0 5px;border-radius:3px;font-size:10px;font-weight:600;margin-right:4px}
|
|
179
|
+
.tag-spawn{background:#1b3628;color:var(--green)}
|
|
180
|
+
.tag-active{background:#1c2d45;color:var(--cyan)}
|
|
181
|
+
.tag-done{background:#1b3628;color:var(--green)}
|
|
182
|
+
.tag-tool{background:#26242b;color:var(--dim)}
|
|
183
|
+
.tag-handoff{background:#2a2318;color:var(--yellow)}
|
|
184
|
+
.tag-error{background:#2d1b1b;color:var(--red)}
|
|
185
|
+
.time{color:var(--dim);font-size:11px;margin-left:6px}
|
|
186
|
+
.detail{color:var(--dim)}
|
|
187
|
+
::-webkit-scrollbar{width:6px}
|
|
188
|
+
::-webkit-scrollbar-track{background:transparent}
|
|
189
|
+
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
|
190
|
+
</style>
|
|
191
|
+
</head>
|
|
192
|
+
<body>
|
|
193
|
+
<h1>⚡ Agent Dashboard <small id="sessionLabel">connecting…</small></h1>
|
|
194
|
+
<div id="status">
|
|
195
|
+
<div class="stat"><span class="num" id="statEvents">0</span> <span class="lbl">events</span></div>
|
|
196
|
+
<div class="stat"><span class="num" id="statSubAgents">0</span> <span class="lbl">sub-agents</span></div>
|
|
197
|
+
<div class="stat"><span class="num" id="statTools">0</span> <span class="lbl">tools</span></div>
|
|
198
|
+
<div class="stat"><span class="num" id="statHandoffs">0</span> <span class="lbl">handoffs</span></div>
|
|
199
|
+
<div class="stat"><span class="lbl" id="connStatus">● disconnected</span></div>
|
|
200
|
+
</div>
|
|
201
|
+
<div class="toolbar">
|
|
202
|
+
<button data-filter="all" class="active">All</button>
|
|
203
|
+
<button data-filter="sub_agent_start,sub_agent_complete">Sub-Agents</button>
|
|
204
|
+
<button data-filter="delegation">Handoffs</button>
|
|
205
|
+
<button data-filter="tool_call,tool_done">Tools</button>
|
|
206
|
+
<button data-filter="error">Errors</button>
|
|
207
|
+
<button id="btnAutoScroll" class="active">Auto-scroll</button>
|
|
208
|
+
<button id="btnClear">Clear</button>
|
|
209
|
+
</div>
|
|
210
|
+
<div class="split">
|
|
211
|
+
<div class="panel" id="eventLog"><h2>Event Log</h2></div>
|
|
212
|
+
<div class="panel" id="subAgentTree"><h2>Sub-Agent Activity</h2></div>
|
|
213
|
+
</div>
|
|
214
|
+
<script>
|
|
215
|
+
(function(){
|
|
216
|
+
const token = '${token}';
|
|
217
|
+
const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
218
|
+
const wsUrl = wsProto + '//' + location.host + '/ws';
|
|
219
|
+
let ws;
|
|
220
|
+
let filters = ['all'];
|
|
221
|
+
let autoScroll = true;
|
|
222
|
+
let eventCount = 0;
|
|
223
|
+
let subAgentCount = 0;
|
|
224
|
+
let toolCount = 0;
|
|
225
|
+
let handoffCount = 0;
|
|
226
|
+
// Sub-agent state: id -> { id, type, query, startTime, tools, status }
|
|
227
|
+
const subAgents = new Map();
|
|
228
|
+
|
|
229
|
+
function connect() {
|
|
230
|
+
ws = new WebSocket(wsUrl);
|
|
231
|
+
ws.onopen = () => { document.getElementById('connStatus').textContent = '● connected'; };
|
|
232
|
+
ws.onclose = () => {
|
|
233
|
+
document.getElementById('connStatus').textContent = '● disconnected';
|
|
234
|
+
setTimeout(connect, 3000);
|
|
235
|
+
};
|
|
236
|
+
ws.onmessage = (msg) => {
|
|
237
|
+
try {
|
|
238
|
+
const ev = JSON.parse(msg.data);
|
|
239
|
+
handleEvent(ev);
|
|
240
|
+
} catch(e) { /* ignore parse errors */ }
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function handleEvent(ev) {
|
|
245
|
+
eventCount++;
|
|
246
|
+
updateStats();
|
|
247
|
+
const line = renderEventLine(ev);
|
|
248
|
+
if (line) appendEvent(line);
|
|
249
|
+
updateSubAgentTree(ev);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function renderEventLine(ev) {
|
|
253
|
+
const type = ev.type || 'unknown';
|
|
254
|
+
const data = ev.data || {};
|
|
255
|
+
const now = Date.now();
|
|
256
|
+
let tag, label, detail = '';
|
|
257
|
+
|
|
258
|
+
switch(type) {
|
|
259
|
+
case 'sub_agent_start':
|
|
260
|
+
tag = '<span class="tag tag-spawn">spawn</span>';
|
|
261
|
+
label = (data.type || 'agent') + (data.query ? ' "' + data.query.slice(0, 60) + '"' : '');
|
|
262
|
+
subAgentCount++; toolCount++; break;
|
|
263
|
+
case 'sub_agent_complete':
|
|
264
|
+
tag = '<span class="tag tag-done">done</span>';
|
|
265
|
+
label = (data.type || 'agent');
|
|
266
|
+
detail = data.tool_calls != null ? ' · ' + data.tool_calls + ' tools' : '';
|
|
267
|
+
if (data.duration_s) detail += ' · ' + data.duration_s.toFixed(1) + 's';
|
|
268
|
+
break;
|
|
269
|
+
case 'delegation':
|
|
270
|
+
tag = '<span class="tag tag-handoff">handoff</span>';
|
|
271
|
+
label = (data.from || '?') + ' → ' + (data.to || '?');
|
|
272
|
+
if (data.instruction) detail = ' "' + data.instruction.slice(0, 50) + '"';
|
|
273
|
+
handoffCount++; break;
|
|
274
|
+
case 'tool_call':
|
|
275
|
+
case 'tool_request':
|
|
276
|
+
tag = '<span class="tag tag-tool">▶ tool</span>';
|
|
277
|
+
label = (data.tool || '?') + (data.args && data.args.file_path ? ' ' + data.args.file_path : '');
|
|
278
|
+
toolCount++; break;
|
|
279
|
+
case 'tool_done':
|
|
280
|
+
case 'tool_result':
|
|
281
|
+
tag = '<span class="tag tag-tool">✓ tool</span>';
|
|
282
|
+
label = (data.tool || '?');
|
|
283
|
+
if (data.duration_s) detail = ' · ' + data.duration_s.toFixed(1) + 's';
|
|
284
|
+
break;
|
|
285
|
+
case 'error':
|
|
286
|
+
tag = '<span class="tag tag-error">error</span>';
|
|
287
|
+
label = (data.message || '').slice(0, 80);
|
|
288
|
+
break;
|
|
289
|
+
case 'complete':
|
|
290
|
+
tag = '<span class="tag tag-done">complete</span>';
|
|
291
|
+
label = 'Agent finished';
|
|
292
|
+
if (data.tool_calls) detail = ' · ' + data.tool_calls + ' tools';
|
|
293
|
+
break;
|
|
294
|
+
case 'thinking':
|
|
295
|
+
return null; // too noisy
|
|
296
|
+
case 'content':
|
|
297
|
+
case 'content_partial':
|
|
298
|
+
return null;
|
|
299
|
+
default:
|
|
300
|
+
tag = '<span class="tag tag-active">' + type.slice(0, 8) + '</span>';
|
|
301
|
+
label = type;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
eventCount++;
|
|
305
|
+
updateStats();
|
|
306
|
+
return '<div class="event-line">' + tag + esc(label) + '<span class="detail">' + esc(detail) + '</span></div>';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function appendEvent(html) {
|
|
310
|
+
if (!html) return;
|
|
311
|
+
const log = document.getElementById('eventLog');
|
|
312
|
+
log.insertAdjacentHTML('beforeend', html);
|
|
313
|
+
if (autoScroll) log.scrollTop = log.scrollHeight;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function updateSubAgentTree(ev) {
|
|
317
|
+
const type = ev.type;
|
|
318
|
+
const data = ev.data || {};
|
|
319
|
+
const container = document.getElementById('subAgentTree');
|
|
320
|
+
let changed = false;
|
|
321
|
+
|
|
322
|
+
if (type === 'sub_agent_start') {
|
|
323
|
+
const id = data.id || 'sa-' + Date.now();
|
|
324
|
+
subAgents.set(id, { id, type: data.type || 'agent', query: data.query || '', startTime: Date.now(), tools: 0, status: 'active' });
|
|
325
|
+
changed = true;
|
|
326
|
+
} else if (type === 'sub_agent_complete') {
|
|
327
|
+
const id = data.id;
|
|
328
|
+
if (id && subAgents.has(id)) {
|
|
329
|
+
const sa = subAgents.get(id);
|
|
330
|
+
sa.status = 'done';
|
|
331
|
+
sa.tools = data.tool_calls || sa.tools;
|
|
332
|
+
changed = true;
|
|
333
|
+
}
|
|
334
|
+
} else if (type === 'sub_agent_tool') {
|
|
335
|
+
const agentType = data.type || '';
|
|
336
|
+
// Find the most recent active sub-agent of this type
|
|
337
|
+
let found = null;
|
|
338
|
+
for (const [id, sa] of subAgents) {
|
|
339
|
+
if (sa.type === agentType && sa.status === 'active') { found = sa; }
|
|
340
|
+
}
|
|
341
|
+
if (found) { found.tools++; changed = true; }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (changed) {
|
|
345
|
+
let html = '<h2>Sub-Agent Activity</h2>';
|
|
346
|
+
if (subAgents.size === 0) { html += '<div class="event-line" style="color:var(--dim)">(none yet)</div>'; }
|
|
347
|
+
else {
|
|
348
|
+
for (const [id, sa] of subAgents) {
|
|
349
|
+
const elapsed = ((Date.now() - sa.startTime) / 1000).toFixed(1);
|
|
350
|
+
const statusTag = sa.status === 'active'
|
|
351
|
+
? '<span class="tag tag-active">active</span>'
|
|
352
|
+
: '<span class="tag tag-done">done</span>';
|
|
353
|
+
html += '<div class="event-line">' + statusTag + ' <b>' + esc(sa.type) + '</b>';
|
|
354
|
+
if (sa.query) html += ' ' + esc(sa.query.slice(0, 40));
|
|
355
|
+
html += ' · <span class="time">' + sa.tools + ' tools · ' + elapsed + 's</span></div>';
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
container.innerHTML = html;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function updateStats() {
|
|
363
|
+
document.getElementById('statEvents').textContent = eventCount;
|
|
364
|
+
document.getElementById('statSubAgents').textContent = subAgents.size;
|
|
365
|
+
document.getElementById('statTools').textContent = toolCount;
|
|
366
|
+
document.getElementById('statHandoffs').textContent = handoffCount;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function esc(s) { return String(s || '').replace(/[&<>"]/g, function(m) {
|
|
370
|
+
return {'&':'&','<':'<','>':'>','"':'"'}[m]; });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Toolbar filter buttons
|
|
374
|
+
document.querySelectorAll('[data-filter]').forEach(btn => {
|
|
375
|
+
btn.addEventListener('click', () => {
|
|
376
|
+
document.querySelectorAll('[data-filter]').forEach(b => b.classList.remove('active'));
|
|
377
|
+
btn.classList.add('active');
|
|
378
|
+
filters = btn.dataset.filter === 'all' ? ['all'] : btn.dataset.filter.split(',');
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
document.getElementById('btnAutoScroll').addEventListener('click', function() {
|
|
383
|
+
autoScroll = !autoScroll;
|
|
384
|
+
this.classList.toggle('active');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
document.getElementById('btnClear').addEventListener('click', () => {
|
|
388
|
+
document.getElementById('eventLog').innerHTML = '<h2>Event Log</h2>';
|
|
389
|
+
document.getElementById('subAgentTree').innerHTML = '<h2>Sub-Agent Activity</h2>';
|
|
390
|
+
subAgents.clear();
|
|
391
|
+
eventCount = 0; subAgentCount = 0; toolCount = 0; handoffCount = 0;
|
|
392
|
+
updateStats();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
connect();
|
|
396
|
+
})();
|
|
397
|
+
</script>
|
|
398
|
+
</body>
|
|
399
|
+
</html>`;
|
|
400
|
+
}
|