Update project files

This commit is contained in:
2026-05-22 15:00:14 +09:00
parent 132d130ff1
commit 8016ef18fa
29 changed files with 1353 additions and 804 deletions
+27 -4
View File
@@ -408,8 +408,31 @@ function move(role,x,y){
ch.style.left=x+'px';
ch.style.top=y+'px';
}
setInterval(()=>{Object.keys(chars).forEach(k=>{const a=anim[k];if(a.mode==='walk'){a.frame=(a.frame+1)%5;setSprite(k,'walk',a.frame,a.dir)}else if(a.mode==='work'){a.frame=(a.frame+1)%4;setSprite(k,'work',a.frame)} });},286)
setInterval(()=>{Object.keys(chars).forEach(k=>{const a=anim[k];if(a.mode==='sit'){a.frame=(a.frame+1)%2;setSprite(k,'sit',a.frame)} });},700)
// ── Managed intervals (pause while the office view is hidden) ──
// The pixel-office runs several animation/roam/banter intervals. While the
// webview tab is not visible they do invisible work and keep timers hot —
// wasteful. _managedInterval registers each one; a visibilitychange handler
// pauses them all when the document is hidden and resumes them when shown.
// Behavior while visible is unchanged (same callbacks, same periods).
const _managedIntervals=[];
function _managedInterval(fn,ms){
const rec={fn:fn,ms:ms,id:null};
rec.id=setInterval(fn,ms);
_managedIntervals.push(rec);
return rec;
}
function _pauseManagedIntervals(){
for(const rec of _managedIntervals){ if(rec.id!==null){ clearInterval(rec.id); rec.id=null; } }
}
function _resumeManagedIntervals(){
for(const rec of _managedIntervals){ if(rec.id===null){ rec.id=setInterval(rec.fn,rec.ms); } }
}
document.addEventListener('visibilitychange',()=>{
if(document.hidden) _pauseManagedIntervals();
else _resumeManagedIntervals();
});
_managedInterval(()=>{Object.keys(chars).forEach(k=>{const a=anim[k];if(a.mode==='walk'){a.frame=(a.frame+1)%5;setSprite(k,'walk',a.frame,a.dir)}else if(a.mode==='work'){a.frame=(a.frame+1)%4;setSprite(k,'work',a.frame)} });},286)
_managedInterval(()=>{Object.keys(chars).forEach(k=>{const a=anim[k];if(a.mode==='sit'){a.frame=(a.frame+1)%2;setSprite(k,'sit',a.frame)} });},700)
// ── 책상 회피 path planner ──
// walkPath의 각 leg를 직선이 아닌 *책상을 우회하는* L자 또는 corridor 경로로
// 펴서 캐릭터가 책상을 가로지르지 않게. 책상이 회전됐을 때를 대비해 padding
@@ -498,7 +521,7 @@ function sendHome(role,mode='sit'){
if(Math.abs(cx-hx)<1&&Math.abs(cy-hy)<1){setSprite(role,mode);return;}
walkPath(role,[st.dock,[hx,hy]],()=>setSprite(role,mode));
}
setInterval(()=>{
_managedInterval(()=>{
if(!['idle','done'].includes(_prevStatus || 'idle')) return;
const free=Object.keys(chars).filter(k=>anim[k]?.mode==='sit'&&!chars[k].classList.contains('active'));
if(!free.length)return;
@@ -725,7 +748,7 @@ function _innerThoughtTick(){
const text = _innerThoughtFor(st.agentKey, anim[role].mode);
if(text) _bubbleFromLog(role, text);
}
setInterval(_innerThoughtTick, 7500);
_managedInterval(_innerThoughtTick, 7500);
// ── Webtoon-style 티키타카 banter (refactor: pipeline-aware) ──
// 각 phase 에 *시퀀스화된 대화 script* 가 있어 phase 진입 시 한 줄씩 시간 차로 emit.
+33 -1
View File
@@ -584,7 +584,35 @@ interface FileScan {
documentProject: string | undefined;
}
/**
* mtime-keyed scan cache. The previous implementation re-read (and re-classified)
* every brain file from disk on every chat message. We now reuse a parsed
* `FileScan` while the file's mtime is unchanged — re-reading only when the file
* actually changes. This mirrors the mtime-keyed caching style of
* `retrieval/brainIndex.ts` (whose `getBrainTokenIndex` caches tokens the same
* way) while keeping the scan output byte-identical, so scoring is unaffected.
*/
interface ScanCacheEntry {
mtimeMs: number;
size: number;
scan: FileScan;
}
const _scanCache = new Map<string, ScanCacheEntry>();
function scanFile(file: string, brainRoot: string): FileScan {
let mtimeMs = 0;
let size = 0;
try {
const stat = fs.statSync(file);
mtimeMs = stat.mtimeMs;
size = stat.size;
const cached = _scanCache.get(file);
if (cached && cached.mtimeMs === mtimeMs && cached.size === size) {
return cached.scan;
}
} catch {
// stat failed — fall through and attempt a fresh read (which will also fail safely)
}
const relative = path.relative(brainRoot, file);
const title = path.basename(file, path.extname(file));
let content = '';
@@ -598,7 +626,11 @@ function scanFile(file: string, brainRoot: string): FileScan {
const lower = content.toLowerCase();
const documentProject = inferDocumentProject(relative, lower);
const titleWithPath = `${relative.replace(/[\\/]/g, ' ')} ${title}`;
return { file, relative, title, titleWithPath, content, lower, sourceType, knowledgeRole, documentProject };
const scan: FileScan = { file, relative, title, titleWithPath, content, lower, sourceType, knowledgeRole, documentProject };
if (mtimeMs > 0) {
_scanCache.set(file, { mtimeMs, size, scan });
}
return scan;
}
function scoreScan(scan: FileScan, terms: string[], intent: SecondBrainQueryIntent, targetProject?: string): SecondBrainTraceDocument {