feat(insight): add insight command and server for personalized programming insights

This commit is contained in:
DragonnZhang 2026-01-16 13:12:22 +08:00
parent b80fe574b8
commit 9ff4be1ae4
4 changed files with 1000 additions and 0 deletions

View file

@ -40,6 +40,7 @@ import { themeCommand } from '../ui/commands/themeCommand.js';
import { toolsCommand } from '../ui/commands/toolsCommand.js';
import { vimCommand } from '../ui/commands/vimCommand.js';
import { setupGithubCommand } from '../ui/commands/setupGithubCommand.js';
import { insightCommand } from '../ui/commands/insightCommand.js';
/**
* Loads the core, hard-coded slash commands that are an integral part
@ -90,6 +91,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
vimCommand,
setupGithubCommand,
terminalSetupCommand,
insightCommand,
];
return allDefinitions.filter((cmd): cmd is SlashCommand => cmd !== null);

View file

@ -0,0 +1,404 @@
/**
* @license
* Copyright 2025 Qwen Code
* SPDX-License-Identifier: Apache-2.0
*/
import express from 'express';
import fs from 'fs/promises';
import path, { dirname } from 'path';
import { fileURLToPath } from 'url';
import type { ChatRecord } from '@qwen-code/qwen-code-core';
import { read } from '@qwen-code/qwen-code-core/src/utils/jsonl-utils.js';
interface StreakData {
currentStreak: number;
longestStreak: number;
dates: string[];
}
// For heat map data
interface HeatMapData {
[date: string]: number;
}
// For token usage data
interface TokenUsageData {
[date: string]: {
input: number;
output: number;
total: number;
};
}
// For achievement data
interface AchievementData {
id: string;
name: string;
description: string;
}
// For the final insight data
interface InsightData {
heatmap: HeatMapData;
tokenUsage: TokenUsageData;
currentStreak: number;
longestStreak: number;
longestWorkDate: string | null;
longestWorkDuration: number; // in minutes
activeHours: { [hour: number]: number };
latestActiveTime: string | null;
achievements: AchievementData[];
}
function debugLog(message: string) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`;
console.log(logMessage);
}
debugLog('Insight server starting...');
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const app = express();
const PORT = process.env['PORT'];
const BASE_DIR = process.env['BASE_DIR'];
if (!BASE_DIR) {
debugLog('BASE_DIR environment variable is required');
process.exit(1);
}
app.get('/', (_req, res) => {
res.sendFile(path.join(__dirname, 'views', 'index.html'));
});
// API endpoint to get insight data
app.get('/api/insights', async (_req, res) => {
try {
debugLog('Received request for insights data');
const insights = await generateInsights(BASE_DIR);
debugLog(
`Returning insights data, heatmap size: ${Object.keys(insights.heatmap).length}`,
);
res.json(insights);
} catch (error) {
debugLog(`Error generating insights: ${error}`);
res.status(500).json({ error: 'Failed to generate insights' });
}
});
// Process chat files from all projects in the base directory and generate insights
async function generateInsights(baseDir: string): Promise<InsightData> {
// Initialize data structures
const heatmap: HeatMapData = {};
const tokenUsage: TokenUsageData = {};
const activeHours: { [hour: number]: number } = {};
const sessionStartTimes: { [sessionId: string]: Date } = {};
const sessionEndTimes: { [sessionId: string]: Date } = {};
try {
// Get all project directories in the base directory
const projectDirs = await fs.readdir(baseDir);
// Process each project directory
for (const projectDir of projectDirs) {
const projectPath = path.join(baseDir, projectDir);
const stats = await fs.stat(projectPath);
// Only process if it's a directory
if (stats.isDirectory()) {
const chatsDir = path.join(projectPath, 'chats');
let chatFiles: string[] = [];
try {
// Get all chat files in the chats directory
const files = await fs.readdir(chatsDir);
chatFiles = files.filter((file) => file.endsWith('.jsonl'));
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
debugLog(
`Error reading chats directory for project ${projectDir}: ${error}`,
);
}
// Continue to next project if chats directory doesn't exist
continue;
}
// Process each chat file in this project
for (const file of chatFiles) {
const filePath = path.join(chatsDir, file);
const records = await read<ChatRecord>(filePath);
debugLog(
`Processing file: ${filePath}, records count: ${records.length}`,
);
// Process each record
for (const record of records) {
const timestamp = new Date(record.timestamp);
const dateKey = formatDate(timestamp);
const hour = timestamp.getHours();
// Update heatmap (count of interactions per day)
heatmap[dateKey] = (heatmap[dateKey] || 0) + 1;
// Update active hours
activeHours[hour] = (activeHours[hour] || 0) + 1;
// Update token usage
if (record.usageMetadata) {
const usage = tokenUsage[dateKey] || {
input: 0,
output: 0,
total: 0,
};
usage.input += record.usageMetadata.promptTokenCount || 0;
usage.output += record.usageMetadata.candidatesTokenCount || 0;
usage.total += record.usageMetadata.totalTokenCount || 0;
tokenUsage[dateKey] = usage;
}
// Track session times
if (!sessionStartTimes[record.sessionId]) {
sessionStartTimes[record.sessionId] = timestamp;
}
sessionEndTimes[record.sessionId] = timestamp;
}
}
}
}
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
// Base directory doesn't exist, return empty insights
debugLog(`Base directory does not exist: ${baseDir}`);
} else {
debugLog(`Error reading base directory: ${error}`);
}
}
// Calculate streak data
const streakData = calculateStreaks(Object.keys(heatmap));
// Calculate longest work session
let longestWorkDuration = 0;
let longestWorkDate: string | null = null;
for (const sessionId in sessionStartTimes) {
const start = sessionStartTimes[sessionId];
const end = sessionEndTimes[sessionId];
const durationMinutes = Math.round(
(end.getTime() - start.getTime()) / (1000 * 60),
);
if (durationMinutes > longestWorkDuration) {
longestWorkDuration = durationMinutes;
longestWorkDate = formatDate(start);
}
}
// Calculate latest active time
let latestActiveTime: string | null = null;
let latestTimestamp = new Date(0);
for (const dateStr in heatmap) {
const date = new Date(dateStr);
if (date > latestTimestamp) {
latestTimestamp = date;
latestActiveTime = date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
}
}
// Calculate achievements
const achievements = calculateAchievements(activeHours, heatmap, tokenUsage);
return {
heatmap,
tokenUsage,
currentStreak: streakData.currentStreak,
longestStreak: streakData.longestStreak,
longestWorkDate,
longestWorkDuration,
activeHours,
latestActiveTime,
achievements,
};
}
// Helper function to format date as YYYY-MM-DD
function formatDate(date: Date): string {
return date.toISOString().split('T')[0];
}
// Calculate streaks from activity dates
function calculateStreaks(dates: string[]): StreakData {
if (dates.length === 0) {
return { currentStreak: 0, longestStreak: 0, dates: [] };
}
// Convert string dates to Date objects and sort them
const dateObjects = dates.map((dateStr) => new Date(dateStr));
dateObjects.sort((a, b) => a.getTime() - b.getTime());
let currentStreak = 1;
let maxStreak = 1;
let currentDate = new Date(dateObjects[0]);
currentDate.setHours(0, 0, 0, 0); // Normalize to start of day
for (let i = 1; i < dateObjects.length; i++) {
const nextDate = new Date(dateObjects[i]);
nextDate.setHours(0, 0, 0, 0); // Normalize to start of day
// Calculate difference in days
const diffDays = Math.floor(
(nextDate.getTime() - currentDate.getTime()) / (1000 * 60 * 60 * 24),
);
if (diffDays === 1) {
// Consecutive day
currentStreak++;
maxStreak = Math.max(maxStreak, currentStreak);
} else if (diffDays > 1) {
// Gap in streak
currentStreak = 1;
}
// If diffDays === 0, same day, so streak continues
currentDate = nextDate;
}
// Check if the streak is still ongoing (if last activity was yesterday or today)
const today = new Date();
today.setHours(0, 0, 0, 0);
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
if (
currentDate.getTime() === today.getTime() ||
currentDate.getTime() === yesterday.getTime()
) {
// The streak might still be active, so we don't reset it
}
return {
currentStreak,
longestStreak: maxStreak,
dates,
};
}
// Calculate achievements based on user behavior
function calculateAchievements(
activeHours: { [hour: number]: number },
heatmap: HeatMapData,
_tokenUsage: TokenUsageData,
): AchievementData[] {
const achievements: AchievementData[] = [];
// Total activities
const totalActivities = Object.values(heatmap).reduce(
(sum, count) => sum + count,
0,
);
// Total tokens used - commented out since it's not currently used
// const totalTokens = Object.values(tokenUsage).reduce((sum, usage) => sum + usage.total, 0);
// Total sessions
const totalSessions = Object.keys(heatmap).length;
// Calculate percentage of activity per hour
const totalHourlyActivity = Object.values(activeHours).reduce(
(sum, count) => sum + count,
0,
);
if (totalHourlyActivity > 0) {
// Midnight debugger: 20% of sessions happen between 12AM-5AM
const midnightActivity =
(activeHours[0] || 0) +
(activeHours[1] || 0) +
(activeHours[2] || 0) +
(activeHours[3] || 0) +
(activeHours[4] || 0) +
(activeHours[5] || 0);
if (midnightActivity / totalHourlyActivity >= 0.2) {
achievements.push({
id: 'midnight-debugger',
name: 'Midnight Debugger',
description: '20% of your sessions happen between 12AM-5AM',
});
}
// Morning coder: 20% of sessions happen between 6AM-9AM
const morningActivity =
(activeHours[6] || 0) +
(activeHours[7] || 0) +
(activeHours[8] || 0) +
(activeHours[9] || 0);
if (morningActivity / totalHourlyActivity >= 0.2) {
achievements.push({
id: 'morning-coder',
name: 'Morning Coder',
description: '20% of your sessions happen between 6AM-9AM',
});
}
}
// Patient king: average conversation length >= 10 exchanges
if (totalSessions > 0) {
const avgExchanges = totalActivities / totalSessions;
if (avgExchanges >= 10) {
achievements.push({
id: 'patient-king',
name: 'Patient King',
description: 'Your average conversation length is 10+ exchanges',
});
}
}
// Quick finisher: 70% of sessions have <= 2 exchanges
let quickSessions = 0;
// Since we don't have per-session exchange counts easily available,
// we'll estimate based on the distribution of activities
if (totalSessions > 0) {
// This is a simplified calculation - in a real implementation,
// we'd need to count exchanges per session
const avgPerSession = totalActivities / totalSessions;
if (avgPerSession <= 2) {
// Estimate based on low average
quickSessions = Math.floor(totalSessions * 0.7);
}
if (quickSessions / totalSessions >= 0.7) {
achievements.push({
id: 'quick-finisher',
name: 'Quick Finisher',
description: '70% of your sessions end in 2 exchanges or fewer',
});
}
}
// Explorer: for users with insufficient data or default
if (achievements.length === 0) {
achievements.push({
id: 'explorer',
name: 'Explorer',
description: 'Getting started with Qwen Code',
});
}
return achievements;
}
// Start the server
app.listen(PORT, () => {
debugLog(`Server running at http://localhost:${PORT}/`);
debugLog(`Analyzing projects in: ${BASE_DIR}`);
debugLog('Server is running. Press Ctrl+C to stop.');
});

