chore: bump version to 2.80.27 and update core features

This commit is contained in:
g1nation
2026-05-09 01:16:12 +09:00
parent 5ffb472d22
commit 3220a126fd
41 changed files with 4457 additions and 72 deletions
+212 -2
View File
@@ -21,8 +21,22 @@ 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 { FileSystemProjectScaffolder } from './scaffolder/projectScaffolder';
import type { ProjectTemplateId } from './scaffolder/templates';
import { TelegramHttpClient } from './integrations/telegram/telegramClient';
import { TelegramBot } from './integrations/telegram/telegramBot';
import { AIService } from './core/services';
import { SettingsPanelProvider } from './features/settings/settingsPanelProvider';
let _lifecycleManager: ModelLifecycleManager | undefined;
let _telegramBot: TelegramBot | undefined;
const TELEGRAM_TOKEN_SECRET_KEY = 'g1nation.telegram.botToken';
/**
* Astra Extension Entry Point
@@ -52,6 +66,9 @@ export async function activate(context: vscode.ExtensionContext) {
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,
@@ -64,6 +81,8 @@ export async function activate(context: vscode.ExtensionContext) {
},
notifyError: (msg) => provider?.postLmStudioError(msg),
initialEngine: resolveEngine(initialUrl),
systemSpecs,
memoryEstimator,
});
_lifecycleManager = lifecycle;
context.subscriptions.push({ dispose: () => activityTracker.dispose() });
@@ -79,18 +98,48 @@ export async function activate(context: vscode.ExtensionContext) {
})
);
// 3. Initialize Agent Executor (with stream lifecycle hooks)
// 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)
const approvalQueue = new ApprovalQueue();
const approvalPanel = new ApprovalPanelProvider(context.extensionUri, approvalQueue);
const approvalStatusBar = new ApprovalStatusBar(approvalQueue);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(ApprovalPanelProvider.viewType, approvalPanel),
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 Sidebar Provider
provider = new SidebarChatProvider(context.extensionUri, context, agent, {
lifecycle,
activity: activityTracker,
loadedModels: () => lmStudioClient.listLoadedCached(),
});
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(SidebarChatProvider.viewType, provider)
@@ -120,7 +169,164 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('g1nation.syncBrain', async () => {
await provider.syncBrain();
await provider!.syncBrain();
})
);
// Telegram Bot integration — opt-in (g1nation.telegram.enabled), token in SecretStorage.
let _cachedTelegramToken = (await context.secrets.get(TELEGRAM_TOKEN_SECRET_KEY)) || '';
const telegramClient = new TelegramHttpClient({
getToken: () => _cachedTelegramToken,
});
const telegramAi = new AIService();
const telegramBot = new TelegramBot({
client: telegramClient,
handle: async (text, chatId) => {
const cfg = vscode.workspace.getConfiguration('g1nation');
const allowed = cfg.get<number[]>('telegram.allowedChatIds', []) || [];
if (allowed.length > 0 && !allowed.includes(chatId)) {
logInfo('Telegram message from unallowed chat ignored.', { chatId });
return null;
}
try {
const reply = await telegramAi.call(text);
return (reply && reply.trim()) ? reply : '(빈 응답)';
} catch (e: any) {
return `⚠️ Astra error: ${e?.message ?? e}`;
}
},
});
_telegramBot = telegramBot;
const refreshTelegramBot = async () => {
const enabled = vscode.workspace.getConfiguration('g1nation').get<boolean>('telegram.enabled', false);
const tokenPresent = !!_cachedTelegramToken.trim();
if (enabled && tokenPresent) {
telegramBot.start();
} else if (telegramBot.isRunning()) {
await telegramBot.stop();
}
};
void refreshTelegramBot();
context.subscriptions.push(
{ dispose: () => { void telegramBot.stop(); } },
vscode.workspace.onDidChangeConfiguration(async (e) => {
if (e.affectsConfiguration('g1nation.telegram.enabled')) {
await refreshTelegramBot();
}
}),
context.secrets.onDidChange(async (e) => {
if (e.key !== TELEGRAM_TOKEN_SECRET_KEY) return;
_cachedTelegramToken = (await context.secrets.get(TELEGRAM_TOKEN_SECRET_KEY)) || '';
await refreshTelegramBot();
}),
vscode.commands.registerCommand('g1nation.telegram.setBotToken', async () => {
const token = await vscode.window.showInputBox({
prompt: 'Telegram bot token (BotFather에서 발급, 형식: 123456:ABC...)',
placeHolder: '123456789:AA...',
password: true,
ignoreFocusOut: true,
validateInput: (v) => /^\d+:[A-Za-z0-9_-]{20,}$/.test((v || '').trim())
? null
: '형식이 올바르지 않습니다 (숫자ID:문자열).',
});
if (!token) return;
await context.secrets.store(TELEGRAM_TOKEN_SECRET_KEY, token.trim());
vscode.window.showInformationMessage(
'Telegram bot token이 저장되었습니다. settings에서 g1nation.telegram.enabled = true 로 켜세요.'
);
}),
vscode.commands.registerCommand('g1nation.telegram.clearBotToken', async () => {
await context.secrets.delete(TELEGRAM_TOKEN_SECRET_KEY);
vscode.window.showInformationMessage('Telegram bot token이 삭제되었습니다.');
}),
vscode.commands.registerCommand('g1nation.telegram.testConnection', async () => {
const token = (await context.secrets.get(TELEGRAM_TOKEN_SECRET_KEY)) || '';
if (!token) {
vscode.window.showErrorMessage('먼저 "Astra: Set Telegram Bot Token" 명령으로 토큰을 등록하세요.');
return;
}
try {
const me = await telegramClient.getMe();
vscode.window.showInformationMessage(
`Telegram 연결 성공: @${me.username || me.first_name} (id ${me.id})`
);
} catch (e: any) {
vscode.window.showErrorMessage(`Telegram 연결 실패: ${e?.message ?? e}`);
}
}),
);
// Astra Settings webview — single entry point for user-facing config (Phase 5-A: Telegram only).
const settingsPanel = new SettingsPanelProvider({
extensionUri: context.extensionUri,
secrets: context.secrets,
telegramClient,
telegramBot,
});
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(SettingsPanelProvider.viewType, settingsPanel),
// Refresh the settings UI whenever any g1nation.* config changes (toggle, allowedChatIds, …).
vscode.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration('g1nation')) void settingsPanel.refresh();
}),
// Same for SecretStorage updates (token saved/cleared from elsewhere).
context.secrets.onDidChange((e) => {
if (e.key === TELEGRAM_TOKEN_SECRET_KEY) void settingsPanel.refresh();
}),
vscode.commands.registerCommand('g1nation.settings.focus', () => settingsPanel.focus()),
vscode.commands.registerCommand('g1nation.settings.diagnose', async () => {
// Diagnostic helper: shows whether the view is registered + opens it if so.
// Useful when the user reports "Set 버튼이 안 먹는다" and we want to confirm
// the new build is actually loaded.
try {
await settingsPanel.focus();
vscode.window.showInformationMessage('Astra Settings 패널이 열렸습니다. 사이드바 Settings 항목을 확인하세요.');
} catch (e: any) {
vscode.window.showErrorMessage(`Settings 패널 열기 실패 (확장 reload가 필요할 수 있음): ${e?.message ?? e}`);
}
}),
);
// Project Scaffolder — Astra의 Developer 빠른 시작 명령
const scaffolder = new FileSystemProjectScaffolder();
context.subscriptions.push(
vscode.commands.registerCommand('g1nation.scaffoldProject', async () => {
const folders = vscode.workspace.workspaceFolders;
if (!folders || folders.length === 0) {
vscode.window.showErrorMessage('워크스페이스 폴더를 먼저 여세요.');
return;
}
const name = await vscode.window.showInputBox({
placeHolder: '프로젝트 이름 (영문/숫자/_/-, 2~40자)',
prompt: 'Astra가 워크스페이스 안에 만들 프로젝트 폴더 이름',
validateInput: (v) => /^[a-zA-Z0-9_-]{2,40}$/.test(v.trim()) ? null : '영문/숫자/_/- 만, 2~40자',
});
if (!name) return;
const picked = await vscode.window.showQuickPick(
scaffolder.listTemplates().map(t => ({ label: t.label, detail: t.detail, id: t.id })),
{ placeHolder: '템플릿 선택' }
);
if (!picked) return;
const result = await scaffolder.scaffold({
name: name.trim(),
template: picked.id as ProjectTemplateId,
rootDir: folders[0].uri.fsPath,
});
if (!result.ok) {
vscode.window.showErrorMessage(`프로젝트 생성 실패: ${result.error}`);
return;
}
const action = await vscode.window.showInformationMessage(
`${name} 생성 완료 — ${result.projectPath}`,
'폴더 열기',
'닫기'
);
if (action === '폴더 열기') {
await vscode.commands.executeCommand('revealFileInOS', vscode.Uri.file(result.projectPath));
}
})
);
@@ -133,6 +339,10 @@ export async function activate(context: vscode.ExtensionContext) {
export async function deactivate() {
HealthCheckMonitor.dispose();
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);