import {
loadSettingsFromFile,
saveSettingsToFile,
escapeHtml,
DEFAULTS,
SETTINGS_KEY,
} from '../js/shared.js';
/** 標準フォルダのカテゴリ名と既定説明(バックエンドと一致) */
const STANDARD_FOLDER_CATEGORIES = [
['汎用ルール', 'エディタや AI エージェント用のルールを格納します。'],
['汎用スキル', 'AI エージェント用のスキル(SKILL.md など)を格納します。'],
['汎用ツール', 'MCP ツール定義やスクリプト(ソースコード)を格納します。監視対象の拡張子は設定(watch_extensions)で指定し、**MCP からも変更可能**にします。'],
['汎用ナレッジ', '調べものや参照用のナレッジを格納します。'],
];
const STANDARD_CATEGORY_NAMES = new Set(STANDARD_FOLDER_CATEGORIES.map(([name]) => name));
class SettingsPanel extends HTMLElement {
constructor() {
super();
this._initialized = false;
this._monitorPaths = [];
this._removeFromIndexPaths = new Set();
this._editingMonitorPathIndex = null;
}
connectedCallback() {
if (this._initialized) return;
this._initialized = true;
this.className = 'panel panel-settings';
this.id = 'panel-settings';
this.innerHTML = `
<h2 style="font-family:Outfit; margin-bottom:12px;">設定</h2>
<div class="settings-form">
<fieldset class="settings-fieldset">
<legend>検索</legend>
<div class="setting-row">
<label for="setting-min-score">スコア足切り (0〜1)</label>
<input type="number" id="setting-min-score" min="0" max="1" step="0.05" value="0.3" title="この値未満の類似度の結果は表示しません">
</div>
<div class="setting-row">
<label for="setting-limit">取得件数</label>
<input type="number" id="setting-limit" min="1" max="100" value="5" title="検索で返す最大件数">
</div>
</fieldset>
<fieldset class="settings-fieldset">
<legend>ログイン時自動起動</legend>
<div class="setting-row setting-row-toggle">
<label for="setting-run-on-login">有効にする</label>
<label class="setting-toggle-wrap" aria-hidden="true">
<input type="checkbox" id="setting-run-on-login" class="setting-toggle" role="switch" aria-describedby="setting-run-on-login-desc">
<span class="setting-toggle-slider"></span>
</label>
<span id="setting-run-on-login-desc" class="setting-hint">OS にサインインしたときに TelosDB を自動で起動します</span>
</div>
</fieldset>
<fieldset class="settings-fieldset" id="fieldset-standard-folders">
<legend id="label-standard-folders">標準フォルダ</legend>
<div class="setting-row setting-row-toggle">
<label for="setting-standard-folders-enabled">標準フォルダを有効にする</label>
<label class="setting-toggle-wrap" aria-hidden="true">
<input type="checkbox" id="setting-standard-folders-enabled" class="setting-toggle" role="switch" aria-describedby="setting-standard-folders-desc">
<span class="setting-toggle-slider"></span>
</label>
<span id="setting-standard-folders-desc" class="setting-hint">汎用ルール・汎用スキル・汎用ツール・汎用ナレッジの4フォルダを監視に追加します(パスは編集可能)</span>
</div>
<div class="setting-monitor-paths-wrap" id="settings-standard-folders-wrap" aria-labelledby="label-standard-folders">
<table class="setting-monitor-paths-table" aria-label="標準フォルダ一覧">
<thead>
<tr>
<th>パス</th>
<th>カテゴリ</th>
<th>説明</th>
<th>操作</th>
</tr>
</thead>
<tbody id="settings-standard-paths-tbody"></tbody>
</table>
</div>
</fieldset>
<fieldset class="settings-fieldset" id="fieldset-monitor-paths">
<legend id="label-monitor-paths">監視フォルダ</legend>
<div id="settings-monitor-paths-list" class="setting-monitor-paths-wrap" aria-labelledby="label-monitor-paths">
<table class="setting-monitor-paths-table" aria-label="監視フォルダ一覧">
<thead>
<tr>
<th>パス</th>
<th>カテゴリ</th>
<th>説明</th>
<th>操作</th>
</tr>
</thead>
<tbody id="settings-custom-paths-tbody"></tbody>
</table>
<button type="button" id="settings-monitor-path-add" class="secondary-btn">追加</button>
</div>
<div id="settings-monitor-path-modal" class="settings-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="settings-monitor-path-modal-title" hidden>
<div class="settings-modal-content">
<h3 id="settings-monitor-path-modal-title" class="settings-modal-title">フォルダを編集</h3>
<div class="setting-row">
<label for="modal-monitor-path">パス</label>
<input type="text" id="modal-monitor-path" class="setting-monitor-path-input" placeholder="例: C:\\Documents\\Notes" aria-label="モニター先フォルダパス">
</div>
<div class="setting-row">
<label for="modal-monitor-category">カテゴリ</label>
<input type="text" id="modal-monitor-category" class="setting-monitor-category-input" placeholder="カテゴリ名" aria-label="カテゴリ名">
</div>
<div class="setting-row">
<label for="modal-monitor-description">説明 (Markdown)</label>
<textarea id="modal-monitor-description" class="setting-monitor-description-input" placeholder="説明 (Markdown)" aria-label="説明" rows="4"></textarea>
</div>
<div class="settings-modal-actions">
<button type="button" id="settings-monitor-path-modal-cancel" class="secondary-btn">キャンセル</button>
<button type="button" id="settings-monitor-path-modal-save" class="secondary-btn">保存</button>
</div>
</div>
</div>
<div class="setting-row setting-row-watch-extensions">
<label for="setting-watch-extensions">取込対象拡張子</label>
<div class="setting-watch-extensions-cell">
<input type="text" id="setting-watch-extensions" class="setting-watch-extensions" value="txt, md, json, html, css, js, mjs, ts, rs" placeholder="例: txt, md, json" aria-describedby="setting-watch-extensions-desc" title="カンマ区切り。監視フォルダ内のこの拡張子のファイルのみ取り込みます">
<span id="setting-watch-extensions-desc" class="setting-hint setting-hint-block">カンマ区切り(例: txt, md, json)。空の場合は既定の拡張子を使用</span>
</div>
</div>
</fieldset>
<div class="setting-actions">
<button type="button" id="settings-refresh-btn" class="secondary-btn" title="MCP 等で変更された設定をバックエンドから再取得">更新</button>
<button type="button" id="settings-save-btn" class="secondary-btn">保存</button>
<button type="button" id="settings-backup-btn" class="secondary-btn">設定をバックアップ</button>
<button type="button" id="settings-restore-btn" class="secondary-btn">設定を復元</button>
<button type="button" id="settings-open-data-folder-btn" class="secondary-btn">データフォルダを開く</button>
<span id="settings-feedback" class="settings-feedback" aria-live="polite"></span>
</div>
</div>
`;
const renderOneRow = (p, i) => {
const path = (p && p.path) || '';
const pathDisplay = path || '(保存後に確定)';
const category = (p && p.category) || '';
const desc = (p && p.description) || '';
const descPreview = desc.length > 40 ? desc.slice(0, 40) + '…' : (desc || '—');
const tr = document.createElement('tr');
tr.innerHTML = `
<td class="setting-monitor-path-cell">${escapeHtml(pathDisplay)}</td>
<td class="setting-monitor-category-cell">${escapeHtml(category)}</td>
<td class="setting-monitor-desc-preview-cell">${escapeHtml(descPreview)}</td>
<td class="setting-monitor-actions-cell">
<button type="button" class="setting-monitor-open secondary-btn" data-index="${i}" data-path="${escapeHtml(path)}" data-category="${escapeHtml(category)}" title="フォルダをエクスプローラで開く">開く</button>
<button type="button" class="setting-monitor-edit secondary-btn" data-index="${i}" title="編集">編集</button>
<button type="button" class="setting-monitor-remove secondary-btn" data-index="${i}" title="削除">削除</button>
</td>
`;
return tr;
};
const renderMonitorPathsTable = () => {
const standardTbody = this.querySelector('#settings-standard-paths-tbody');
const customTbody = this.querySelector('#settings-custom-paths-tbody');
const standardWrap = this.querySelector('#settings-standard-folders-wrap');
const standardFoldersEl = this.querySelector('#setting-standard-folders-enabled');
const arr = Array.isArray(this._monitorPaths) ? this._monitorPaths : [];
const enabled = standardFoldersEl ? standardFoldersEl.checked : false;
if (standardWrap) standardWrap.style.display = enabled ? '' : 'none';
if (standardTbody) {
standardTbody.innerHTML = '';
arr.forEach((p, i) => {
if (STANDARD_CATEGORY_NAMES.has((p && p.category) || '')) standardTbody.appendChild(renderOneRow(p, i));
});
}
if (customTbody) {
customTbody.innerHTML = '';
arr.forEach((p, i) => {
if (!STANDARD_CATEGORY_NAMES.has((p && p.category) || '')) customTbody.appendChild(renderOneRow(p, i));
});
}
};
const renderMonitorPathsList = (paths) => {
const arr = Array.isArray(paths) ? paths : [];
this._monitorPaths = arr.map((p) => {
if (typeof p === 'object' && p !== null) {
return { path: String(p.path || ''), category: String(p.category || ''), description: String(p.description || '') };
}
return { path: String(p || ''), category: '', description: '' };
});
renderMonitorPathsTable();
};
const loadSettingsIntoForm = (settingsOverrides) => {
try {
const s = settingsOverrides != null
? { ...DEFAULTS, ...settingsOverrides }
: (() => {
const raw = localStorage.getItem(SETTINGS_KEY);
return raw ? { ...DEFAULTS, ...JSON.parse(raw) } : DEFAULTS;
})();
const minScoreEl = this.querySelector('#setting-min-score');
const limitEl = this.querySelector('#setting-limit');
const runOnLoginEl = this.querySelector('#setting-run-on-login');
if (minScoreEl) minScoreEl.value = String(Number(s.min_score));
if (limitEl) limitEl.value = String(Number(s.limit));
if (runOnLoginEl) runOnLoginEl.checked = Boolean(s.run_on_login);
const standardFoldersEl = this.querySelector('#setting-standard-folders-enabled');
if (standardFoldersEl) standardFoldersEl.checked = Boolean(s.standard_folders_enabled);
renderMonitorPathsList(s.monitor_paths);
const extEl = this.querySelector('#setting-watch-extensions');
if (extEl) extEl.value = Array.isArray(s.watch_extensions) ? s.watch_extensions.join(', ') : (s.watch_extensions || '');
} catch (e) {
const minScoreEl = this.querySelector('#setting-min-score');
const limitEl = this.querySelector('#setting-limit');
const runOnLoginEl = this.querySelector('#setting-run-on-login');
if (minScoreEl) minScoreEl.value = String(DEFAULTS.min_score);
if (limitEl) limitEl.value = String(DEFAULTS.limit);
if (runOnLoginEl) runOnLoginEl.checked = DEFAULTS.run_on_login;
const standardFoldersEl = this.querySelector('#setting-standard-folders-enabled');
if (standardFoldersEl) standardFoldersEl.checked = DEFAULTS.standard_folders_enabled;
renderMonitorPathsList(DEFAULTS.monitor_paths);
const extEl = this.querySelector('#setting-watch-extensions');
if (extEl) extEl.value = DEFAULTS.watch_extensions.join(', ');
}
};
this.loadForm = async (data) => {
let fromFile = data;
if (fromFile == null) {
fromFile = await loadSettingsFromFile();
if (fromFile && window.__TAURI__?.core?.invoke) {
try {
const invoke = window.__TAURI__.core.invoke;
const osEnabled = await invoke('autostart_is_enabled');
if (fromFile.run_on_login !== osEnabled) {
fromFile.run_on_login = osEnabled;
}
} catch (_) {}
}
}
if (fromFile) {
loadSettingsIntoForm(fromFile);
localStorage.setItem(SETTINGS_KEY, JSON.stringify(fromFile));
} else {
loadSettingsIntoForm();
}
};
const modalEl = this.querySelector('#settings-monitor-path-modal');
const modalPath = this.querySelector('#modal-monitor-path');
const modalCategory = this.querySelector('#modal-monitor-category');
const modalDescription = this.querySelector('#modal-monitor-description');
const openMonitorPathModal = (index) => {
this._editingMonitorPathIndex = index;
const titleEl = this.querySelector('#settings-monitor-path-modal-title');
if (titleEl) titleEl.textContent = index === null ? 'フォルダを追加' : 'フォルダを編集';
if (modalPath) modalPath.value = index !== null && this._monitorPaths[index] ? this._monitorPaths[index].path : '';
if (modalCategory) modalCategory.value = index !== null && this._monitorPaths[index] ? this._monitorPaths[index].category : '';
if (modalDescription) modalDescription.value = index !== null && this._monitorPaths[index] ? this._monitorPaths[index].description : '';
if (modalEl) {
modalEl.hidden = false;
modalPath?.focus();
}
};
const closeMonitorPathModal = () => {
if (modalEl) modalEl.hidden = true;
this._editingMonitorPathIndex = null;
};
this.querySelector('#settings-monitor-path-add')?.addEventListener('click', (e) => {
e.preventDefault();
openMonitorPathModal(null);
});
this.addEventListener('click', async (e) => {
if (e.target.classList.contains('setting-monitor-open')) {
const path = (e.target.dataset.path || '').trim();
const category = (e.target.dataset.category || '').trim();
if (window.__TAURI__?.core?.invoke) {
try {
await window.__TAURI__.core.invoke('open_path_in_explorer', {
path: path,
category: category || undefined,
});
} catch (err) {
const feedback = this.querySelector('#settings-feedback');
if (feedback) feedback.textContent = err?.toString?.() || 'フォルダを開けませんでした';
}
}
return;
}
if (e.target.classList.contains('setting-monitor-edit')) {
const i = parseInt(e.target.dataset.index, 10);
if (!Number.isNaN(i) && i >= 0) openMonitorPathModal(i);
return;
}
if (e.target.classList.contains('setting-monitor-remove')) {
const i = parseInt(e.target.dataset.index, 10);
if (Number.isNaN(i) || i < 0) return;
const path = (this._monitorPaths[i] && this._monitorPaths[i].path) || '';
if (path) this._removeFromIndexPaths.add(path);
this._monitorPaths.splice(i, 1);
renderMonitorPathsTable();
}
});
this.querySelector('#settings-monitor-path-modal-cancel')?.addEventListener('click', () => closeMonitorPathModal());
this.querySelector('#settings-monitor-path-modal-save')?.addEventListener('click', () => {
const path = (modalPath?.value || '').trim();
const category = (modalCategory?.value || '').trim();
const description = (modalDescription?.value || '').trim();
const idx = this._editingMonitorPathIndex;
if (idx === null) {
if (path) this._monitorPaths.push({ path, category, description });
} else if (typeof idx === 'number' && this._monitorPaths[idx] !== undefined) {
this._monitorPaths[idx] = { path, category, description };
}
renderMonitorPathsTable();
closeMonitorPathModal();
});
modalEl?.addEventListener('click', (e) => {
if (e.target === modalEl) closeMonitorPathModal();
});
this.querySelector('#setting-run-on-login')?.addEventListener('change', () => {
const runOnLoginCheckbox = this.querySelector('#setting-run-on-login');
if (runOnLoginCheckbox) {
console.log('[TelosDB] 自動起動:', runOnLoginCheckbox.checked ? 'オン' : 'オフ');
}
});
// 標準フォルダONで一覧に4行を即表示(path は空で保存時にバックエンドが実パスを付与)
const standardFoldersToggle = this.querySelector('#setting-standard-folders-enabled');
if (standardFoldersToggle) {
standardFoldersToggle.addEventListener('change', () => {
const enabled = standardFoldersToggle.checked;
const arr = Array.isArray(this._monitorPaths) ? [...this._monitorPaths] : [];
const standardNames = new Set(STANDARD_FOLDER_CATEGORIES.map(([name]) => name));
if (enabled) {
for (const [category, description] of STANDARD_FOLDER_CATEGORIES) {
const has = arr.some((p) => (p && p.category) === category);
if (!has) arr.push({ path: '', category, description });
}
} else {
for (let i = arr.length - 1; i >= 0; i--) {
if (standardNames.has((arr[i] && arr[i].category) || '')) arr.splice(i, 1);
}
}
this._monitorPaths = arr;
renderMonitorPathsTable();
});
}
this.querySelector('#settings-refresh-btn')?.addEventListener('click', async () => {
const feedbackEl = this.querySelector('#settings-feedback');
if (feedbackEl) feedbackEl.textContent = '再読み込み中…';
try {
await this.loadForm();
if (feedbackEl) feedbackEl.textContent = '設定を再読み込みしました';
if (feedbackEl) setTimeout(() => { feedbackEl.textContent = ''; }, 2000);
} catch (e) {
if (feedbackEl) feedbackEl.textContent = '再読み込みに失敗しました';
}
});
const saveBtn = this.querySelector('#settings-save-btn');
const feedbackEl = this.querySelector('#settings-feedback');
if (saveBtn && feedbackEl) {
saveBtn.addEventListener('click', async () => {
const minScoreEl = this.querySelector('#setting-min-score');
const limitEl = this.querySelector('#setting-limit');
const runOnLoginEl = this.querySelector('#setting-run-on-login');
const min_score = Math.max(0, Math.min(1, parseFloat(minScoreEl?.value) || DEFAULTS.min_score));
const limit = Math.max(1, Math.min(100, parseInt(limitEl?.value, 10) || DEFAULTS.limit));
const run_on_login = runOnLoginEl ? runOnLoginEl.checked : DEFAULTS.run_on_login;
const standardFoldersEl = this.querySelector('#setting-standard-folders-enabled');
const standard_folders_enabled = standardFoldersEl ? standardFoldersEl.checked : DEFAULTS.standard_folders_enabled;
const monitor_paths = (Array.isArray(this._monitorPaths) ? this._monitorPaths : [])
.map((p) => {
const path = (p && p.path || '').trim();
const category = (p && p.category || '').trim();
const description = (p && p.description || '').trim();
return path ? { path, category, ...(description ? { description } : {}) } : null;
})
.filter(Boolean);
const extInput = this.querySelector('#setting-watch-extensions');
const watch_extensions = (extInput?.value || '')
.split(/[\s,]+/)
.map((s) => s.trim().toLowerCase())
.filter(Boolean);
try {
let autostartError = null;
try {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke) throw new Error('Tauri API が利用できません');
if (run_on_login) {
await invoke('autostart_enable');
console.log('[TelosDB] autostart: 有効化成功');
} else {
await invoke('autostart_disable');
console.log('[TelosDB] autostart: 無効化成功');
}
} catch (autoErr) {
autostartError = String(autoErr?.message || autoErr);
console.error('[TelosDB] autostart 設定失敗:', autoErr);
}
const remove_from_index_paths = Array.from(this._removeFromIndexPaths || []);
const payload = {
min_score,
limit,
run_on_login,
standard_folders_enabled,
monitor_paths,
watch_extensions: watch_extensions.length ? watch_extensions : DEFAULTS.watch_extensions,
remove_from_index_paths,
};
console.log('[TelosDB] 保存: run_on_login =', run_on_login, 'monitor_paths =', monitor_paths.length, 'watch_extensions =', payload.watch_extensions.length, 'remove_from_index_paths =', remove_from_index_paths.length);
const saved = await saveSettingsToFile(payload);
if (saved) {
this._removeFromIndexPaths?.clear();
let fromFile = await loadSettingsFromFile();
if (!fromFile) {
await new Promise((r) => setTimeout(r, 300));
fromFile = await loadSettingsFromFile();
}
if (fromFile) {
loadSettingsIntoForm(fromFile);
localStorage.setItem(SETTINGS_KEY, JSON.stringify(fromFile));
} else {
const toStore = { min_score, limit, run_on_login, standard_folders_enabled, monitor_paths, watch_extensions: payload.watch_extensions };
localStorage.setItem(SETTINGS_KEY, JSON.stringify(toStore));
if (minScoreEl) minScoreEl.value = String(min_score);
if (limitEl) limitEl.value = String(limit);
if (runOnLoginEl) runOnLoginEl.checked = run_on_login;
if (standardFoldersEl) standardFoldersEl.checked = standard_folders_enabled;
renderMonitorPathsList(monitor_paths);
}
if (autostartError) {
feedbackEl.textContent = '保存しました(自動起動の登録に失敗: ' + autostartError + ')';
feedbackEl.classList.add('error');
} else {
feedbackEl.textContent = '保存しました';
feedbackEl.classList.remove('error');
}
setTimeout(() => { feedbackEl.textContent = ''; }, autostartError ? 5000 : 2000);
} else {
feedbackEl.textContent = '保存に失敗しました(ファイルへの書き込みに失敗しました)';
feedbackEl.classList.add('error');
}
} catch (e) {
feedbackEl.textContent = '保存に失敗しました';
feedbackEl.classList.add('error');
}
});
}
this.querySelector('#settings-backup-btn')?.addEventListener('click', async () => {
const feedbackEl = this.querySelector('#settings-feedback');
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke || !feedbackEl) return;
try {
const date = new Date();
const yyyy = date.getFullYear();
const mm = String(date.getMonth() + 1).padStart(2, '0');
const dd = String(date.getDate()).padStart(2, '0');
const path = await invoke('plugin:dialog|save', {
options: {
defaultPath: `telosdb-settings-${yyyy}-${mm}-${dd}.json`,
filters: [{ name: 'JSON', extensions: ['json'] }],
title: '設定のバックアップ先を選択',
},
});
if (path == null) return;
await invoke('export_settings_to_file', { path });
feedbackEl.textContent = 'バックアップしました';
feedbackEl.classList.remove('error');
setTimeout(() => { feedbackEl.textContent = ''; }, 2000);
} catch (e) {
feedbackEl.textContent = 'バックアップに失敗しました: ' + (e?.message || e);
feedbackEl.classList.add('error');
}
});
this.querySelector('#settings-restore-btn')?.addEventListener('click', async () => {
const feedbackEl = this.querySelector('#settings-feedback');
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke || !feedbackEl) return;
try {
const path = await invoke('plugin:dialog|open', {
options: {
multiple: false,
filters: [{ name: 'JSON', extensions: ['json'] }],
title: '復元する設定ファイルを選択',
},
});
if (path == null) return;
const pathStr = Array.isArray(path) ? path[0] : path;
await invoke('import_settings_from_file', { path: pathStr });
const fromFile = await loadSettingsFromFile();
if (fromFile) {
loadSettingsIntoForm(fromFile);
localStorage.setItem(SETTINGS_KEY, JSON.stringify(fromFile));
}
feedbackEl.textContent = '設定を復元しました。自動起動を変更する場合は保存を押してください。';
feedbackEl.classList.remove('error');
setTimeout(() => { feedbackEl.textContent = ''; }, 4000);
} catch (e) {
feedbackEl.textContent = '復元に失敗しました: ' + (e?.message || e);
feedbackEl.classList.add('error');
}
});
this.querySelector('#settings-open-data-folder-btn')?.addEventListener('click', async () => {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke) return;
try {
await invoke('open_app_data_folder');
} catch (e) {
const feedbackEl = this.querySelector('#settings-feedback');
if (feedbackEl) {
feedbackEl.textContent = 'フォルダを開けませんでした: ' + (e?.message || e);
feedbackEl.classList.add('error');
setTimeout(() => { feedbackEl.textContent = ''; feedbackEl.classList.remove('error'); }, 4000);
}
}
});
}
}
customElements.define('settings-panel', SettingsPanel);
export {};