b4ddd4f79a
설정 패널 dropdown 이 LM Studio 에서 모델 1개만 보이고, 변경하면 원복되던 회귀 수정. 원인: settings 패널의 discoverModels 가 REST /v1/models 만 사용 → JIT 로딩 환경에서 '현재 로드된' 모델만 반환. (사이드바는 SDK 로 전체를 가져옴) - discoverModels: LM Studio SDK listDownloadedModels(전체 다운로드) 우선, 실패/0개면 REST 폴백. 사이드바 ModelDiscovery 와 동일 정책으로 통일 → 두 경로가 갈라져 다시 회귀하지 않도록 가이드라인 주석 명시. - SettingsPanelDeps/SettingsSetupDeps 에 lmStudioDownloaded 콜백 추가, extension.ts 에서 lmStudioClient.listDownloadedCached 연결. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
365 lines
17 KiB
TypeScript
365 lines
17 KiB
TypeScript
import * as vscode from 'vscode';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
// TeamOps + System + Datacollect handler 자기 등록 — side-effect import. slashRouter
|
|
// 가 이미 로드된 후 등록되도록 entry point 에서. (v2.2.196~201 도메인별 파일 분리)
|
|
import './features/teamops/handlers';
|
|
import './features/system/handlers';
|
|
import './features/datacollect/handlers';
|
|
// axios removed in favor of native fetch
|
|
import {
|
|
_getBrainDir,
|
|
_isBrainDirExplicitlySet,
|
|
findBrainFiles,
|
|
SYSTEM_PROMPT,
|
|
buildApiUrl,
|
|
logError,
|
|
logInfo,
|
|
resolveEngine,
|
|
openInEditorGroup
|
|
} from './utils';
|
|
import { getConfig, validateConfig } from './config';
|
|
import { AgentExecutor } from './agent';
|
|
import { BridgeServer } from './bridge';
|
|
import { SidebarChatProvider } from './sidebarProvider';
|
|
import { HealthCheckMonitor } from './core/health';
|
|
import { initAstraPathResolver } from './core/astraPath';
|
|
import { LMStudioClient } from './lmstudio/client';
|
|
import { ActivityTracker } from './lmstudio/activityTracker';
|
|
import { ModelLifecycleManager } from './lmstudio/lifecycleManager';
|
|
import { LMStudioStreamer } from './lmstudio/streamer';
|
|
import { NodeSystemSpecsProvider, HeuristicModelMemoryEstimator } from './system/specs';
|
|
import { ApprovalQueue } from './features/approval/approvalQueue';
|
|
import { ApprovalPanelProvider } from './features/approval/approvalPanelProvider';
|
|
import { ApprovalStatusBar } from './features/approval/approvalStatusBar';
|
|
import { TelegramHttpClient } from './integrations/telegram/telegramClient';
|
|
import { TelegramBot } from './integrations/telegram/telegramBot';
|
|
import { getBrainTokenIndex, clearBrainTokenIndex } from './retrieval';
|
|
import { lessonTemplate, lessonSlug, parseLessonFrontmatter, normalizeLessonTitle, bumpLessonOccurrences } from './retrieval/lessonHelpers';
|
|
import { runConnectGoogleCalendarIcal, runConnectGoogleCalendarOAuth } from './extension/calendarSetup';
|
|
import { runInitialSetup } from './extension/initialSetup';
|
|
import { startStocksWatcher } from './features/stocks';
|
|
import { registerProviderCommands } from './extension/providerCommands';
|
|
import { registerScaffoldCommand } from './extension/scaffoldCommand';
|
|
import { registerLessonCommands } from './extension/lessonCommands';
|
|
import { registerEvalCommands } from './extension/evalCommands';
|
|
import { registerTelegramCommands, TELEGRAM_TOKEN_SECRET_KEY, type TelegramTokenStore } from './extension/telegramCommands';
|
|
import { setupSettingsPanel } from './extension/settingsSetup';
|
|
import { createTelegramBot } from './integrations/telegram/telegramSetup';
|
|
|
|
let _lifecycleManager: ModelLifecycleManager | undefined;
|
|
let _telegramBot: TelegramBot | undefined;
|
|
|
|
/**
|
|
* Astra Extension Entry Point
|
|
*/
|
|
export async function activate(context: vscode.ExtensionContext) {
|
|
// Activation 시점 popup + DevTools console — 사용자 환경(Antigravity 등 VS Code 변종)
|
|
// 에서 우리 vsix가 실제로 활성화됐는지 결정적으로 가시화. console.error는 사용자가
|
|
// F12 DevTools console에서 다른 모든 출력과 함께 그대로 보인다 (logInfo의 OutputChannel
|
|
// 과 별개 채널 — popup도 OutputChannel도 못 보는 경우의 마지막 안전망).
|
|
const ext = vscode.extensions.getExtension('g1nation.astra');
|
|
const version = ext?.packageJSON?.version || '(unknown)';
|
|
console.log(`[ASTRA-DEBUG] activate v${version} pid=${process.pid}`);
|
|
void vscode.window.showInformationMessage(`📡 Astra v${version} activated (PID=${process.pid})`);
|
|
logInfo(`Astra activating... version=${version} pid=${process.pid}`);
|
|
|
|
// Initialize Astra Path Resolver (.astra → ConnectAI/.astra/)
|
|
initAstraPathResolver(context);
|
|
|
|
// Start Environment Health Monitoring
|
|
HealthCheckMonitor.runAllChecks();
|
|
HealthCheckMonitor.startInterval(600000); // Check every 10 mins
|
|
|
|
// 0. Validate Configuration
|
|
const validation = validateConfig();
|
|
if (!validation.valid) {
|
|
vscode.window.showErrorMessage(`Astra Configuration Error: ${validation.errors.join(' ')}`);
|
|
logError('Configuration validation failed.', { errors: validation.errors });
|
|
}
|
|
|
|
// 1. Ensure Brain Directory
|
|
await _ensureBrainDir(context);
|
|
|
|
// 2. Initialize LM Studio Lifecycle Subsystem
|
|
let provider: SidebarChatProvider | undefined;
|
|
const initialUrl = getConfig().ollamaUrl;
|
|
const activityTracker = new ActivityTracker();
|
|
const lmStudioClient = new LMStudioClient(initialUrl);
|
|
const systemSpecs = new NodeSystemSpecsProvider();
|
|
const memoryEstimator = new HeuristicModelMemoryEstimator();
|
|
logInfo('System specs detected.', { summary: systemSpecs.get().summary });
|
|
const lifecycle = new ModelLifecycleManager({
|
|
client: lmStudioClient,
|
|
activity: activityTracker,
|
|
getConfig: () => {
|
|
// Read from getConfig() so we share the same setting parsers (incl. gpuOffloadRatio coercion)
|
|
// with the rest of the codebase instead of duplicating the logic here.
|
|
const ag = getConfig();
|
|
const cfg = vscode.workspace.getConfiguration('g1nation');
|
|
return {
|
|
idleTimeoutMs: cfg.get<number>('lmStudio.idleTimeoutMs', 300000),
|
|
autoLoadOnSelect: cfg.get<boolean>('lmStudio.autoLoadOnSelect', true),
|
|
loadConfig: {
|
|
flashAttention: ag.lmStudioLoad.flashAttention,
|
|
gpuOffloadRatio: ag.lmStudioLoad.gpuOffloadRatio,
|
|
offloadKVCacheToGpu: ag.lmStudioLoad.offloadKVCacheToGpu,
|
|
keepModelInMemory: ag.lmStudioLoad.keepModelInMemory,
|
|
useFp16ForKVCache: ag.lmStudioLoad.useFp16ForKVCache,
|
|
evalBatchSize: ag.lmStudioLoad.evalBatchSize,
|
|
},
|
|
draftModel: ag.lmStudioDraftModel || undefined,
|
|
};
|
|
},
|
|
notifyError: (msg) => provider?.postLmStudioError(msg),
|
|
initialEngine: resolveEngine(initialUrl),
|
|
systemSpecs,
|
|
memoryEstimator,
|
|
});
|
|
_lifecycleManager = lifecycle;
|
|
context.subscriptions.push({ dispose: () => activityTracker.dispose() });
|
|
context.subscriptions.push({ dispose: () => lifecycle.dispose() });
|
|
|
|
// React to engine URL changes — re-target the SDK and reset state.
|
|
context.subscriptions.push(
|
|
vscode.workspace.onDidChangeConfiguration((e) => {
|
|
if (!e.affectsConfiguration('g1nation.ollamaUrl')) return;
|
|
const newUrl = vscode.workspace.getConfiguration('g1nation').get<string>('ollamaUrl', '');
|
|
lmStudioClient.setBaseUrl(newUrl);
|
|
lifecycle.setEngine(resolveEngine(newUrl));
|
|
})
|
|
);
|
|
|
|
// Keep the sidebar's model dropdown in sync when defaultModel / ollamaUrl is
|
|
// changed from elsewhere (Settings panel, raw settings.json, …). Without
|
|
// this the user sees a desync: Settings shows the new model, sidebar still
|
|
// shows the old one until a manual refresh.
|
|
context.subscriptions.push(
|
|
vscode.workspace.onDidChangeConfiguration((e) => {
|
|
const touchedModel = e.affectsConfiguration('g1nation.defaultModel');
|
|
const touchedUrl = e.affectsConfiguration('g1nation.ollamaUrl');
|
|
if (!touchedModel && !touchedUrl) return;
|
|
// _sendModels is best-effort; the provider may not have a webview
|
|
// attached yet during very early activation.
|
|
void provider?._sendModels(touchedUrl);
|
|
})
|
|
);
|
|
|
|
// 3. Initialize Approval subsystem (queue + panel webview + status bar badge)
|
|
// Astra 2.81: sidebar view container is gone; all webviews open in editor
|
|
// column 3 instead. We don't register a WebviewViewProvider — panels are
|
|
// created on-demand via openAsPanel().
|
|
const approvalQueue = new ApprovalQueue();
|
|
const approvalPanel = new ApprovalPanelProvider(context.extensionUri, approvalQueue);
|
|
const approvalStatusBar = new ApprovalStatusBar(approvalQueue);
|
|
context.subscriptions.push(
|
|
approvalStatusBar,
|
|
{ dispose: () => approvalQueue.dispose() },
|
|
vscode.commands.registerCommand(ApprovalStatusBar.focusCommand, () => approvalPanel.focus()),
|
|
);
|
|
|
|
// 4. Initialize Agent Executor (with stream lifecycle hooks + LM Studio SDK streamer + approval queue)
|
|
const lmStudioStreamer = new LMStudioStreamer(lmStudioClient);
|
|
const agent = new AgentExecutor(context, {
|
|
onStreamLifecycle: {
|
|
start: () => lifecycle.onStreamStart(),
|
|
end: () => lifecycle.onStreamEnd(),
|
|
},
|
|
lmStudioStreamer,
|
|
approvalQueue,
|
|
});
|
|
|
|
// 4. Initialize Chat Provider (renders into an editor column, not a sidebar view)
|
|
provider = new SidebarChatProvider(context.extensionUri, context, agent, {
|
|
lifecycle,
|
|
activity: activityTracker,
|
|
loadedModels: () => lmStudioClient.listLoadedCached(),
|
|
downloadedModels: () => lmStudioClient.listDownloadedCached(),
|
|
});
|
|
// One-time repair: rewrite any chronicle projects that were saved with the
|
|
// workspace parent as their `projectRoot` (a side-effect of the old
|
|
// pre-multi-subproject activation code). Idempotent and silent when there's
|
|
// nothing to fix.
|
|
void provider._repairCorruptedChronicleProjectRoots().catch((e: any) => {
|
|
logError('architecture: chronicle repair failed.', { error: e?.message ?? String(e) });
|
|
});
|
|
context.subscriptions.push(
|
|
vscode.commands.registerCommand('g1nation.openChat', () => {
|
|
provider!.openAsPanel(vscode.ViewColumn.Three);
|
|
})
|
|
);
|
|
|
|
// Datacollect Python 의존성 자동 점검·설치 — `Astra: Setup Datacollect Dependencies`.
|
|
// 슬래시 명령(/youtube /research /wikify 등)이 yt-dlp / youtube-transcript-api 같은
|
|
// Python 패키지를 필요로 하는데, Astra 확장만 깔면 그게 자동으로 따라오지 않아
|
|
// 사용자가 매번 수동 pip install 해야 했음. 이 명령으로 한 번에 처리.
|
|
context.subscriptions.push(
|
|
vscode.commands.registerCommand('g1nation.setupDatacollect', async () => {
|
|
const { runDatacollectSetup } = await import('./features/setup/datacollectSetup');
|
|
await runDatacollectSetup();
|
|
})
|
|
);
|
|
|
|
// ── Activity Bar launcher view ────────────────────────────────────────
|
|
// Adds a sparkle (✦) icon to VS Code's left activity bar. Clicking it
|
|
// opens a small sidebar with action buttons (Open Chat / New Chat /
|
|
// Settings / Company / Architecture). Solves the user-reported pain
|
|
// point: when the Astra Chat editor tab is accidentally closed, there
|
|
// was no one-click way to reopen it short of restarting the extension.
|
|
//
|
|
// The view itself has no items — VS Code renders the `viewsWelcome`
|
|
// content from package.json instead, which is just a list of command
|
|
// links. Cheap, theme-aware, no webview to maintain.
|
|
const astraLauncherProvider: vscode.TreeDataProvider<never> = {
|
|
getTreeItem: () => new vscode.TreeItem(''),
|
|
getChildren: () => [],
|
|
};
|
|
context.subscriptions.push(
|
|
vscode.window.registerTreeDataProvider('g1nation-astra-launcher', astraLauncherProvider),
|
|
);
|
|
|
|
// 4. Initialize Bridge Server (Port 4825)
|
|
const bridge = new BridgeServer(provider);
|
|
try {
|
|
bridge.start();
|
|
logInfo('Bridge server started on port 4825.');
|
|
} catch (err) {
|
|
logError('Failed to start bridge server.', err);
|
|
}
|
|
|
|
|
|
// 5. Register Core Commands
|
|
context.subscriptions.push(
|
|
vscode.commands.registerCommand('g1nation.focusInput', () => {
|
|
provider.focusInput();
|
|
})
|
|
);
|
|
|
|
context.subscriptions.push(
|
|
vscode.commands.registerCommand('g1nation.clearChat', () => {
|
|
provider.clearChat();
|
|
})
|
|
);
|
|
|
|
context.subscriptions.push(
|
|
vscode.commands.registerCommand('g1nation.syncBrain', async () => {
|
|
await provider!.syncBrain();
|
|
})
|
|
);
|
|
|
|
// Telegram Bot integration — opt-in (g1nation.telegram.enabled), token in SecretStorage.
|
|
// `tokenStore` 는 telegramCommands 모듈과 공유 — 모듈이 secrets change 시
|
|
// `tokenStore.current` 를 갱신하면 client 가 다음 호출에서 새 값을 본다.
|
|
const tokenStore: TelegramTokenStore = {
|
|
current: (await context.secrets.get(TELEGRAM_TOKEN_SECRET_KEY)) || '',
|
|
};
|
|
const telegramClient = new TelegramHttpClient({
|
|
getToken: () => tokenStore.current,
|
|
});
|
|
|
|
// buildTelegramSystemPrompt / looksLikeWorkOrder / buildTelegramCompanyContext /
|
|
// latestCompanySessionDir / chunkTelegramMessage → `src/integrations/telegram/promptBuilders.ts`
|
|
// handler 콜백 + AIService → `src/integrations/telegram/telegramSetup.ts`
|
|
const telegramBot = createTelegramBot(context, { telegramClient, getProvider: () => provider });
|
|
_telegramBot = telegramBot;
|
|
|
|
context.subscriptions.push(
|
|
// refresh + dispose + listeners + setBotToken/clearBotToken/testConnection → `src/extension/telegramCommands.ts`
|
|
...registerTelegramCommands(context, { telegramBot, telegramClient, tokenStore }),
|
|
// knowledge map + lesson cards → `src/extension/lessonCommands.ts`
|
|
...registerLessonCommands({ getAgent: () => agent }),
|
|
// 검색 평가 하니스 (recall@k / MRR) → `src/extension/evalCommands.ts`
|
|
...registerEvalCommands(),
|
|
// architecture / company / calendar / devil commands → `src/extension/providerCommands.ts`
|
|
...registerProviderCommands(context, { getProvider: () => provider }),
|
|
);
|
|
|
|
// listLessonFiles / createLessonCard / manageLessons → `src/extension/lessons.ts`
|
|
|
|
// Astra Settings webview — single entry point for user-facing config (Phase 5-A: Telegram only).
|
|
// panel 인스턴스 + listener/command disposables → `src/extension/settingsSetup.ts`
|
|
const { settingsPanel, disposables: settingsDisposables } = setupSettingsPanel(context, {
|
|
telegramClient,
|
|
telegramBot,
|
|
// 모델 dropdown 이 보유 모델 전부를 보이도록 SDK 다운로드 목록을 전달.
|
|
lmStudioDownloaded: () => lmStudioClient.listDownloadedCached(),
|
|
});
|
|
context.subscriptions.push(...settingsDisposables);
|
|
|
|
// g1nation.scaffoldProject → `src/extension/scaffoldCommand.ts`
|
|
context.subscriptions.push(registerScaffoldCommand());
|
|
|
|
// 6. Run Initial Setup (Automatic Model/Engine Detection)
|
|
const setupComplete = context.globalState.get<boolean>('setupComplete', false);
|
|
if (!setupComplete) {
|
|
await runInitialSetup(context);
|
|
}
|
|
|
|
// Stocks watcher — VS Code 시작 시 자동 활성화. KST 09:00 / 15:00 자동 트리거.
|
|
// disposable 은 subscriptions 에 푸시해 종료 시 timer cleanup.
|
|
context.subscriptions.push(startStocksWatcher(context));
|
|
|
|
// 7. Auto-open all three Astra webviews as tabs in editor column 3.
|
|
// The sidebar/activity-bar entry point was removed in 2.81 — all three views
|
|
// (Chat, Approvals, Settings) now stack as tabs in the third editor column.
|
|
// Order matters: Chat opens last so it ends up as the active tab.
|
|
try {
|
|
approvalPanel.openAsPanel(vscode.ViewColumn.Three);
|
|
await settingsPanel.openAsPanel(vscode.ViewColumn.Three);
|
|
provider!.openAsPanel(vscode.ViewColumn.Three);
|
|
} catch (e) {
|
|
logError('Failed to auto-open Astra panels.', e);
|
|
}
|
|
}
|
|
|
|
export async function deactivate() {
|
|
HealthCheckMonitor.dispose();
|
|
// Release the in-memory brain token index (and any pending debounced disk
|
|
// write timer) — the `_states` Map is otherwise never cleared for the
|
|
// process lifetime.
|
|
clearBrainTokenIndex();
|
|
if (_telegramBot) {
|
|
try { await _telegramBot.stop(); } catch (e) { logError('Telegram bot stop during deactivate failed.', e); }
|
|
_telegramBot = undefined;
|
|
}
|
|
if (_lifecycleManager) {
|
|
try {
|
|
await _lifecycleManager.disposeAndUnload(2000);
|
|
} catch (e) {
|
|
logError('Lifecycle dispose during deactivate failed.', e);
|
|
}
|
|
_lifecycleManager = undefined;
|
|
}
|
|
}
|
|
|
|
// runConnectGoogleCalendarIcal / runConnectGoogleCalendarOAuth → `src/extension/calendarSetup.ts`
|
|
|
|
// runInitialSetup → `src/extension/initialSetup.ts`
|
|
|
|
async function _ensureBrainDir(context: vscode.ExtensionContext): Promise<string | null> {
|
|
if (_isBrainDirExplicitlySet()) {
|
|
const dir = _getBrainDir();
|
|
if (!fs.existsSync(dir)) {
|
|
try {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
} catch (e) {
|
|
logError('Failed to create brain directory.', e);
|
|
}
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
const defaultDir = _getBrainDir();
|
|
if (!fs.existsSync(defaultDir)) {
|
|
try {
|
|
fs.mkdirSync(defaultDir, { recursive: true });
|
|
// Create a welcome file
|
|
fs.writeFileSync(path.join(defaultDir, 'Welcome.md'), "# Welcome to your Second Brain\n\nAstra will store and retrieve knowledge from here.");
|
|
} catch (e) {
|
|
logError('Failed to initialize default brain directory.', e);
|
|
}
|
|
}
|
|
return defaultDir;
|
|
}
|
|
|