View file

@ -0,0 +1,404 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Qwen Code Insights</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f7fa;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 30px;
}
h1 {
color: #2c3e50;
}
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.card {
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 20px;
text-align: center;
}
.card h3 {
margin-top: 0;
color: #3498db;
}
.streaks {
display: flex;
justify-content: space-around;
margin: 20px 0;
}
.streak-box {
text-align: center;
}
.streak-value {
font-size: 2em;
font-weight: bold;
color: #2ecc71;
}
.heatmap-container {
height: 300px;
display: flex;
align-items: flex-end;
justify-content: space-around;
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.day-bar {
width: 12px;
background: #3498db;
margin: 0 2px;
border-radius: 3px 3px 0 0;
position: relative;
}
.day-label {
position: absolute;
bottom: -20px;
left: 50%;
transform: translateX(-50%);
font-size: 10px;
white-space: nowrap;
}
.achievement {
background: #f8f9fa;
border-left: 4px solid #3498db;
padding: 10px;
margin: 10px 0;
border-radius: 0 4px 4px 0;
}
.token-chart-container {
height: 300px;
}
.export-btn {
display: block;
margin: 20px auto;
padding: 10px 20px;
background: #3498db;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
.export-btn:hover {
background: #2980b9;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Qwen Code Insights</h1>
<p>Your personalized coding journey and patterns</p>
</header>
<div class="dashboard">
<div class="card">
<h3>Current Streak</h3>
<div class="streaks">
<div class="streak-box">
<div class="streak-value" id="current-streak">0</div>
<div>Days</div>
</div>
<div class="streak-box">
<div class="streak-value" id="longest-streak">0</div>
<div>Longest</div>
</div>
</div>
</div>
<div class="card">
<h3>Active Hours</h3>
<canvas id="hourChart" width="400" height="200"></canvas>
</div>
<div class="card">
<h3>Work Session</h3>
<div>Longest: <span id="longest-work-duration">0</span> min</div>
<div>Date: <span id="longest-work-date">-</span></div>
<div>Last Active: <span id="latest-active-time">-</span></div>
</div>
</div>
<div class="card">
<h3>Activity Heatmap</h3>
<div class="heatmap-container" id="heatmap-container">
<!-- Heatmap bars will be populated by JavaScript -->
</div>
</div>
<div class="card token-chart-container">
<h3>Token Usage</h3>
<canvas id="tokenChart"></canvas>
</div>
<div class="card">
<h3>Achievements</h3>
<div id="achievements-container">
<!-- Achievements will be populated by JavaScript -->
</div>
</div>
<button class="export-btn" id="export-btn">Export as Image</button>
</div>
<script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
<script>
// Fetch insights data from API
async function loadInsights() {
try {
const response = await fetch('/api/insights');
const data = await response.json();
console.log('🚀 ~ loadInsights ~ data:', data);
updateDashboard(data);
} catch (error) {
console.error('Error loading insights:', error);
document.body.innerHTML =
'<div class="container"><h2>Error loading insights</h2><p>Please try again later.</p></div>';
}
}
function updateDashboard(data) {
// Update streaks
document.getElementById('current-streak').textContent =
data.currentStreak;
document.getElementById('longest-streak').textContent =
data.longestStreak;
// Update work session data
document.getElementById('longest-work-duration').textContent =
data.longestWorkDuration;
document.getElementById('longest-work-date').textContent =
data.longestWorkDate || '-';
document.getElementById('latest-active-time').textContent =
data.latestActiveTime || '-';
// Create heatmap
createHeatmap(data.heatmap);
// Create token usage chart
createTokenChart(data.tokenUsage);
// Create hour activity chart
createHourChart(data.activeHours);
// Display achievements
displayAchievements(data.achievements);
}
function createHeatmap(heatmapData) {
const container = document.getElementById('heatmap-container');
container.innerHTML = ''; // Clear existing content
// Get the last 30 days of data
const dates = Object.keys(heatmapData).sort().slice(-30);
// Calculate max value for scaling
const maxValue = Math.max(...Object.values(heatmapData));
dates.forEach((date) => {
const value = heatmapData[date] || 0;
const height = maxValue > 0 ? (value / maxValue) * 200 : 0;
const barContainer = document.createElement('div');
barContainer.style.position = 'relative';
barContainer.style.height = '250px';
barContainer.style.display = 'flex';
barContainer.style.alignItems = 'flex-end';
const bar = document.createElement('div');
bar.className = 'day-bar';
bar.style.height = `${height}px`;
// Color intensity based on activity
const intensity = value / maxValue;
const r = 52;
const g = Math.floor(152 - 152 * intensity);
const b = Math.floor(100 + 100 * intensity);
bar.style.backgroundColor = `rgb(${r}, ${g}, ${b})`;
const label = document.createElement('div');
label.className = 'day-label';
label.textContent = new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
});
barContainer.appendChild(bar);
barContainer.appendChild(label);
container.appendChild(barContainer);
});
}
function createTokenChart(tokenData) {
const ctx = document.getElementById('tokenChart').getContext('2d');
// Prepare data for chart
const labels = Object.keys(tokenData).slice(-15); // Last 15 days
const inputData = labels.map((date) => tokenData[date]?.input || 0);
const outputData = labels.map((date) => tokenData[date]?.output || 0);
const totalData = labels.map((date) => tokenData[date]?.total || 0);
new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Input Tokens',
data: inputData,
borderColor: '#3498db',
backgroundColor: 'rgba(52, 152, 219, 0.1)',
tension: 0.1,
},
{
label: 'Output Tokens',
data: outputData,
borderColor: '#2ecc71',
backgroundColor: 'rgba(46, 204, 113, 0.1)',
tension: 0.1,
},
{
label: 'Total Tokens',
data: totalData,
borderColor: '#9b59b6',
backgroundColor: 'rgba(155, 89, 182, 0.1)',
tension: 0.1,
},
],
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Token Usage Over Time',
},
},
scales: {
y: {
beginAtZero: true,
},
},
},
});
}
function createHourChart(hourData) {
const ctx = document.getElementById('hourChart').getContext('2d');
// Prepare data for chart - use all 24 hours
const labels = Array.from({ length: 24 }, (_, i) => `${i}:00`);
const data = labels.map((_, i) => hourData[i] || 0);
new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [
{
label: 'Activity per Hour',
data: data,
backgroundColor: 'rgba(52, 152, 219, 0.7)',
borderColor: 'rgba(52, 152, 219, 1)',
borderWidth: 1,
},
],
},
options: {
indexAxis: 'y',
scales: {
x: {
beginAtZero: true,
},
},
plugins: {
legend: {
display: false,
},
},
},
});
}
function displayAchievements(achievements) {
const container = document.getElementById('achievements-container');
if (achievements.length === 0) {
container.innerHTML = '<p>No achievements yet. Keep coding!</p>';
return;
}
container.innerHTML = '';
achievements.forEach((achievement) => {
const achievementEl = document.createElement('div');
achievementEl.className = 'achievement';
achievementEl.innerHTML = `
<strong>${achievement.name}</strong>
<p>${achievement.description}</p>
`;
container.appendChild(achievementEl);
});
}
// Export functionality
document
.getElementById('export-btn')
.addEventListener('click', function () {
// Hide the button temporarily
const exportBtn = document.getElementById('export-btn');
exportBtn.style.display = 'none';
// Use html2canvas to capture the dashboard
html2canvas(document.querySelector('.container'), {
scale: 2, // Higher resolution
useCORS: true,
logging: false,
})
.then((canvas) => {
// Convert canvas to data URL
const imgData = canvas.toDataURL('image/png');
// Create download link
const link = document.createElement('a');
link.href = imgData;
link.download = `qwen-insights-${new Date().toISOString().slice(0, 10)}.png`;
link.click();
// Show the button again
exportBtn.style.display = 'block';
})
.catch((err) => {
console.error('Error capturing image:', err);
alert('Failed to export image. Please try again.');
exportBtn.style.display = 'block';
});
});
// Load insights when page loads
window.onload = loadInsights;
</script>
</body>
</html>

