From 36db170844c6f6f5b5f2e5426a0713719bc371ff Mon Sep 17 00:00:00 2001 From: g1nation Date: Sat, 23 May 2026 09:37:29 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20v2.2.64=20=E2=80=94=20LM=20Studio=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=20=EB=B0=9C=EA=B2=AC/=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20+=20macOS=20=EC=85=B8=20=ED=98=B8=ED=99=98?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LM Studio 모델 dropdown을 SDK system.listDownloadedModels('llm') 으로 조회하도록 변경. REST /v1/models 는 JIT 옵션이 꺼져 있으면 로드된 모델만 반환하여 macOS 환경에서 dropdown 이 비거나 fallback 한 줄만 남던 문제 해결. SDK 실패 시 REST 로 자동 fallback. - LM Studio 로드/언로드 실패를 readyBar 의 영속 segment 로 표시. 모델을 다시 선택하면 clearLmStudioError() 로 해제. - src/security.ts: PowerShell '&&' rewrite 를 win32 에서만 수행. macOS/Linux 에서는 'if (\$?) { ... }' 가 zsh/bash 문법 오류라 명령 자체가 깨졌음. - src/utils.ts: system prompt 에 OS 별 [ENVIRONMENT] 블록 동적 주입 (셸/경로 스타일/체이닝 연산자). 'cd E:\\... ; ...' 같은 Windows 전용 예시를 macOS 에서 그대로 따라하던 회귀 차단. - 테스트 mock 에 listDownloaded() 추가. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...d46d2ca2057b05c488be1dcf439166ac5a9a1.json | 2 +- ...9f4f39d2bc368f77456c37b5eef9a94a66b5c.json | 2 +- ...5c7a44d7661af673b24e3f49551a7a2e50280.json | 2 +- ...adc543795e4b427b64540a49c9ab27c7fe213.json | 4 +- ...son => stress_conflict_1779495116612.json} | 20 ++--- media/sidebar.js | 3 + package.json | 2 +- src/extension.ts | 1 + src/lmstudio/client.ts | 21 +++++ src/security.ts | 5 ++ src/sidebar/chatHandlers.ts | 3 + src/sidebarProvider.ts | 79 +++++++++++++++---- src/utils.ts | 56 +++++++++++-- tests/lmStudioLifecycle.test.ts | 4 + tests/lmStudioStreamer.test.ts | 1 + 15 files changed, 168 insertions(+), 37 deletions(-) rename .astra/tests/stress/.astra/missions/{stress_conflict_1779444327825.json => stress_conflict_1779495116612.json} (80%) diff --git a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json index 2914e96..961f8fc 100644 --- a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json +++ b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json @@ -1,5 +1,5 @@ { "result": "Final report with inconsistencies. This should be long enough to pass validation.", - "createdAt": 1779444327842, + "createdAt": 1779495116625, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json index 02f6601..12cd895 100644 --- a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json +++ b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json @@ -1,5 +1,5 @@ { "result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", - "createdAt": 1779444327840, + "createdAt": 1779495116625, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json index b7886f0..0c29e17 100644 --- a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json +++ b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json @@ -1,5 +1,5 @@ { "result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.", - "createdAt": 1779444327837, + "createdAt": 1779495116624, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json index 0f89d62..28d5f34 100644 --- a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json +++ b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json @@ -1,5 +1,5 @@ { - "result": "---\nid: stress_conflict_1779444327825\ndate: 2026-05-22T10:05:27.844Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (11ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (2ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (3ms)\n", - "createdAt": 1779444327844, + "result": "---\nid: stress_conflict_1779495116612\ndate: 2026-05-23T00:11:56.625Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (11ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (0ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (1ms)\n", + "createdAt": 1779495116625, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/missions/stress_conflict_1779444327825.json b/.astra/tests/stress/.astra/missions/stress_conflict_1779495116612.json similarity index 80% rename from .astra/tests/stress/.astra/missions/stress_conflict_1779444327825.json rename to .astra/tests/stress/.astra/missions/stress_conflict_1779495116612.json index fbc8897..561530c 100644 --- a/.astra/tests/stress/.astra/missions/stress_conflict_1779444327825.json +++ b/.astra/tests/stress/.astra/missions/stress_conflict_1779495116612.json @@ -1,8 +1,8 @@ { - "missionId": "stress_conflict_1779444327825", + "missionId": "stress_conflict_1779495116612", "status": "completed", - "startTime": "2026-05-22T10:05:27.825Z", - "totalElapsedMs": 20, + "startTime": "2026-05-23T00:11:56.613Z", + "totalElapsedMs": 13, "results": { "planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.", "researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", @@ -18,28 +18,28 @@ "to": "planner", "durationMs": 11, "message": "전략 수립 중...", - "ts": "2026-05-22T10:05:27.836Z" + "ts": "2026-05-23T00:11:56.624Z" }, { "from": "planner", "to": "researcher", - "durationMs": 2, + "durationMs": 0, "message": "핵심 정보 수집 및 분석 중...", - "ts": "2026-05-22T10:05:27.838Z" + "ts": "2026-05-23T00:11:56.624Z" }, { "from": "researcher", "to": "writer", - "durationMs": 3, + "durationMs": 1, "message": "최종 리포트 작성 및 편집 중...", - "ts": "2026-05-22T10:05:27.841Z" + "ts": "2026-05-23T00:11:56.625Z" }, { "from": "writer", "to": "completed", - "durationMs": 4, + "durationMs": 1, "message": "미션 완료", - "ts": "2026-05-22T10:05:27.845Z" + "ts": "2026-05-23T00:11:56.626Z" } ], "resilienceMetrics": { diff --git a/media/sidebar.js b/media/sidebar.js index 1308d97..35aa236 100644 --- a/media/sidebar.js +++ b/media/sidebar.js @@ -392,6 +392,9 @@ } segs.push(`메모리 ${s.memory ? 'On' : 'Off'}`); if (s.multiAgent) segs.push(`멀티에이전트`); + if (s.lmStudioError) { + segs.push(`⚠ LM Studio 로드 실패`); + } rbContent.innerHTML = segs.join('·'); if (rbDot) { const on = s.engine && s.engine.online; diff --git a/package.json b/package.json index 24abafc..06124de 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "astra", "displayName": "Astra", "description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.", - "version": "2.2.63", + "version": "2.2.64", "publisher": "g1nation", "license": "MIT", "icon": "assets/icon.png", diff --git a/src/extension.ts b/src/extension.ts index f679531..a3b4fef 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -157,6 +157,7 @@ export async function activate(context: vscode.ExtensionContext) { lifecycle, activity: activityTracker, loadedModels: () => lmStudioClient.listLoadedCached(), + downloadedModels: () => lmStudioClient.listDownloaded(), }); // One-time repair: rewrite any chronicle projects that were saved with the // workspace parent as their `projectRoot` (a side-effect of the old diff --git a/src/lmstudio/client.ts b/src/lmstudio/client.ts index 17bfc24..0a5876b 100644 --- a/src/lmstudio/client.ts +++ b/src/lmstudio/client.ts @@ -7,6 +7,14 @@ export interface ILMStudioClient { listLoaded(): Promise; /** Like listLoaded() but caches the result for `ttlMs` to avoid hammering the SDK. */ listLoadedCached(ttlMs?: number): Promise; + /** + * List every LLM the user has downloaded into LM Studio, regardless of + * whether it is currently loaded. Returns the SDK `modelKey` of each entry — + * the exact identifier `llm.load()` accepts. Use this for the dropdown so + * the list does not depend on LM Studio's JIT setting (REST `/v1/models` + * only returns loaded models when JIT is off). + */ + listDownloaded(): Promise; /** * Resolve a chat-ready handle for an already-loaded (or just-loaded) model. * @@ -117,6 +125,19 @@ export class LMStudioClient implements ILMStudioClient { } } + async listDownloaded(): Promise { + try { + const items: any[] = await this.getSdk().system.listDownloadedModels('llm'); + return items + .map((m) => m?.modelKey ?? null) + .filter((k): k is string => typeof k === 'string' && k.length > 0); + } catch (e: any) { + const msg = e?.message ?? String(e); + logError('Failed to list downloaded LM Studio models.', { error: msg }); + return []; + } + } + async getModelHandle(modelKey: string, options?: { refresh?: boolean }): Promise { try { if (options?.refresh) { diff --git a/src/security.ts b/src/security.ts index 4a3875e..b240489 100644 --- a/src/security.ts +++ b/src/security.ts @@ -98,8 +98,13 @@ function splitTopLevelAnd(command: string): string[] { * `$?` reflects the success of the previous command, so a failed step still * short-circuits the rest — important so e.g. a failed `cd` never lets `git` * run in the wrong directory. + * + * On macOS/Linux this rewrite is actively harmful — `&&` is the native + * POSIX-shell operator and the PowerShell `if ($?) { ... }` shape is a zsh/bash + * syntax error. So skip the rewrite entirely off-Windows. */ function rewriteForPowerShell(command: string): string { + if (process.platform !== 'win32') { return command; } if (!command.includes('&&')) { return command; } const parts = splitTopLevelAnd(command); if (parts.length <= 1) { return command; } diff --git a/src/sidebar/chatHandlers.ts b/src/sidebar/chatHandlers.ts index 45c51c4..a2c0c52 100644 --- a/src/sidebar/chatHandlers.ts +++ b/src/sidebar/chatHandlers.ts @@ -302,6 +302,9 @@ export async function handleChatMessage(provider: SidebarChatProvider, data: any const { target } = pickConfigTarget('g1nation', 'defaultModel'); await vscode.workspace.getConfiguration('g1nation').update('defaultModel', data.value, target); logInfo(`Default model updated to: ${data.value}`, { target }); + // Wipe any persistent LM Studio error segment from the readyBar — the + // next load attempt will repaint it if it also fails. + provider.clearLmStudioError(); provider._lmStudio?.lifecycle.onModelSelected(data.value); return true; } diff --git a/src/sidebarProvider.ts b/src/sidebarProvider.ts index 2734bac..ebcfb01 100644 --- a/src/sidebarProvider.ts +++ b/src/sidebarProvider.ts @@ -60,6 +60,13 @@ export interface SidebarLmStudioDeps { activity: IActivityTracker; /** Returns the list of model identifiers currently loaded in LM Studio (cached). */ loadedModels: () => Promise; + /** + * Returns every model downloaded into LM Studio (modelKey form). Used by the + * dropdown so it does not depend on LM Studio's JIT setting — REST + * `/v1/models` only lists loaded models when JIT is off, which on macOS + * commonly leaves the dropdown with just the fallback `defaultModel`. + */ + downloadedModels: () => Promise; } interface LastVisibleChatSnapshot { @@ -790,9 +797,24 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn }); } + /** Latest LM Studio load/unload error — surfaced as a persistent segment in the readyBar + * until the next successful model selection clears it. The transient chat toast alone + * was getting missed (scrolled away, blended into chat noise). */ + private _lmStudioLastError: string | undefined; + /** Surface LM Studio lifecycle errors (load/unload failures) to the chat UI as a non-fatal toast. */ public postLmStudioError(message: string): void { + this._lmStudioLastError = message; this._view?.webview.postMessage({ type: 'lmStudioError', value: message }); + void this._sendReadyStatus(); + } + + /** Clear the persistent LM Studio error segment. Called when the user picks a new + * model so a stale failure does not haunt the next attempt. */ + public clearLmStudioError(): void { + if (this._lmStudioLastError === undefined) return; + this._lmStudioLastError = undefined; + void this._sendReadyStatus(); } public resolveWebviewView( @@ -1221,6 +1243,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn contextLength: effectiveContextLength, nominalContextLength: config.contextLength, cappedForSmallModel, + lmStudioError: this._lmStudioLastError ?? null, }; } catch (err: any) { logError('Failed to build ready status.', { error: err?.message || String(err) }); @@ -3915,24 +3938,50 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn } else { const engine = resolveEngine(url); // 단일 엔진만 const modelsUrl = buildApiUrl(url, engine, 'models'); - try { - logInfo('Model discovery started.', { engine, modelsUrl, force }); - const res = await fetch(modelsUrl, { signal: AbortSignal.timeout(5000) }); - const rawText = await res.text(); - if (!res.ok) { - logError('Model discovery returned non-OK status.', { engine, modelsUrl, status: res.status, body: summarizeText(rawText, 300) }); - } else { - const data = rawText ? JSON.parse(rawText) as any : {}; - models = engine === 'lmstudio' - ? (data.data || []).map((m: any) => m.id) - : (data.models || []).map((m: any) => m.name); - if (models.length > 0) { - logInfo('Model discovery succeeded.', { engine, count: models.length, modelsPreview: models.slice(0, 5) }); + // LM Studio: prefer the SDK's `system.listDownloadedModels('llm')` over the + // REST `/v1/models` endpoint. REST only returns models that are currently + // loaded when LM Studio's "Just-In-Time Model Loading" setting is off — + // which on macOS leaves the dropdown with zero entries (or one fallback + // entry that may not match any real `modelKey`). The SDK call enumerates + // every downloaded LLM regardless of JIT and returns the exact `modelKey` + // that `llm.load()` accepts. Falls back to REST if the SDK round-trip + // throws (e.g. LM Studio app open but local server not enabled). + if (engine === 'lmstudio' && this._lmStudio) { + try { + logInfo('Model discovery started (SDK).', { engine, force }); + const sdkModels = await this._lmStudio.downloadedModels(); + if (sdkModels.length > 0) { + models = sdkModels; + logInfo('Model discovery succeeded (SDK).', { engine, count: models.length, modelsPreview: models.slice(0, 5) }); + } else { + logInfo('LM Studio SDK returned no downloaded models — falling back to REST /v1/models.', { engine }); } + } catch (e: any) { + logError('LM Studio SDK model discovery failed — falling back to REST.', { engine, error: e?.message || String(e) }); + } + } + + if (models.length === 0) { + try { + logInfo('Model discovery started (REST).', { engine, modelsUrl, force }); + const res = await fetch(modelsUrl, { signal: AbortSignal.timeout(5000) }); + const rawText = await res.text(); + if (!res.ok) { + logError('Model discovery returned non-OK status.', { engine, modelsUrl, status: res.status, body: summarizeText(rawText, 300) }); + } else { + const data = rawText ? JSON.parse(rawText) as any : {}; + models = engine === 'lmstudio' + ? (data.data || []).map((m: any) => m.id) + : (data.models || []).map((m: any) => m.name); + + if (models.length > 0) { + logInfo('Model discovery succeeded (REST).', { engine, count: models.length, modelsPreview: models.slice(0, 5) }); + } + } + } catch (e: any) { + logError('Model discovery failed (REST).', { engine, modelsUrl, error: e?.message || String(e) }); } - } catch (e: any) { - logError('Model discovery failed.', { engine, modelsUrl, error: e?.message || String(e) }); } online = models.length > 0; diff --git a/src/utils.ts b/src/utils.ts index bae6060..15d297d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -219,15 +219,15 @@ When the user asks to run, start, launch, boot, or serve something (실행/구 - NEVER invent a script name, port number, or environment variable. If you have not seen it in a file THIS session, do not state it as fact. - If you do not know the exact start command, FIRST read the project's package.json with , then emit with the real script name. - runs in a real terminal. If the target folder differs from the workspace, cd into its absolute path first. -- The terminal is Windows PowerShell. Chain steps with ";" — NEVER "&&" (it is a syntax error in PowerShell 5.1). Example: cd 'C:\proj'; git add .; git commit -m 'msg'; git push +- The terminal type, shell, path style, and command-chaining operator are NOT fixed — they depend on the host OS. ALWAYS follow the [ENVIRONMENT] block at the bottom of this prompt; that block is the single source of truth. Never assume Windows / PowerShell / drive letters, never assume macOS / zsh / forward slashes — read the block first. - After acting, reply with ONE short line: what you started and where. No tutorial, no follow-up checklist. -Worked example — user says: "E:\Wiki\Datacollect 서버 실행해줘" +Worked example pattern (adapt path style and chaining operator to whatever the [ENVIRONMENT] block says — the literal paths below are illustrative only): Step 1 (only when the start script is unknown): - + Step 2 (after the real scripts are known — pick the actual one, never a guessed name): -cd 'E:\Wiki\Datacollect'; npm run start-full -Then reply: "Datacollect 서버를 start-full 스크립트로 터미널에서 실행했습니다." +cd '' npm run +Then reply with one short line stating what was started and where. [STRICT GLOBAL RULES] 1. [NO EMOJIS - ABSOLUTE RULE] NEVER use ANY emojis, emoticons, Unicode pictorial symbols (including but not limited to emoji, kaomoji, Unicode icons), or decorative symbols anywhere in your response. NO EXCEPTIONS. Use plain text dashes (-) or asterisks (*) for bullets. Use plain markdown ## for headers. This rule overrides ALL other formatting instructions. @@ -387,11 +387,55 @@ When you do ask: it is ONE plain sentence on its own line. NEVER put it under a 2. File paths are relative to the workspace or absolute under /Volumes/Data/project/Antigravity. 3. When the user says "분석해줘", "봐줘", "확인해줘", "리뷰해줘" with a path — that IS the confirmation. Access the path immediately and run the full analysis in one continuous response. Do not pause to ask "진행할까요?" or "시작할까요?".`; +function getEnvironmentBlock(): string { + const platform = process.platform; + const homeDir = os.homedir(); + const cwd = process.cwd(); + const wsFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + + let osLabel: string; + let shellLabel: string; + let chainRule: string; + let pathRule: string; + let example: string; + + if (platform === 'win32') { + osLabel = 'Windows'; + shellLabel = process.env.ComSpec?.toLowerCase().includes('powershell') ? 'PowerShell' : 'PowerShell / cmd'; + chainRule = `Chain steps with ";" — NEVER "&&" (it is a hard parser error in PowerShell 5.1 and the whole command fails). Use "if ($?) { ... }" for short-circuit semantics when the next step must depend on the previous step succeeding.`; + pathRule = `Paths use drive letters and backslashes: C:\\Users\\name\\project, E:\\Wiki\\Datacollect. Quote with single quotes when the path contains spaces.`; + example = `cd 'C:\\proj'; git add .; git commit -m 'msg'; git push`; + } else if (platform === 'darwin') { + osLabel = 'macOS'; + shellLabel = process.env.SHELL?.split('/').pop() || 'zsh'; + chainRule = `Chain steps with "&&" (POSIX short-circuit). Use ";" only when steps are independent. NEVER emit PowerShell-only syntax like "if ($?) { ... }" — that is a zsh/bash parser error.`; + pathRule = `Paths are POSIX with forward slashes: /Volumes/Data/project/..., /Users//.... NEVER use Windows drive letters like C:\\ or E:\\ — those are not valid paths on macOS and "cd C:/..." will fail with "no such file or directory".`; + example = `cd '/Volumes/Data/project/Antigravity/Wiki' && git pull`; + } else { + osLabel = `Linux (${platform})`; + shellLabel = process.env.SHELL?.split('/').pop() || 'bash'; + chainRule = `Chain steps with "&&" (POSIX short-circuit). Use ";" only when steps are independent. NEVER emit PowerShell-only syntax like "if ($?) { ... }" — that is a bash/zsh parser error.`; + pathRule = `Paths are POSIX with forward slashes: /home//..., /opt/.... NEVER use Windows drive letters like C:\\ or E:\\.`; + example = `cd /home/user/proj && git pull`; + } + + return `\n\n[ENVIRONMENT — authoritative, overrides any conflicting example elsewhere in this prompt] +- OS: ${osLabel} (process.platform=${platform}) +- Shell: ${shellLabel} +- Home: ${homeDir} +- Workspace: ${wsFolder ?? '(no workspace open)'} +- CWD at extension start: ${cwd} +- Terminal chaining: ${chainRule} +- Path style: ${pathRule} +- Canonical example for this OS: ${example}`; +} + export function getSystemPrompt(): string { const now = new Date(); const dateTimeStr = now.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'long', hour: '2-digit', minute: '2-digit' }); const isoDate = now.toISOString().split('T')[0]; - const base = `${BASE_SYSTEM_PROMPT}\n\n[CURRENT DATE/TIME]\nToday: ${isoDate} (${dateTimeStr})\nUse this date as the absolute reference for any date-related calculations (e.g., "this week", "today", "yesterday").\n\n[출력 위생 규칙 — 반드시 준수]\n- 자연스러운 한국어로 작성하고, 한 단어 안에 한글과 영문 알파벳을 섞지 마시오 ("결ently", "인orp" 같은 깨진 합성 표기 절대 금지).\n- 외래어·기술 용어는 완전한 한글 표기 또는 완전한 영문 단어 중 하나로 일관되게 쓰시오.\n- 내부 검증·체크 로그(Consistency/Completeness/Accuracy 등) 블록을 사용자 출력에 포함하지 마시오.`; + const envBlock = getEnvironmentBlock(); + const base = `${BASE_SYSTEM_PROMPT}\n\n[CURRENT DATE/TIME]\nToday: ${isoDate} (${dateTimeStr})\nUse this date as the absolute reference for any date-related calculations (e.g., "this week", "today", "yesterday").\n\n[출력 위생 규칙 — 반드시 준수]\n- 자연스러운 한국어로 작성하고, 한 단어 안에 한글과 영문 알파벳을 섞지 마시오 ("결ently", "인orp" 같은 깨진 합성 표기 절대 금지).\n- 외래어·기술 용어는 완전한 한글 표기 또는 완전한 영문 단어 중 하나로 일관되게 쓰시오.\n- 내부 검증·체크 로그(Consistency/Completeness/Accuracy 등) 블록을 사용자 출력에 포함하지 마시오.${envBlock}`; // Self-Reflector Phase A — 사용자 설정이 켜져 있으면 답변 끝에 자기검증 // 블록을 강제하는 룰을 prepend. require로 동적 로드해 순환 import 회피. try { diff --git a/tests/lmStudioLifecycle.test.ts b/tests/lmStudioLifecycle.test.ts index c1c2d73..e41a220 100644 --- a/tests/lmStudioLifecycle.test.ts +++ b/tests/lmStudioLifecycle.test.ts @@ -78,6 +78,10 @@ class FakeLMStudioClient implements ILMStudioClient { return []; } + async listDownloaded(): Promise { + return []; + } + async getModelHandle(_modelKey: string): Promise { return {}; } diff --git a/tests/lmStudioStreamer.test.ts b/tests/lmStudioStreamer.test.ts index 375e1f2..f2acfbd 100644 --- a/tests/lmStudioStreamer.test.ts +++ b/tests/lmStudioStreamer.test.ts @@ -79,6 +79,7 @@ class FakeClient implements ILMStudioClient { async unload(_: string): Promise { /* noop */ } async listLoaded(): Promise { return []; } async listLoadedCached(): Promise { return []; } + async listDownloaded(): Promise { return []; } async isReachable(): Promise { return true; } async getModelHandle(modelKey: string): Promise {