Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,22 @@ version with its date and start a fresh empty `[Unreleased]` above it.
snapshots now carry the configured context-window tier, and zeroed
snapshots can no longer overwrite an existing reading.

### Changed

- The credits usage button now shows a static "Usage" tooltip through
Obsidian's native tooltip (aria-label), matching the other nav-row
buttons, instead of a browser title tooltip with the live percentage.

### Fixed

- Settings changed on Obsidian 1.13+ (language, auto-scroll, and the
other simple toggles) now persist across restarts: the declarative
control writes were only mirrored into the plugin data file, not the
`.qoderian/qoderian-settings.json` store Qoderian loads at startup.
Changing the language also re-localizes open chat views immediately
(nav tooltips and tab titles) instead of keeping the old language
until the view is reopened.

## [1.0.4] - 2026-08-12

### Fixed
Expand Down
22 changes: 19 additions & 3 deletions src/features/chat/chat-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
scheduleAnimationFrame,
type ScheduledAnimationFrame,
} from '../../shared/dom/animation-frame';
import { setButtonTooltip } from '../../shared/dom/tooltip';
import { createIconSvg, QODER_ICON,QODERIAN_ICON_ID } from '../../shared/icons';
import type { HistoryConversationStatus } from './controllers/conversation-controller';
import {
Expand Down Expand Up @@ -44,6 +45,8 @@ export class QoderianView extends ItemView {
private viewContainerEl: HTMLElement | null = null;
private logoEl: HTMLElement | null = null;
private newTabButtonEl: HTMLElement | null = null;
private newConversationButtonEl: HTMLElement | null = null;
private historyButtonEl: HTMLElement | null = null;

// Header elements
private historyDropdown: HTMLElement | null = null;
Expand Down Expand Up @@ -244,14 +247,15 @@ export class QoderianView extends ItemView {

this.newTabButtonEl = navActionsEl.createDiv({ cls: 'qoderian-input-nav-btn qoderian-new-tab-btn' });
setIcon(this.newTabButtonEl, 'square-plus');
this.newTabButtonEl.setAttribute('aria-label', 'New tab');
setButtonTooltip(this.newTabButtonEl, t('commands.newTab'));
this.newTabButtonEl.addEventListener('click', () => {
void this.createNewTab().catch(() => new Notice('Failed to create tab'));
});

const newBtn = navActionsEl.createDiv({ cls: 'qoderian-input-nav-btn' });
setIcon(newBtn, 'square-pen');
newBtn.setAttribute('aria-label', 'New conversation');
setButtonTooltip(newBtn, t('nav.newConversation'));
this.newConversationButtonEl = newBtn;
newBtn.addEventListener('click', () => {
void (async () => {
await this.tabManager?.createNewConversation();
Expand All @@ -263,7 +267,8 @@ export class QoderianView extends ItemView {
const historyContainer = navActionsEl.createDiv({ cls: 'qoderian-history-container' });
const historyBtn = historyContainer.createDiv({ cls: 'qoderian-input-nav-btn' });
setIcon(historyBtn, 'history');
historyBtn.setAttribute('aria-label', 'Chat history');
setButtonTooltip(historyBtn, t('nav.chatHistory'));
this.historyButtonEl = historyBtn;

this.historyDropdown = historyContainer.createDiv({ cls: 'qoderian-history-menu' });

Expand Down Expand Up @@ -346,6 +351,17 @@ export class QoderianView extends ItemView {
this.updateTabBarVisibility();
}

/** Re-applies locale-dependent static text after a language change. */
refreshLocalizedChrome(): void {
if (this.newTabButtonEl) setButtonTooltip(this.newTabButtonEl, t('commands.newTab'));
if (this.newConversationButtonEl) {
setButtonTooltip(this.newConversationButtonEl, t('nav.newConversation'));
}
if (this.historyButtonEl) setButtonTooltip(this.historyButtonEl, t('nav.chatHistory'));
this.creditsUsageButton?.refreshLocale();
this.updateTabBar();
}

// ============================================
// Tab Management
// ============================================
Expand Down
3 changes: 2 additions & 1 deletion src/features/chat/tabs/tab-bar.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { scheduleAnimationFrame } from '../../../shared/dom/animation-frame';
import { setButtonTooltip } from '../../../shared/dom/tooltip';
import type { TabBarItem, TabId } from './types';

const EXPANDED_TITLE_MAX_LENGTH = 32;
Expand Down Expand Up @@ -82,7 +83,7 @@ export class TabBar {
});

// Obsidian uses aria-label for hover tooltips here; adding title causes duplicate tooltip text.
badgeEl.setAttribute('aria-label', item.title);
setButtonTooltip(badgeEl, item.title);
badgeEl.setAttribute('data-title-expanded', isTitleExpanded ? 'true' : 'false');

// Click handler to switch tab
Expand Down
3 changes: 2 additions & 1 deletion src/features/chat/tabs/tab-lifecycle.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { t } from '../../../i18n/i18n';
import type QoderianPlugin from '../../../main';
import { cleanupThinkingBlock } from '../rendering/thinking-block-renderer';
import type { TabData } from './types';
Expand Down Expand Up @@ -71,5 +72,5 @@ export function getTabTitle(tab: TabData, plugin: QoderianPlugin): string {
const conversation = plugin.getConversationSync(tab.conversationId);
if (conversation?.title) return conversation.title;
}
return 'New Chat';
return t('nav.newChat');
}
16 changes: 10 additions & 6 deletions src/features/chat/ui/credits-usage-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
} from '../../../core/types/services';
import { getLocale, t } from '../../../i18n/i18n';
import { getQoderAccountUsageUrl } from '../../../qoder/config/cli-edition';
import { setButtonTooltip } from '../../../shared/dom/tooltip';
import { ClickPopover } from './toolbar/click-popover';

/** Usage snapshots older than this are refreshed when the panel opens. */
Expand Down Expand Up @@ -97,6 +98,12 @@ export class CreditsUsageButton {
this.container.remove();
}

/** Re-applies locale-dependent text after a language change. */
refreshLocale(): void {
this.updateButton();
this.renderPanel();
}

/** Fetches a fresh snapshot; cached snapshots within the TTL are kept. */
async refresh(force: boolean): Promise<void> {
if (this.loading) return;
Expand Down Expand Up @@ -125,12 +132,9 @@ export class CreditsUsageButton {
};

private updateButton(): void {
const percent = this.snapshot?.totalUsagePercentage;
if (typeof percent === 'number') {
this.buttonEl.setAttribute('title', t('credits.trigger', { percent: Math.round(percent) }));
} else {
this.buttonEl.setAttribute('title', t('credits.unavailable'));
}
// Same tooltip path as the other nav-row buttons (aria-label + 300ms
// delay); only the click behavior (popover) differs.
setButtonTooltip(this.buttonEl, t('credits.trigger'));
}

private renderPanel(): void {
Expand Down
32 changes: 26 additions & 6 deletions src/features/settings/settings-tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,15 +417,30 @@ export class QoderianSettingTab extends PluginSettingTab {
return this.plugin.saveSettings();
}

if (key === 'mediaFolder') {
return super.setControlValue(key, String(value).trim());
}
return this.persistControlValue(key, value);
}
// Unreachable: Obsidian < 1.13 never calls setControlValue.
}

const result = super.setControlValue(key, value);
/**
* The declarative base class persists into the plugin data file, but
* Qoderian keeps its settings in .qoderian/qoderian-settings.json, so per
* the API contract ("override to write to a different data source") we
* mutate the settings bag ourselves and save once through the store that
* loadSettings() actually reads — no duplicate write to data.json.
*/
private async persistControlValue(key: string, value: unknown): Promise<void> {
// Only reached from setControlValue on Obsidian 1.13+; re-checked here so
// the 1.13-only view-refresh calls stay behind an explicit version guard.
if (requireApiVersion('1.13.0')) {
const settings = this.plugin.settings as unknown as Record<string, unknown>;
settings[key] = key === 'mediaFolder' ? String(value).trim() : value;
await this.plugin.saveSettings();

if (key === 'locale') {
setLocale(this.plugin.settings.locale as Locale);
this.update();
this.refreshViewChrome();
} else if (key === 'maxTabs') {
for (const view of this.plugin.getAllViews()) {
view.refreshTabControls();
Expand All @@ -435,10 +450,14 @@ export class QoderianSettingTab extends PluginSettingTab {
} else if (PROMPT_SETTING_KEYS.has(key)) {
this.schedulePromptRestart();
}
}
}

return result;
/** Re-applies locale-dependent text in open chat views after a language change. */
private refreshViewChrome(): void {
for (const view of this.plugin.getAllViews()) {
view.refreshLocalizedChrome();
}
// Unreachable: Obsidian < 1.13 never calls setControlValue.
}

/**
Expand Down Expand Up @@ -518,6 +537,7 @@ export class QoderianSettingTab extends PluginSettingTab {
this.plugin.settings.locale = locale;
await this.plugin.saveSettings();
this.display();
this.refreshViewChrome();
});
});

Expand Down
7 changes: 6 additions & 1 deletion src/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
"newSession": "Neue Sitzung (im aktuellen Tab)",
"closeCurrentTab": "Aktuellen Tab schließen"
},
"nav": {
"newConversation": "Neue Unterhaltung",
"chatHistory": "Chatverlauf",
"newChat": "Neuer Chat"
},
"chat": {
"rewind": {
"confirmMessage": "Zu diesem Punkt zurückspulen? Dateiänderungen nach dieser Nachricht werden rückgängig gemacht. Das Zurückspulen betrifft keine manuell oder über Bash bearbeiteten Dateien.",
Expand Down Expand Up @@ -292,7 +297,7 @@
"renewsOn": "Verlängert am {date}",
"usedPercent": "{percent}% verwendet",
"left": "{count} übrig",
"trigger": "Nutzung - {percent}%",
"trigger": "Nutzung",
"unavailable": "Nutzung nicht verfügbar"
},
"model": {
Expand Down
7 changes: 6 additions & 1 deletion src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
"newSession": "New session (in current tab)",
"closeCurrentTab": "Close current tab"
},
"nav": {
"newConversation": "New conversation",
"chatHistory": "Chat history",
"newChat": "New Chat"
},
"chat": {
"rewind": {
"confirmMessage": "Rewind to this point? File changes after this message will be reverted. Rewinding does not affect files edited manually or via bash.",
Expand Down Expand Up @@ -292,7 +297,7 @@
"renewsOn": "Renews on {date}",
"usedPercent": "{percent}% used",
"left": "{count} left",
"trigger": "Usage - {percent}%",
"trigger": "Usage",
"unavailable": "Usage unavailable"
},
"model": {
Expand Down
7 changes: 6 additions & 1 deletion src/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
"newSession": "Nueva sesión (en la pestaña actual)",
"closeCurrentTab": "Cerrar pestaña actual"
},
"nav": {
"newConversation": "Nueva conversación",
"chatHistory": "Historial del chat",
"newChat": "Nuevo chat"
},
"chat": {
"rewind": {
"confirmMessage": "¿Rebobinar a este punto? Los cambios de archivos después de este mensaje serán revertidos. El rebobinado no afecta archivos editados manualmente o mediante bash.",
Expand Down Expand Up @@ -292,7 +297,7 @@
"renewsOn": "Se renueva el {date}",
"usedPercent": "{percent}% usado",
"left": "{count} restantes",
"trigger": "Uso - {percent}%",
"trigger": "Uso",
"unavailable": "Uso no disponible"
},
"model": {
Expand Down
7 changes: 6 additions & 1 deletion src/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
"newSession": "Nouvelle session (dans l'onglet actuel)",
"closeCurrentTab": "Fermer l'onglet actuel"
},
"nav": {
"newConversation": "Nouvelle conversation",
"chatHistory": "Historique du chat",
"newChat": "Nouveau chat"
},
"chat": {
"rewind": {
"confirmMessage": "Rembobiner jusqu'à ce point ? Les modifications de fichiers après ce message seront annulées. Le rembobinage n'affecte pas les fichiers modifiés manuellement ou via bash.",
Expand Down Expand Up @@ -292,7 +297,7 @@
"renewsOn": "Renouvellement le {date}",
"usedPercent": "{percent}% utilisés",
"left": "{count} restants",
"trigger": "Utilisation - {percent}%",
"trigger": "Utilisation",
"unavailable": "Utilisation indisponible"
},
"model": {
Expand Down
7 changes: 6 additions & 1 deletion src/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
"newSession": "新しいセッション(現在のタブ)",
"closeCurrentTab": "現在のタブを閉じる"
},
"nav": {
"newConversation": "新しい会話",
"chatHistory": "チャット履歴",
"newChat": "新しいチャット"
},
"chat": {
"rewind": {
"confirmMessage": "この時点に巻き戻しますか?このメッセージ以降のファイル変更が元に戻されます。手動またはbashで編集されたファイルには影響しません。",
Expand Down Expand Up @@ -292,7 +297,7 @@
"renewsOn": "{date} に更新",
"usedPercent": "{percent}% 使用済み",
"left": "残り {count}",
"trigger": "利用量 - {percent}%",
"trigger": "利用量",
"unavailable": "利用量情報を取得できません"
},
"model": {
Expand Down
7 changes: 6 additions & 1 deletion src/i18n/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
"newSession": "새 세션 (현재 탭)",
"closeCurrentTab": "현재 탭 닫기"
},
"nav": {
"newConversation": "새 대화",
"chatHistory": "대화 기록",
"newChat": "새 채팅"
},
"chat": {
"rewind": {
"confirmMessage": "이 시점으로 되감으시겠습니까? 이 메시지 이후의 파일 변경 사항이 되돌려집니다. 수동으로 또는 bash를 통해 편집된 파일에는 영향을 미치지 않습니다.",
Expand Down Expand Up @@ -292,7 +297,7 @@
"renewsOn": "{date}에 갱신",
"usedPercent": "{percent}% 사용",
"left": "{count} 남음",
"trigger": "사용량 - {percent}%",
"trigger": "사용량",
"unavailable": "사용량을 확인할 수 없음"
},
"model": {
Expand Down
7 changes: 6 additions & 1 deletion src/i18n/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
"newSession": "Nova sessão (na aba atual)",
"closeCurrentTab": "Fechar aba atual"
},
"nav": {
"newConversation": "Nova conversa",
"chatHistory": "Histórico de conversas",
"newChat": "Novo chat"
},
"chat": {
"rewind": {
"confirmMessage": "Retroceder até este ponto? As alterações de arquivos após esta mensagem serão revertidas. O retrocesso não afeta arquivos editados manualmente ou via bash.",
Expand Down Expand Up @@ -292,7 +297,7 @@
"renewsOn": "Renova em {date}",
"usedPercent": "{percent}% usado",
"left": "{count} restantes",
"trigger": "Uso - {percent}%",
"trigger": "Uso",
"unavailable": "Uso indisponível"
},
"model": {
Expand Down
7 changes: 6 additions & 1 deletion src/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
"newSession": "Новая сессия (в текущей вкладке)",
"closeCurrentTab": "Закрыть текущую вкладку"
},
"nav": {
"newConversation": "Новый разговор",
"chatHistory": "История чата",
"newChat": "Новый чат"
},
"chat": {
"rewind": {
"confirmMessage": "Откатить до этой точки? Изменения файлов после этого сообщения будут отменены. Откат не затрагивает файлы, отредактированные вручную или через bash.",
Expand Down Expand Up @@ -292,7 +297,7 @@
"renewsOn": "Обновление {date}",
"usedPercent": "использовано {percent}%",
"left": "осталось {count}",
"trigger": "Использование - {percent}%",
"trigger": "Использование",
"unavailable": "Использование недоступно"
},
"model": {
Expand Down
7 changes: 6 additions & 1 deletion src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
"newSession": "新建会话(当前标签页)",
"closeCurrentTab": "关闭当前标签页"
},
"nav": {
"newConversation": "新建会话",
"chatHistory": "聊天历史",
"newChat": "新会话"
},
"chat": {
"rewind": {
"confirmMessage": "回退到此处?此消息之后的文件更改将被还原。回退不会影响手动或通过 bash 编辑的文件。",
Expand Down Expand Up @@ -292,7 +297,7 @@
"renewsOn": "将于 {date} 刷新",
"usedPercent": "已使用 {percent}%",
"left": "剩余 {count}",
"trigger": "用量 - {percent}%",
"trigger": "用量",
"unavailable": "用量暂不可用"
},
"model": {
Expand Down
Loading
Loading