View file

@ -0,0 +1,190 @@
/**
* @license
* Copyright 2025 Qwen Code
* SPDX-License-Identifier: Apache-2.0
*/
import type { CommandContext, SlashCommand } from './types.js';
import { CommandKind } from './types.js';
import { MessageType } from '../types.js';
import { t } from '../../i18n/index.js';
import { spawn } from 'child_process';
import { join } from 'path';
import os from 'os';
import { registerCleanup } from '../../utils/cleanup.js';
import net from 'net';
// Track the insight server subprocess so we can terminate it on quit
let insightServerProcess: import('child_process').ChildProcess | null = null;
// Find an available port starting from a default port
async function findAvailablePort(startingPort: number = 3000): Promise<number> {
return new Promise((resolve, reject) => {
let port = startingPort;
const checkPort = () => {
const server = net.createServer();
server.listen(port, () => {
server.once('close', () => {
resolve(port);
});
server.close();
});
server.on('error', (err: NodeJS.ErrnoException) => {
if (err.code === 'EADDRINUSE') {
port++; // Try next port
checkPort();
} else {
reject(err);
}
});
};
checkPort();
});
}
export const insightCommand: SlashCommand = {
name: 'insight',
get description() {
return t(
'generate personalized programming insights from your chat history',
);
},
kind: CommandKind.BUILT_IN,
action: async (context: CommandContext) => {
try {
context.ui.setDebugMessage(t('Starting insight server...'));
// If there's an existing insight server process, terminate it first
if (insightServerProcess && !insightServerProcess.killed) {
insightServerProcess.kill();
insightServerProcess = null;
}
// Find an available port
const availablePort = await findAvailablePort(3000);
const projectsDir = join(os.homedir(), '.qwen', 'projects');
// Path to the insight server script
const insightScriptPath = join(
process.cwd(),
'packages',
'cli',
'src',
'services',
'insightServer.ts',
);
// Spawn the insight server process
const serverProcess = spawn('npx', ['tsx', insightScriptPath], {
stdio: 'pipe',
env: {
...process.env,
NODE_ENV: 'production',
BASE_DIR: projectsDir,
PORT: String(availablePort),
},
});
// Store the server process for cleanup
insightServerProcess = serverProcess;
// Register cleanup function to terminate the server process on quit
registerCleanup(() => {
if (insightServerProcess && !insightServerProcess.killed) {
insightServerProcess.kill();
insightServerProcess = null;
}
});
serverProcess.stderr.on('data', (data) => {
// Forward error output to parent process stderr
process.stderr.write(`Insight server error: ${data}`);
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Insight server error: ${data.toString()}`,
},
Date.now(),
);
});
serverProcess.on('close', (code) => {
console.log(`Insight server process exited with code ${code}`);
context.ui.setDebugMessage(t('Insight server stopped.'));
// Reset the reference when the process closes
if (insightServerProcess === serverProcess) {
insightServerProcess = null;
}
});
const url = `http://localhost:${availablePort}`;
// Open browser automatically
const openBrowser = async () => {
try {
const { exec } = await import('child_process');
const { promisify } = await import('util');
const execAsync = promisify(exec);
switch (process.platform) {
case 'darwin': // macOS
await execAsync(`open ${url}`);
break;
case 'win32': // Windows
await execAsync(`start ${url}`);
break;
default: // Linux and others
await execAsync(`xdg-open ${url}`);
}
context.ui.addItem(
{
type: MessageType.INFO,
text: `Insight server started. Visit: ${url}`,
},
Date.now(),
);
} catch (err) {
console.error('Failed to open browser automatically:', err);
context.ui.addItem(
{
type: MessageType.INFO,
text: `Insight server started. Please visit: ${url}`,
},
Date.now(),
);
}
};
// Wait for the server to start (give it some time to bind to the port)
setTimeout(openBrowser, 1000);
// Inform the user that the server is running
context.ui.addItem(
{
type: MessageType.INFO,
text: t(
'Insight server started. Check your browser for the visualization.',
),
},
Date.now(),
);
} catch (error) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: t('Failed to start insight server: {{error}}', {
error: (error as Error).message,
}),
},
Date.now(),
);
}
},
};