feat: v2.2.92 → v2.2.158 — god-file 분해 + Stocks feature + 대화 연속성
R56–R59: agent.ts 2731→1529줄 god-file 분해 (25 modules) · attrParsers + LLM 메서드 8개 (callNonStreaming, streamChatOnce 등) · executeActions 415줄 → 8 handler 그룹 (file/run/list/brain/calendar/sheets/tasks) · handlePrompt 1100줄 → 7 phase 모듈 (system prompt + budget + autoContinue 등) R50–R55: extension.ts 1145→349줄 (telegram/settings/provider commands 분리) Stocks feature 신규: /stocks slash command (v2.2.152~158) · .astra/stocks.json 저장소 + Yahoo Finance 현재가 갱신 · 8 키워드 필터 (ROE/성장성/유동성/수익성/영업효율/기술력/안정성/PBR) · Naver 시가총액 페이지 JSON API (m.stock.naver.com) 발굴 · LLM Top 5 매력도 분석 + Telegram 자동 보고서 · KST 09:00/15:00 watcher 자동 모니터링 대화 연속성 (v2.2.150~157): · [PRIOR TURN CONCLUSION] block 으로 직전 결론 anchor · thin follow-up 분류 → boilerplate 헤더 suppression · slash 명령 결과 chatHistory mirror (capture wrapper) · echo/parrot 금지 system prompt rule 기타: /stocks 슬래시 자동완성 dropdown UI, Naver JSON API 전환 (cheerio 제거) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -390,6 +390,14 @@
|
||||
</div>
|
||||
<small class="hint">Chunked 가 답변을 쪼갤 수 있는 최대 섹션 수. 실제 LLM 호출 = `2 + N` 회 (outline 1 + section N + polish 1). 기본 3 (총 5회). 빨리 받고 싶으면 2 (총 4회), 답변을 더 세분화하려면 5 (총 7회).</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="advPolishPersona">Polish persona 커스텀 (polishPersonaOverride)</label>
|
||||
<div class="input-group" style="flex-direction:column; align-items:stretch;">
|
||||
<textarea id="advPolishPersona" rows="6" placeholder="비워두면 기본 polish persona 사용. 내용을 입력하면 그 텍스트가 그대로 polish 단계의 system prompt 로 들어갑니다. 예: '당신은 한국 법률 문서 톤의 편집자입니다. 격식체로 작성하고...'"></textarea>
|
||||
<button data-save="advanced.polishPersonaOverride" style="margin-top: 6px; align-self: flex-start;">저장</button>
|
||||
</div>
|
||||
<small class="hint">답변의 최종 다듬기 단계(polish) 톤·구조를 직접 정의합니다. 예: 격식체/반말/법률·마케팅 도메인 톤. 빈 값이면 기본 persona (한 줄 요약 + subheading + 5-check) 사용.</small>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
const advChatTemp = $('advChatTemp');
|
||||
const advChunkedSwitch = $('advChunkedSwitch');
|
||||
const advChunkedMax = $('advChunkedMax');
|
||||
const advPolishPersona = $('advPolishPersona');
|
||||
|
||||
// ---- Google (Calendar + Sheets) ----
|
||||
const gClientId = $('gClientId');
|
||||
@@ -251,6 +252,10 @@
|
||||
vscode.postMessage({ type: 'advanced.update', chunkedMaxSections: Number(advChunkedMax.value) })
|
||||
);
|
||||
|
||||
document.querySelector('[data-save="advanced.polishPersonaOverride"]').addEventListener('click', () =>
|
||||
vscode.postMessage({ type: 'advanced.update', polishPersonaOverride: String(advPolishPersona.value || '') })
|
||||
);
|
||||
|
||||
// ---- Header ----
|
||||
$('openVscodeSettings').addEventListener('click', () =>
|
||||
vscode.postMessage({ type: 'openVscodeSettings' })
|
||||
@@ -409,6 +414,7 @@
|
||||
setIfNotFocused(advChatTemp, adv.chatTemperature);
|
||||
setIfNotFocused(advChunkedSwitch, adv.chunkedSwitchTokens);
|
||||
setIfNotFocused(advChunkedMax, adv.chunkedMaxSections);
|
||||
setIfNotFocused(advPolishPersona, adv.polishPersonaOverride);
|
||||
|
||||
// ---- Google (Calendar + Sheets) ----
|
||||
const g = state.google;
|
||||
|
||||
@@ -2136,3 +2136,36 @@
|
||||
}
|
||||
.records-line .rl-latest { color: var(--border-bright); overflow: hidden; text-overflow: ellipsis; }
|
||||
.records-line .hdr-dropdown { flex-shrink: 0; }
|
||||
|
||||
/* Slash 명령 자동완성 dropdown — input 바로 위에 floating. */
|
||||
.slash-suggest {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
margin-bottom: 6px;
|
||||
background: var(--bg-secondary, #2a2a2a);
|
||||
border: 1px solid var(--border, #444);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
font-size: 13px;
|
||||
}
|
||||
.slash-suggest .ss-item {
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: baseline;
|
||||
border-bottom: 1px solid var(--border-faint, #333);
|
||||
}
|
||||
.slash-suggest .ss-item:last-child { border-bottom: none; }
|
||||
.slash-suggest .ss-item.active { background: var(--accent-faint, #094771); }
|
||||
.slash-suggest .ss-item:hover { background: var(--bg-hover, #353535); }
|
||||
.slash-suggest .ss-name { font-weight: 600; color: var(--accent, #4ec9b0); flex-shrink: 0; }
|
||||
.slash-suggest .ss-desc { color: var(--text-dim, #999); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.slash-suggest .ss-empty { padding: 8px 12px; color: var(--text-dim, #999); font-style: italic; }
|
||||
/* input-box 가 position:relative 여야 absolute 가 제대로 anchored. */
|
||||
.input-box { position: relative; }
|
||||
|
||||
@@ -510,6 +510,7 @@
|
||||
</div>
|
||||
<div class="input-box">
|
||||
<div id="attachPreview" class="attachment-preview"></div>
|
||||
<div id="slashSuggest" class="slash-suggest" style="display:none;"></div>
|
||||
<textarea id="input" rows="1" placeholder="Astra에게 무엇이든 물어보세요..."></textarea>
|
||||
<div class="input-footer">
|
||||
<div class="footer-left">
|
||||
|
||||
+130
-1
@@ -788,6 +788,10 @@
|
||||
window.addEventListener('message', e => {
|
||||
const msg = e.data;
|
||||
switch(msg.type) {
|
||||
case 'slashCommandList':
|
||||
// Extension 측에서 listSlashCommands() 결과 한 번 전달. webview ready 직후.
|
||||
setSlashCommands(msg.commands || []);
|
||||
break;
|
||||
case 'addMessage':
|
||||
addMsg(msg.value, msg.role, msg.rationale);
|
||||
// Update state for non-streamed messages
|
||||
@@ -1722,6 +1726,125 @@
|
||||
// Draft State: 내용이 있으면 cancelBtn 표시
|
||||
setDraftActive(input.value.trim().length > 0 || pendingFiles.length > 0);
|
||||
bumpActivity();
|
||||
// Slash 자동완성 dropdown — 첫 단어가 `/` 면 후보 표시.
|
||||
updateSlashSuggest();
|
||||
});
|
||||
|
||||
// ── Slash command autocomplete ──────────────────────────────────────
|
||||
// 등록된 slash 명령 (extension 측 listSlashCommands() 결과). webview ready
|
||||
// 시 한 번 전달받아 캐싱.
|
||||
let _slashCommands = []; // [{ name, description }]
|
||||
let _slashActiveIdx = -1;
|
||||
const slashSuggestEl = document.getElementById('slashSuggest');
|
||||
|
||||
function setSlashCommands(cmds) {
|
||||
_slashCommands = Array.isArray(cmds) ? cmds.slice().sort((a, b) => a.name.localeCompare(b.name)) : [];
|
||||
}
|
||||
|
||||
function hideSlashSuggest() {
|
||||
if (slashSuggestEl) {
|
||||
slashSuggestEl.style.display = 'none';
|
||||
slashSuggestEl.innerHTML = '';
|
||||
}
|
||||
_slashActiveIdx = -1;
|
||||
}
|
||||
|
||||
function getCurrentSlashHead() {
|
||||
// input 의 첫 단어가 `/` 로 시작할 때만 자동완성 활성. arg 입력 중에는 닫음.
|
||||
const v = input.value;
|
||||
const trimmed = v.trimStart();
|
||||
if (!trimmed.startsWith('/')) return null;
|
||||
const spaceIdx = trimmed.indexOf(' ');
|
||||
// 공백이 있으면 사용자가 이미 명령 입력 끝내고 arg 치는 중 — dropdown 닫음.
|
||||
if (spaceIdx !== -1) return null;
|
||||
// 커서가 첫 단어 안에 있는지 확인 (입력 중간 편집 시 false-positive 방지).
|
||||
const caret = input.selectionStart ?? v.length;
|
||||
const leading = v.length - v.trimStart().length;
|
||||
if (caret > leading + trimmed.length) return null;
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
|
||||
function renderSlashSuggest(matched) {
|
||||
if (!slashSuggestEl) return;
|
||||
if (matched.length === 0) {
|
||||
slashSuggestEl.innerHTML = '<div class="ss-empty">일치하는 명령 없음</div>';
|
||||
} else {
|
||||
slashSuggestEl.innerHTML = matched.map((c, i) => {
|
||||
const cls = i === _slashActiveIdx ? 'ss-item active' : 'ss-item';
|
||||
const desc = (c.description || '').replace(/[<>&]/g, ch => ({'<':'<','>':'>','&':'&'}[ch]));
|
||||
return `<div class="${cls}" data-idx="${i}" data-name="${c.name}">` +
|
||||
`<span class="ss-name">${c.name}</span>` +
|
||||
`<span class="ss-desc">${desc}</span>` +
|
||||
`</div>`;
|
||||
}).join('');
|
||||
// 클릭으로 선택.
|
||||
slashSuggestEl.querySelectorAll('.ss-item').forEach(el => {
|
||||
el.addEventListener('mousedown', e => {
|
||||
e.preventDefault(); // textarea blur 막음
|
||||
applySlashSuggest(el.dataset.name);
|
||||
});
|
||||
});
|
||||
}
|
||||
slashSuggestEl.style.display = 'block';
|
||||
}
|
||||
|
||||
function updateSlashSuggest() {
|
||||
const head = getCurrentSlashHead();
|
||||
if (head === null) { hideSlashSuggest(); return; }
|
||||
const matched = _slashCommands.filter(c => c.name.toLowerCase().startsWith(head));
|
||||
if (_slashActiveIdx >= matched.length) _slashActiveIdx = matched.length - 1;
|
||||
if (_slashActiveIdx < 0 && matched.length > 0) _slashActiveIdx = 0;
|
||||
renderSlashSuggest(matched);
|
||||
}
|
||||
|
||||
function applySlashSuggest(cmdName) {
|
||||
// input 의 첫 단어를 cmdName 으로 치환 + 공백 추가 (arg 입력 시작 친화적).
|
||||
const v = input.value;
|
||||
const leading = v.length - v.trimStart().length;
|
||||
const head = v.trimStart().split(/\s+/, 1)[0];
|
||||
input.value = ' '.repeat(leading) + cmdName + ' ' + v.slice(leading + head.length).trimStart();
|
||||
// textarea 높이 갱신 + 커서 명령 끝으로 이동.
|
||||
input.style.height = 'auto';
|
||||
input.style.height = input.scrollHeight + 'px';
|
||||
const newCaret = leading + cmdName.length + 1;
|
||||
input.setSelectionRange(newCaret, newCaret);
|
||||
hideSlashSuggest();
|
||||
input.focus();
|
||||
setDraftActive(true);
|
||||
}
|
||||
|
||||
// 키보드 네비게이션 — dropdown 표시 중일 때만 적용.
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (!slashSuggestEl || slashSuggestEl.style.display === 'none') return;
|
||||
const items = slashSuggestEl.querySelectorAll('.ss-item');
|
||||
if (items.length === 0) return;
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
_slashActiveIdx = (_slashActiveIdx + 1) % items.length;
|
||||
updateSlashSuggest();
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
_slashActiveIdx = (_slashActiveIdx - 1 + items.length) % items.length;
|
||||
updateSlashSuggest();
|
||||
} else if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
if (_slashActiveIdx >= 0 && _slashActiveIdx < items.length) {
|
||||
applySlashSuggest(items[_slashActiveIdx].dataset.name);
|
||||
}
|
||||
} else if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
if (_slashActiveIdx >= 0 && _slashActiveIdx < items.length) {
|
||||
applySlashSuggest(items[_slashActiveIdx].dataset.name);
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
hideSlashSuggest();
|
||||
}
|
||||
});
|
||||
|
||||
// input blur 시 살짝 delay 두고 닫음 (dropdown mousedown 먼저 처리되게).
|
||||
input.addEventListener('blur', () => {
|
||||
setTimeout(() => hideSlashSuggest(), 150);
|
||||
});
|
||||
|
||||
cancelBtn.onclick = () => clearDraft();
|
||||
@@ -3137,7 +3260,13 @@
|
||||
discardBtn.textContent = '버리기';
|
||||
discardBtn.title = '이 작업을 더 이상 이어가지 않습니다. 목록에서만 빠지고 기존 산출물 파일은 그대로 남습니다.';
|
||||
discardBtn.onclick = () => {
|
||||
if (!confirm('이 미완 작업을 목록에서 버릴까요? 이미 만들어진 산출물 파일은 사라지지 않습니다.')) return;
|
||||
// VS Code webview iframe 에서 `confirm()` 은 보안상 차단되어 false
|
||||
// 즉시 반환됨 (사용자 화면엔 다이얼로그 안 뜸) → postMessage 가
|
||||
// 영영 호출 안 되는 사고가 있었음. 산출물 파일은 안 지우므로
|
||||
// 별도 확인 없이 바로 처리 — 사용자가 실수로 눌러도 ad-hoc 복구 가능.
|
||||
// 작업 자체의 두 번 클릭 방지로 버튼은 즉시 disabled 처리.
|
||||
discardBtn.disabled = true;
|
||||
discardBtn.textContent = '버리는 중...';
|
||||
vscode.postMessage({ type: 'discardResumableSession', timestamp: it.timestamp });
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user