diff --git a/__tests__/classifier/ClassificationService.test.ts b/__tests__/classifier/ClassificationService.test.ts
index 971980a..ba23e4f 100644
--- a/__tests__/classifier/ClassificationService.test.ts
+++ b/__tests__/classifier/ClassificationService.test.ts
@@ -1,27 +1,27 @@
-import { ClassificationService, ClassificationContext } from '../../src/classifier/ClassificationService';
+import { ClassificationService, ClassificationContext } from '../../src/ui/ClassificationService';
import type { App, TFile, MetadataCache, Vault } from 'obsidian';
import type { FrontmatterField, ProviderConfig, OAuthTokens } from '../../src/types';
-import { processAPIRequest } from '../../src/provider';
-import { insertToFrontMatter, getFieldValues } from '../../src/lib/frontmatter';
-import { getPromptTemplate } from '../../src/provider/prompt';
+import { processAPIRequest } from '../../src/ui/provider-api';
+import { insertToFrontMatter, getFieldValues } from '../../src/ui/frontmatter';
+import { getPromptTemplate } from '../../src/domain/prompt';
// Mock dependencies
-vi.mock('../../src/provider', () => ({
+vi.mock('../../src/ui/provider-api', () => ({
processAPIRequest: vi.fn(),
}));
-vi.mock('../../src/provider/prompt', () => ({
+vi.mock('../../src/domain/prompt', () => ({
DEFAULT_SYSTEM_ROLE: 'Test system role',
getPromptTemplate: vi.fn().mockReturnValue('Test prompt'),
}));
-vi.mock('../../src/lib/frontmatter', () => ({
+vi.mock('../../src/ui/frontmatter', () => ({
getContentWithoutFrontmatter: vi.fn().mockReturnValue('Test content'),
getFieldValues: vi.fn().mockReturnValue(['tag1', 'tag2']),
insertToFrontMatter: vi.fn().mockResolvedValue(undefined),
}));
-vi.mock('../../src/settings/components/Notice', () => ({
+vi.mock('../../src/ui/settings/components/Notice', () => ({
Notice: {
error: vi.fn(),
success: vi.fn(),
diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts
index 5843873..e34c8db 100644
--- a/__tests__/main.test.ts
+++ b/__tests__/main.test.ts
@@ -1,7 +1,7 @@
import AutoClassifierPlugin from 'main';
import { App, TFile, createMockTFile } from 'obsidian';
-import { DEFAULT_SETTINGS, DEFAULT_FRONTMATTER_SETTING } from '../src/constants';
-import type { ProviderConfig, FrontmatterField, OAuthTokens } from 'types';
+import { DEFAULT_SETTINGS, DEFAULT_FRONTMATTER_SETTING } from '../src/domain/constants';
+import type { ProviderConfig, FrontmatterField, OAuthTokens } from '../src/types/index';
import { Notice as SettingsNotice } from 'settings/components/Notice';
import { processAPIRequest } from 'provider';
import { insertToFrontMatter, getFieldValues } from 'lib/frontmatter';
diff --git a/__tests__/provider/auth/oauth.test.ts b/__tests__/provider/auth/oauth.test.ts
index ece29a8..9317351 100644
--- a/__tests__/provider/auth/oauth.test.ts
+++ b/__tests__/provider/auth/oauth.test.ts
@@ -1,9 +1,9 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { Platform, requestUrl } from 'obsidian';
-import { CodexOAuth } from '../../../src/provider/auth/oauth';
-import type { OAuthTokens } from '../../../src/provider/auth/types';
-import { OAuthCallbackServer } from '../../../src/provider/auth/oauth-server';
-import { createTokensFromResponse, isTokenExpired } from '../../../src/provider/auth/token-manager';
+import { CodexOAuth } from '../../../src/ui/auth/oauth';
+import type { OAuthTokens } from '../../../src/types/auth';
+import { OAuthCallbackServer } from '../../../src/domain/auth/oauth-server';
+import { createTokensFromResponse, isTokenExpired } from '../../../src/domain/auth/token-manager';
import type { Mock } from 'vitest';
// Mock obsidian
@@ -15,7 +15,7 @@ vi.mock('obsidian', () => ({
}));
// Mock the oauth-server - use a class-like constructor mock
-vi.mock('../../../src/provider/auth/oauth-server', () => {
+vi.mock('../../../src/domain/auth/oauth-server', () => {
const MockServer = vi.fn(function (this: any) {
this.waitForCallback = vi.fn();
this.stop = vi.fn();
@@ -24,7 +24,7 @@ vi.mock('../../../src/provider/auth/oauth-server', () => {
});
// Mock pkce
-vi.mock('../../../src/provider/auth/pkce', () => ({
+vi.mock('../../../src/domain/auth/pkce', () => ({
generatePKCEChallenge: vi.fn().mockResolvedValue({
codeVerifier: 'test-verifier',
codeChallenge: 'test-challenge',
@@ -33,7 +33,7 @@ vi.mock('../../../src/provider/auth/pkce', () => ({
}));
// Mock token-manager
-vi.mock('../../../src/provider/auth/token-manager', () => ({
+vi.mock('../../../src/domain/auth/token-manager', () => ({
createTokensFromResponse: vi.fn().mockReturnValue({
accessToken: 'new-access-token',
refreshToken: 'new-refresh-token',
diff --git a/__tests__/provider/auth/pkce.test.ts b/__tests__/provider/auth/pkce.test.ts
index 960e2b8..f5abb17 100644
--- a/__tests__/provider/auth/pkce.test.ts
+++ b/__tests__/provider/auth/pkce.test.ts
@@ -3,7 +3,7 @@ import {
generateCodeChallenge,
generateState,
generatePKCEChallenge,
-} from '../../../src/provider/auth/pkce';
+} from '../../../src/domain/auth/pkce';
describe('pkce', () => {
describe('generateCodeVerifier', () => {
diff --git a/__tests__/provider/auth/token-manager.test.ts b/__tests__/provider/auth/token-manager.test.ts
index 4e3fb33..f261661 100644
--- a/__tests__/provider/auth/token-manager.test.ts
+++ b/__tests__/provider/auth/token-manager.test.ts
@@ -5,8 +5,8 @@ import {
createTokensFromResponse,
getTokenRemainingTime,
formatTokenExpiry,
-} from '../../../src/provider/auth/token-manager';
-import type { OAuthTokens, TokenResponse } from '../../../src/provider/auth/types';
+} from '../../../src/domain/auth/token-manager';
+import type { OAuthTokens, TokenResponse } from '../../../src/types/auth';
describe('token-manager', () => {
// Mock Date.now for consistent testing
diff --git a/__tests__/settings/ProviderSection.test.ts b/__tests__/settings/ProviderSection.test.ts
index 0213715..f0716f5 100644
--- a/__tests__/settings/ProviderSection.test.ts
+++ b/__tests__/settings/ProviderSection.test.ts
@@ -1,13 +1,13 @@
import type { Mock } from 'vitest';
-import { ProviderSection } from '../../src/settings/ProviderSection';
+import { ProviderSection } from '../../src/ui/settings/ProviderSection';
import type { App } from 'obsidian';
import type AutoClassifierPlugin from '../../src/main';
import type { ProviderConfig, OAuthTokens } from '../../src/types';
-import { formatTokenExpiry, isTokenExpired } from '../../src/provider/auth';
-import { Setting } from '../../src/settings/components/Setting';
+import { formatTokenExpiry, isTokenExpired } from '../../src/ui/auth';
+import { Setting } from '../../src/ui/settings/components/Setting';
// Mock the provider/auth module
-vi.mock('../../src/provider/auth', () => ({
+vi.mock('../../src/ui/auth', () => ({
formatTokenExpiry: vi.fn((tokens) => {
const remaining = tokens.expiresAt - Math.floor(Date.now() / 1000);
if (remaining <= 0) return 'Expired';
@@ -22,14 +22,14 @@ vi.mock('../../src/provider/auth', () => ({
}));
// Mock the ProviderModal
-vi.mock('../../src/settings/modals/ProviderModal', () => ({
+vi.mock('../../src/ui/settings/modals/ProviderModal', () => ({
ProviderModal: vi.fn().mockImplementation(() => ({
open: vi.fn(),
})),
}));
// Mock Setting component
-vi.mock('../../src/settings/components/Setting', () => ({
+vi.mock('../../src/ui/settings/components/Setting', () => ({
Setting: {
create: vi.fn(),
},
diff --git a/__tests__/settings/TagSection.test.ts b/__tests__/settings/TagSection.test.ts
index 2267b56..71b102d 100644
--- a/__tests__/settings/TagSection.test.ts
+++ b/__tests__/settings/TagSection.test.ts
@@ -5,7 +5,7 @@ vi.mock('settings/modals/FrontmatterEditorModal', () => ({
import type { Mock } from 'vitest';
import type { FrontmatterField } from 'types';
import { Tag } from 'settings/TagSection';
-import { DEFAULT_TAG_SETTING } from '../../src/constants';
+import { DEFAULT_TAG_SETTING } from '../../src/domain/constants';
interface MockPlugin {
app: { vault: { getMarkdownFiles: Mock } };
diff --git a/eslint.base.js b/eslint.base.js
index ae1291f..168952a 100644
--- a/eslint.base.js
+++ b/eslint.base.js
@@ -24,6 +24,18 @@ export const baseConfig = tseslint.config(
'no-console': 'error',
},
},
+ {
+ files: ['src/domain/**/*.ts', 'src/types/**/*.ts', 'src/utils/**/*.ts'],
+ ignores: ['**/*.d.ts'],
+ rules: {
+ 'no-restricted-imports': ['error', {
+ patterns: [{
+ group: ['obsidian', 'obsidian/*'],
+ message: 'domain/, types/, and utils/ layers must not import from obsidian. Move this code to ui/ or inject the dependency.',
+ }],
+ }],
+ },
+ },
{
files: ['scripts/**/*.{js,mjs}', 'tooling/**/*.{js,mjs}', 'esbuild.config.mjs', 'version-bump.mjs'],
languageOptions: {
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 2cb6b4d..ee8ffb9 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -20,51 +20,56 @@ export default [
unicorn,
},
settings: {
- // Module boundary definitions
- // Structure: provider/ | classifier/ | settings/ | lib/
+ // Module boundary definitions — 4-layer architecture
+ // main → ui → domain → utils/types
'boundaries/elements': [
{
- type: 'provider',
- pattern: 'src/provider/**',
+ type: 'main',
+ pattern: 'src/main.ts',
+ mode: 'file',
+ },
+ {
+ type: 'ui',
+ pattern: 'src/ui/**',
mode: 'folder',
},
{
- type: 'classifier',
- pattern: 'src/classifier/**',
+ type: 'domain',
+ pattern: 'src/domain/**',
mode: 'folder',
},
{
- type: 'settings',
- pattern: 'src/settings/**',
+ type: 'utils',
+ pattern: 'src/utils/**',
mode: 'folder',
},
{
- type: 'lib',
- pattern: 'src/lib/**',
+ type: 'types',
+ pattern: 'src/types/**',
mode: 'folder',
},
{
- type: 'main',
- pattern: 'src/main.ts',
- mode: 'file',
+ type: 'shared',
+ pattern: 'src/shared/**',
+ mode: 'folder',
},
],
- // Dependency rules between modules
+ // Dependency rules between layers
'boundaries/rules': [
{
- from: 'settings',
- disallow: ['provider'],
- message: 'settings cannot import provider directly. Use classifier instead.',
+ from: 'domain',
+ disallow: ['ui'],
+ message: 'domain layer must not import from ui layer.',
},
{
- from: 'provider',
- disallow: ['settings', 'classifier'],
- message: 'provider is pure API layer. No UI/business logic dependency.',
+ from: 'utils',
+ disallow: ['ui', 'domain'],
+ message: 'utils layer must not import from ui or domain.',
},
{
- from: 'lib',
- disallow: ['provider', 'classifier', 'settings'],
- message: 'lib is pure utility. No domain module dependency.',
+ from: 'types',
+ disallow: ['ui', 'domain', 'utils'],
+ message: 'types layer must not import from other layers.',
},
],
},
diff --git a/src/domain/.gitkeep b/src/domain/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/domain/auth/index.ts b/src/domain/auth/index.ts
new file mode 100644
index 0000000..0798422
--- /dev/null
+++ b/src/domain/auth/index.ts
@@ -0,0 +1,16 @@
+// Domain auth module exports — pure, no obsidian imports
+export { CODEX_OAUTH } from './oauth-constants';
+export {
+ isTokenExpired,
+ formatTokenExpiry,
+ getTokenRemainingTime,
+ parseJwtClaims,
+ extractAccountId,
+ createTokensFromResponse,
+} from './token-manager';
+export type {
+ OAuthTokens,
+ PKCEChallenge,
+ TokenResponse,
+ OAuthCallbackResponse,
+} from '../../types/auth';
diff --git a/src/provider/auth/oauth-constants.ts b/src/domain/auth/oauth-constants.ts
similarity index 100%
rename from src/provider/auth/oauth-constants.ts
rename to src/domain/auth/oauth-constants.ts
diff --git a/src/provider/auth/oauth-server.ts b/src/domain/auth/oauth-server.ts
similarity index 98%
rename from src/provider/auth/oauth-server.ts
rename to src/domain/auth/oauth-server.ts
index d2fc996..d9251f4 100644
--- a/src/provider/auth/oauth-server.ts
+++ b/src/domain/auth/oauth-server.ts
@@ -1,5 +1,5 @@
import { CODEX_OAUTH } from './oauth-constants';
-import type { OAuthCallbackResponse } from './types';
+import type { OAuthCallbackResponse } from '../../types/auth';
import { macLogger } from '../../shared/mac-logger';
// Types for Node.js http module (imported dynamically)
diff --git a/src/provider/auth/pkce.ts b/src/domain/auth/pkce.ts
similarity index 96%
rename from src/provider/auth/pkce.ts
rename to src/domain/auth/pkce.ts
index 26daac0..efe48a7 100644
--- a/src/provider/auth/pkce.ts
+++ b/src/domain/auth/pkce.ts
@@ -1,4 +1,4 @@
-import type { PKCEChallenge } from './types';
+import type { PKCEChallenge } from '../../types/auth';
/**
* Generate a cryptographically secure random string for PKCE code verifier
diff --git a/src/provider/auth/token-manager.ts b/src/domain/auth/token-manager.ts
similarity index 97%
rename from src/provider/auth/token-manager.ts
rename to src/domain/auth/token-manager.ts
index 07bfa7d..5fec344 100644
--- a/src/provider/auth/token-manager.ts
+++ b/src/domain/auth/token-manager.ts
@@ -1,5 +1,5 @@
import { CODEX_OAUTH } from './oauth-constants';
-import type { OAuthTokens, TokenResponse } from './types';
+import type { OAuthTokens, TokenResponse } from '../../types/auth';
/**
* Parse JWT claims without verification (for extracting account_id)
diff --git a/src/constants.ts b/src/domain/constants.ts
similarity index 96%
rename from src/constants.ts
rename to src/domain/constants.ts
index 0b8f7b0..63f8e3a 100644
--- a/src/constants.ts
+++ b/src/domain/constants.ts
@@ -2,11 +2,11 @@
// Consolidated Constants - All constants in one place
// ==========================================
-import type { FrontmatterField, LinkType, ProviderConfig, AutoClassifierSettings } from './types';
-import type { NoticeCatalog } from './shared/plugin-notices';
+import type { FrontmatterField, LinkType, ProviderConfig, AutoClassifierSettings } from '../types';
+import type { NoticeCatalog } from '../shared/plugin-notices';
// ==========================================
-// Common Constants (from api/constants.ts)
+// Common Constants
// ==========================================
export const COMMON_CONSTANTS = {
@@ -152,10 +152,10 @@ export const OLLAMA_STRUCTURE_OUTPUT = {
};
// ==========================================
-// Default Settings (from utils/constants.ts)
+// Default Settings
// ==========================================
-// Prompt template - originally in api/prompt.ts but used by constants
+// Prompt template
export const DEFAULT_TASK_TEMPLATE = `
Classify the following content using the provided reference categories.
diff --git a/src/provider/presets.json b/src/domain/presets.json
similarity index 100%
rename from src/provider/presets.json
rename to src/domain/presets.json
diff --git a/src/provider/prompt.ts b/src/domain/prompt.ts
similarity index 95%
rename from src/provider/prompt.ts
rename to src/domain/prompt.ts
index ab88e3c..b959ae1 100644
--- a/src/provider/prompt.ts
+++ b/src/domain/prompt.ts
@@ -1,5 +1,5 @@
-import { DEFAULT_TASK_TEMPLATE } from '../constants';
-import { sanitizePromptInput, sanitizeReferenceValues } from '../lib/sanitizer';
+import { DEFAULT_TASK_TEMPLATE } from './constants';
+import { sanitizePromptInput, sanitizeReferenceValues } from '../utils/sanitizer';
// Constants for system behaviour
export const DEFAULT_SYSTEM_ROLE = `You are a JSON classification assistant. Respond only with a valid JSON object adhering to the specified schema.`;
diff --git a/src/main.ts b/src/main.ts
index fb5b6f1..4a67f6c 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,11 +1,11 @@
import type { TFile } from 'obsidian';
import { Plugin } from 'obsidian';
-import { CodexOAuth, isTokenExpired } from './provider/auth';
-import { ClassificationService, CommandService } from './classifier';
-import { DEFAULT_FRONTMATTER_SETTING, DEFAULT_SETTINGS, NOTICE_CATALOG } from './constants';
-import type { AutoClassifierSettings } from './settings';
-import { AutoClassifierSettingTab } from './settings';
-import type { FrontmatterField, ProviderConfig } from './types';
+import { CodexOAuth, isTokenExpired } from './ui/auth';
+import { ClassificationService } from './ui/ClassificationService';
+import { CommandService } from './ui/CommandService';
+import { DEFAULT_FRONTMATTER_SETTING, DEFAULT_SETTINGS, NOTICE_CATALOG } from './domain/constants';
+import type { AutoClassifierSettings, FrontmatterField, ProviderConfig } from './types';
+import { AutoClassifierSettingTab } from './ui/settings';
import { PluginLogger } from './shared/plugin-logger';
import { PluginNotices } from './shared/plugin-notices';
import { migrateSettings } from './shared/settings-migration';
diff --git a/src/provider/auth/index.ts b/src/provider/auth/index.ts
deleted file mode 100644
index 141652b..0000000
--- a/src/provider/auth/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-// Provider Auth module exports
-export { CodexOAuth } from './oauth';
-export { CODEX_OAUTH } from './oauth-constants';
-export {
- isTokenExpired,
- formatTokenExpiry,
- getTokenRemainingTime,
- parseJwtClaims,
- extractAccountId,
-} from './token-manager';
-export type { OAuthTokens, PKCEChallenge, TokenResponse } from './types';
diff --git a/src/settings/components/BaseSettings.ts b/src/settings/components/BaseSettings.ts
index b22acd5..cf7e9eb 100644
--- a/src/settings/components/BaseSettings.ts
+++ b/src/settings/components/BaseSettings.ts
@@ -1,64 +1,2 @@
-import type { FrontmatterField } from '../../types';
-import type AutoClassifierPlugin from '../../main';
-import { ConfigurableSettingModal } from '../modals/FrontmatterEditorModal';
-import type {
- FrontmatterActions,
- FrontmatterEditorModalProps,
- SettingsComponent,
- SettingsComponentOptions,
-} from '../../types';
-import { Setting } from './Setting';
-
-export abstract class BaseSettingsComponent implements SettingsComponent {
- protected abstract readonly options: SettingsComponentOptions;
-
- constructor(
- protected readonly plugin: AutoClassifierPlugin,
- protected readonly containerEl: HTMLElement
- ) {}
-
- abstract display(frontmatterId?: number): void;
-
- protected createFrontmatterSetting(
- containerEl: HTMLElement,
- frontmatterSetting: FrontmatterField,
- actions: FrontmatterActions,
- showDeleteButton: boolean = false
- ): void {
- const button = [
- {
- icon: 'pencil',
- tooltip: 'Edit Frontmatter',
- onClick: () => actions.onEdit(frontmatterSetting),
- },
- ];
- if (showDeleteButton) {
- button.push({
- icon: 'trash',
- tooltip: 'Delete Frontmatter',
- onClick: () => actions.onDelete(frontmatterSetting),
- });
- }
-
- Setting.create(containerEl, {
- name: frontmatterSetting.name || 'Please enter name',
- desc: `Type: ${frontmatterSetting.linkType}, Max: ${frontmatterSetting.count?.max ?? 5}, Overwrite: ${frontmatterSetting.overwrite}`,
- buttons: button,
- });
- }
-
- // Common modal creation and opening logic
- protected openEditModal(
- frontmatterSetting: FrontmatterField,
- onSave: (updatedFrontmatter: FrontmatterField) => Promise
- ): void {
- const modalProps: FrontmatterEditorModalProps = {
- frontmatterSetting: frontmatterSetting,
- options: this.options,
- onSave: onSave,
- };
-
- const modal = new ConfigurableSettingModal(this.plugin.app, modalProps);
- modal.open();
- }
-}
+// Compatibility shim — new code should import from '../../ui/settings/components/BaseSettings' directly
+export * from '../../ui/settings/components/BaseSettings';
diff --git a/src/types.ts b/src/types.ts
index 5494021..b470257 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,161 +1,3 @@
-// ==========================================
-// Consolidated Types - Centralized type definitions
-// ==========================================
-
-import type { TFile } from 'obsidian';
-import type { OAuthTokens } from './provider/auth/types';
-
-// ==========================================
-// Frontmatter Types (from frontmatter/types.ts)
-// ==========================================
-
-export type LinkType = 'WikiLink' | 'Normal';
-
-export interface Range {
- min: number;
- max: number;
-}
-
-export interface FrontmatterField {
- id: number;
- name: string;
- count: Range;
- refs: string[];
- overwrite: boolean;
- linkType: LinkType;
- customQuery: string;
-}
-
-export interface FrontMatter {
- [key: string]: string[];
-}
-
-export type ProcessFrontMatterFn = (
- file: TFile,
- fn: (frontmatter: FrontMatter) => void
-) => Promise;
-
-export interface InsertFrontMatterParams extends Pick<
- FrontmatterField,
- 'name' | 'overwrite' | 'linkType'
-> {
- file: TFile;
- value: string[];
-}
-
-// ==========================================
-// Provider Types (from api/types.ts)
-// ==========================================
-
-export interface Model {
- id: string;
- name: string;
-}
-
-export type AuthType = 'apiKey' | 'oauth';
-
-/**
- * Unified authentication type - supports both API Key and OAuth
- */
-export type ProviderAuth =
- | { type: 'apiKey'; apiKey: string }
- | { type: 'oauth'; oauth: OAuthTokens };
-
-/**
- * Provider configuration interface
- * NOTE: During migration, both old format (apiKey, oauth fields) and new format (auth field) are supported
- */
-export interface ProviderBase {
- name: string;
- baseUrl: string;
- temperature?: number;
- models: Model[];
- // New unified auth field
- auth?: ProviderAuth;
- // Legacy fields (kept for backward compatibility during migration)
- apiKey?: string;
- authType?: AuthType;
- oauth?: OAuthTokens;
-}
-
-export type ProviderConfig = ProviderBase;
-
-export interface StructuredOutput {
- output: string[];
- reliability: number;
-}
-
-export interface ProviderPreset extends Omit {
- presetId?: string;
- apiKeyUrl: string;
- apiKeyRequired: boolean;
- modelsList: string;
- popularModels: Model[];
-}
-
-/**
- * Callback for when OAuth tokens are refreshed during API calls
- */
-export type OnTokenRefreshCallback = (tokens: OAuthTokens) => Promise;
-
-export interface APIProvider {
- callAPI(
- systemRole: string,
- user_prompt: string,
- provider: ProviderConfig,
- selectedModel: string,
- temperature?: number,
- onTokenRefresh?: OnTokenRefreshCallback
- ): Promise;
-
- buildHeaders(apiKey: string): Record;
- processApiResponse(responseData: any): StructuredOutput;
-}
-
-// ==========================================
-// Settings UI Types (from ui/types.ts)
-// ==========================================
-
-export interface SettingsComponentOptions {
- showLinkType?: boolean;
- showOptions?: boolean;
- showTextArea?: boolean;
-}
-
-export interface SettingsComponent {
- display(frontmatterId?: number): void;
-}
-
-export interface FrontmatterActions {
- onEdit: (frontmatterSetting: FrontmatterField) => void;
- onDelete: (frontmatterSetting: FrontmatterField) => void;
-}
-
-export interface FrontmatterEditorModalProps {
- frontmatterSetting: FrontmatterField;
- options: SettingsComponentOptions;
- onSave: (frontmatter: FrontmatterField) => Promise;
-}
-
-/**
- * Frontmatter reference type - minimal identification info for selection/reference
- * Used in modals, dropdowns, lists etc. to identify frontmatter
- */
-export type FrontmatterRef = Pick;
-
-// ==========================================
-// Plugin Settings Type
-// ==========================================
-
-export interface AutoClassifierSettings {
- providers: ProviderConfig[];
- selectedProvider: string;
- selectedModel: string;
- frontmatter: FrontmatterField[];
- classificationRule: string;
- codexConnection?: OAuthTokens;
- plugin_notices?: { muted: Record };
-}
-
-// Re-export auth types for convenience
-export type { OAuthTokens } from './provider/auth/types';
+// Re-export all types from the new layered architecture types module
+// This file is kept as a compatibility shim — new code should import from './types/index' directly
+export * from './types/index';
diff --git a/src/types/.gitkeep b/src/types/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/provider/auth/types.ts b/src/types/auth.ts
similarity index 100%
rename from src/provider/auth/types.ts
rename to src/types/auth.ts
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..72b30c0
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,163 @@
+// ==========================================
+// Consolidated Types - Centralized type definitions
+// ==========================================
+
+import type { OAuthTokens } from './auth';
+
+// ==========================================
+// Obsidian type shims — used to avoid importing from obsidian in domain/utils layers
+// Keep in sync with the Obsidian API signatures that we depend on.
+// ==========================================
+
+/**
+ * Minimal shim for Obsidian's TFile used in pure-type positions.
+ * Do NOT add methods — only structural properties needed by domain code.
+ */
+export interface TFileShim {
+ path: string;
+ name: string;
+ basename: string;
+ extension: string;
+ parent: { path: string } | null;
+}
+
+// ==========================================
+// Frontmatter Types
+// ==========================================
+
+export type LinkType = 'WikiLink' | 'Normal';
+
+export interface Range {
+ min: number;
+ max: number;
+}
+
+export interface FrontmatterField {
+ id: number;
+ name: string;
+ count: Range;
+ refs: string[];
+ overwrite: boolean;
+ linkType: LinkType;
+ customQuery: string;
+}
+
+export interface FrontMatter {
+ [key: string]: string[];
+}
+
+// ==========================================
+// Provider Types
+// ==========================================
+
+export interface Model {
+ id: string;
+ name: string;
+}
+
+export type AuthType = 'apiKey' | 'oauth';
+
+/**
+ * Unified authentication type - supports both API Key and OAuth
+ */
+export type ProviderAuth =
+ | { type: 'apiKey'; apiKey: string }
+ | { type: 'oauth'; oauth: OAuthTokens };
+
+/**
+ * Provider configuration interface
+ * NOTE: During migration, both old format (apiKey, oauth fields) and new format (auth field) are supported
+ */
+export interface ProviderBase {
+ name: string;
+ baseUrl: string;
+ temperature?: number;
+ models: Model[];
+ // New unified auth field
+ auth?: ProviderAuth;
+ // Legacy fields (kept for backward compatibility during migration)
+ apiKey?: string;
+ authType?: AuthType;
+ oauth?: OAuthTokens;
+}
+
+export type ProviderConfig = ProviderBase;
+
+export interface StructuredOutput {
+ output: string[];
+ reliability: number;
+}
+
+export interface ProviderPreset extends Omit {
+ presetId?: string;
+ apiKeyUrl: string;
+ apiKeyRequired: boolean;
+ modelsList: string;
+ popularModels: Model[];
+}
+
+/**
+ * Callback for when OAuth tokens are refreshed during API calls
+ */
+export type OnTokenRefreshCallback = (tokens: OAuthTokens) => Promise;
+
+export interface APIProvider {
+ callAPI(
+ systemRole: string,
+ user_prompt: string,
+ provider: ProviderConfig,
+ selectedModel: string,
+ temperature?: number,
+ onTokenRefresh?: OnTokenRefreshCallback
+ ): Promise;
+
+ buildHeaders(apiKey: string): Record;
+ processApiResponse(responseData: any): StructuredOutput;
+}
+
+// ==========================================
+// Settings UI Types
+// ==========================================
+
+export interface SettingsComponentOptions {
+ showLinkType?: boolean;
+ showOptions?: boolean;
+ showTextArea?: boolean;
+}
+
+export interface SettingsComponent {
+ display(frontmatterId?: number): void;
+}
+
+export interface FrontmatterActions {
+ onEdit: (frontmatterSetting: FrontmatterField) => void;
+ onDelete: (frontmatterSetting: FrontmatterField) => void;
+}
+
+export interface FrontmatterEditorModalProps {
+ frontmatterSetting: FrontmatterField;
+ options: SettingsComponentOptions;
+ onSave: (frontmatter: FrontmatterField) => Promise;
+}
+
+/**
+ * Frontmatter reference type - minimal identification info for selection/reference
+ */
+export type FrontmatterRef = Pick;
+
+// ==========================================
+// Plugin Settings Type
+// ==========================================
+
+export interface AutoClassifierSettings {
+ providers: ProviderConfig[];
+ selectedProvider: string;
+ selectedModel: string;
+ frontmatter: FrontmatterField[];
+ classificationRule: string;
+ codexConnection?: OAuthTokens;
+ plugin_notices?: { muted: Record };
+}
+
+// Re-export auth types for convenience
+export type { OAuthTokens } from './auth';
diff --git a/src/ui/.gitkeep b/src/ui/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/classifier/ClassificationService.ts b/src/ui/ClassificationService.ts
similarity index 94%
rename from src/classifier/ClassificationService.ts
rename to src/ui/ClassificationService.ts
index a8be727..ab3ede8 100644
--- a/src/classifier/ClassificationService.ts
+++ b/src/ui/ClassificationService.ts
@@ -1,14 +1,10 @@
import type { TFile, App } from 'obsidian';
-import { COMMON_CONSTANTS } from '../constants';
-import {
- getContentWithoutFrontmatter,
- getFieldValues,
- insertToFrontMatter,
-} from '../lib/frontmatter';
-import { processAPIRequest } from '../provider';
-import { DEFAULT_SYSTEM_ROLE, getPromptTemplate } from '../provider/prompt';
-import { Notice } from '../settings/components/Notice';
+import { COMMON_CONSTANTS } from '../domain/constants';
+import { getContentWithoutFrontmatter, getFieldValues, insertToFrontMatter } from './frontmatter';
+import { processAPIRequest } from './provider-api';
+import { DEFAULT_SYSTEM_ROLE, getPromptTemplate } from '../domain/prompt';
+import { Notice } from './settings/components/Notice';
import type { PluginNotices } from '../shared/plugin-notices';
import type { FrontmatterField, FrontMatter, ProviderConfig, StructuredOutput } from '../types';
diff --git a/src/classifier/CommandService.ts b/src/ui/CommandService.ts
similarity index 100%
rename from src/classifier/CommandService.ts
rename to src/ui/CommandService.ts
diff --git a/src/provider/UnifiedProvider.ts b/src/ui/UnifiedProvider.ts
similarity index 98%
rename from src/provider/UnifiedProvider.ts
rename to src/ui/UnifiedProvider.ts
index 76cd374..a34400e 100644
--- a/src/provider/UnifiedProvider.ts
+++ b/src/ui/UnifiedProvider.ts
@@ -4,8 +4,8 @@ import {
GEMINI_STRUCTURE_OUTPUT,
OLLAMA_STRUCTURE_OUTPUT,
OPENAI_STRUCTURE_OUTPUT,
-} from '../constants';
-import { PROVIDER_NAMES } from '../lib';
+} from '../domain/constants';
+import { PROVIDER_NAMES } from '../utils/lib-utils';
import type {
APIProvider,
ProviderConfig,
@@ -13,7 +13,7 @@ import type {
OnTokenRefreshCallback,
} from '../types';
import { sendRequest, sendStreamingRequest, HttpError, type SSEEvent } from './request';
-import { CODEX_OAUTH } from './auth/oauth-constants';
+import { CODEX_OAUTH } from '../domain/auth/oauth-constants';
import { CodexOAuth } from './auth/oauth';
import { macLogger } from '../shared/mac-logger';
diff --git a/src/ui/auth/index.ts b/src/ui/auth/index.ts
new file mode 100644
index 0000000..8d15780
--- /dev/null
+++ b/src/ui/auth/index.ts
@@ -0,0 +1,12 @@
+// UI auth module exports — obsidian-dependent auth implementations
+export { CodexOAuth } from './oauth';
+// Re-export pure domain auth utilities for convenience
+export { CODEX_OAUTH } from '../../domain/auth/oauth-constants';
+export {
+ isTokenExpired,
+ formatTokenExpiry,
+ getTokenRemainingTime,
+ parseJwtClaims,
+ extractAccountId,
+} from '../../domain/auth/token-manager';
+export type { OAuthTokens, PKCEChallenge, TokenResponse } from '../../types/auth';
diff --git a/src/provider/auth/oauth.ts b/src/ui/auth/oauth.ts
similarity index 92%
rename from src/provider/auth/oauth.ts
rename to src/ui/auth/oauth.ts
index 30f7e38..909bde8 100644
--- a/src/provider/auth/oauth.ts
+++ b/src/ui/auth/oauth.ts
@@ -1,9 +1,9 @@
import { Platform, requestUrl } from 'obsidian';
-import { CODEX_OAUTH } from './oauth-constants';
-import { OAuthCallbackServer } from './oauth-server';
-import { generatePKCEChallenge, generateState } from './pkce';
-import { createTokensFromResponse, isTokenExpired } from './token-manager';
-import type { OAuthTokens, TokenResponse } from './types';
+import { CODEX_OAUTH } from '../../domain/auth/oauth-constants';
+import { OAuthCallbackServer } from '../../domain/auth/oauth-server';
+import { generatePKCEChallenge, generateState } from '../../domain/auth/pkce';
+import { createTokensFromResponse, isTokenExpired } from '../../domain/auth/token-manager';
+import type { OAuthTokens, TokenResponse } from '../../types/auth';
/**
* OAuth handler for OpenAI Codex/ChatGPT Pro authentication
diff --git a/src/lib/frontmatter.ts b/src/ui/frontmatter.ts
similarity index 89%
rename from src/lib/frontmatter.ts
rename to src/ui/frontmatter.ts
index 513bca4..452bb16 100644
--- a/src/lib/frontmatter.ts
+++ b/src/ui/frontmatter.ts
@@ -1,12 +1,17 @@
import type { MetadataCache, TFile } from 'obsidian';
import { getAllTags, getFrontMatterInfo, parseFrontMatterStringArray } from 'obsidian';
-import type {
- FrontMatter,
+import type { FrontMatter, FrontmatterField } from '../types';
+
+type ProcessFrontMatterFn = (file: TFile, fn: (frontmatter: FrontMatter) => void) => Promise;
+
+interface InsertFrontMatterParams extends Pick<
FrontmatterField,
- InsertFrontMatterParams,
- ProcessFrontMatterFn,
-} from '../types';
+ 'name' | 'overwrite' | 'linkType'
+> {
+ file: TFile;
+ value: string[];
+}
/**
* Extracts the content of a markdown file excluding frontmatter
@@ -53,10 +58,7 @@ export function getFieldValues(
/**
* Gets a frontmatter setting by ID from the settings array.
*/
-export function getFrontmatterSetting(
- id: number,
- settings: FrontmatterField[]
-): FrontmatterField {
+export function getFrontmatterSetting(id: number, settings: FrontmatterField[]): FrontmatterField {
const setting = settings?.find((f) => f.id === id);
if (!setting) {
throw new Error('Setting not found');
diff --git a/src/classifier/index.ts b/src/ui/index.ts
similarity index 100%
rename from src/classifier/index.ts
rename to src/ui/index.ts
diff --git a/src/provider/index.ts b/src/ui/provider-api.ts
similarity index 95%
rename from src/provider/index.ts
rename to src/ui/provider-api.ts
index cf46811..36d8d26 100644
--- a/src/provider/index.ts
+++ b/src/ui/provider-api.ts
@@ -1,4 +1,4 @@
-import { COMMON_CONSTANTS } from '../constants';
+import { COMMON_CONSTANTS } from '../domain/constants';
import type { APIProvider, ProviderConfig, StructuredOutput } from '../types';
import { UnifiedProvider } from './UnifiedProvider';
diff --git a/src/provider/request.ts b/src/ui/request.ts
similarity index 99%
rename from src/provider/request.ts
rename to src/ui/request.ts
index 2d97a1c..69c6fcf 100644
--- a/src/provider/request.ts
+++ b/src/ui/request.ts
@@ -130,8 +130,6 @@ async function sendStreamingRequestViaNode(
body: Record,
timeoutMs: number = STREAM_TIMEOUT_MS
): Promise<{ status: number; text: string }> {
- // https is imported at module level for proper testability
-
const parsedUrl = new URL(url);
const payload = JSON.stringify(body);
const payloadLength = Buffer.byteLength(payload);
diff --git a/src/settings/ApiSection.ts b/src/ui/settings/ApiSection.ts
similarity index 96%
rename from src/settings/ApiSection.ts
rename to src/ui/settings/ApiSection.ts
index 16da814..860653b 100644
--- a/src/settings/ApiSection.ts
+++ b/src/ui/settings/ApiSection.ts
@@ -1,6 +1,6 @@
import { App } from 'obsidian';
-import type AutoClassifierPlugin from '../main';
+import type AutoClassifierPlugin from '../../main';
import { ClassificationRuleSection } from './ClassificationRuleSection';
import { ModelSection } from './ModelSection';
import { ProviderSection } from './ProviderSection';
diff --git a/src/settings/ClassificationRuleSection.ts b/src/ui/settings/ClassificationRuleSection.ts
similarity index 92%
rename from src/settings/ClassificationRuleSection.ts
rename to src/ui/settings/ClassificationRuleSection.ts
index fe4ec57..5fb957c 100644
--- a/src/settings/ClassificationRuleSection.ts
+++ b/src/ui/settings/ClassificationRuleSection.ts
@@ -1,7 +1,7 @@
import { TextAreaComponent } from 'obsidian';
-import { DEFAULT_TASK_TEMPLATE } from '../constants';
-import type AutoClassifierPlugin from '../main';
+import { DEFAULT_TASK_TEMPLATE } from '../../domain/constants';
+import type AutoClassifierPlugin from '../../main';
import { Setting } from './components/Setting';
export class ClassificationRuleSection {
diff --git a/src/settings/FrontmatterSection.ts b/src/ui/settings/FrontmatterSection.ts
similarity index 58%
rename from src/settings/FrontmatterSection.ts
rename to src/ui/settings/FrontmatterSection.ts
index 432ba18..bb9e461 100644
--- a/src/settings/FrontmatterSection.ts
+++ b/src/ui/settings/FrontmatterSection.ts
@@ -1,4 +1,4 @@
-import type { FrontmatterActions, FrontmatterField, SettingsComponentOptions } from '../types';
+import type { FrontmatterActions, FrontmatterField, SettingsComponentOptions } from '../../types';
import { BaseSettingsComponent } from './components/BaseSettings';
export class Frontmatter extends BaseSettingsComponent {
@@ -13,17 +13,17 @@ export class Frontmatter extends BaseSettingsComponent {
const filteredFrontmatter = frontmatter.filter((frontmatter) => frontmatter.id !== 0);
this.containerEl.empty();
- filteredFrontmatter.forEach((frontmatter: FrontmatterField) => {
+ filteredFrontmatter.forEach((frontmatter: FrontmatterField) => {
const actions: FrontmatterActions = {
- onEdit: (setting: FrontmatterField) => this.handleEdit(setting),
- onDelete: (setting: FrontmatterField) => this.handleDelete(setting),
+ onEdit: (setting: FrontmatterField) => this.handleEdit(setting),
+ onDelete: (setting: FrontmatterField) => this.handleDelete(setting),
};
this.createFrontmatterSetting(this.containerEl, frontmatter, actions, true);
});
}
- private handleEdit(frontmatterSetting: FrontmatterField): void {
+ private handleEdit(frontmatterSetting: FrontmatterField): void {
this.openEditModal(frontmatterSetting, async (updatedFrontmatter) => {
// Register command for the updated frontmatter
this.plugin.registerCommand(
@@ -36,15 +36,17 @@ export class Frontmatter extends BaseSettingsComponent {
});
}
- private async handleDelete(frontmatterSetting: FrontmatterField): Promise {
- const confirmed = confirm(`Are you sure you want to delete "${frontmatterSetting.name}" frontmatter?`);
+ private async handleDelete(frontmatterSetting: FrontmatterField): Promise {
+ const confirmed = confirm(
+ `Are you sure you want to delete "${frontmatterSetting.name}" frontmatter?`
+ );
if (!confirmed) {
return;
}
- this.plugin.settings.frontmatter = this.plugin.settings.frontmatter.filter(
- (f: FrontmatterField) => f.id !== frontmatterSetting.id
- );
+ this.plugin.settings.frontmatter = this.plugin.settings.frontmatter.filter(
+ (f: FrontmatterField) => f.id !== frontmatterSetting.id
+ );
await this.plugin.saveSettings();
this.display();
diff --git a/src/settings/ModelSection.ts b/src/ui/settings/ModelSection.ts
similarity index 96%
rename from src/settings/ModelSection.ts
rename to src/ui/settings/ModelSection.ts
index 4c82b9b..12a08a7 100644
--- a/src/settings/ModelSection.ts
+++ b/src/ui/settings/ModelSection.ts
@@ -1,8 +1,8 @@
import type { App } from 'obsidian';
-import type AutoClassifierPlugin from '../main';
-import { testModel } from '../provider';
-import type { Model, ProviderConfig } from '../types';
+import type AutoClassifierPlugin from '../../main';
+import { testModel } from '../provider-api';
+import type { Model, ProviderConfig } from '../../types';
import { Setting } from './components/Setting';
import { ModelModal, type ModelModalProps } from './modals/ModelModal';
diff --git a/src/settings/ProviderSection.ts b/src/ui/settings/ProviderSection.ts
similarity index 94%
rename from src/settings/ProviderSection.ts
rename to src/ui/settings/ProviderSection.ts
index 3b9f4ec..c860d77 100644
--- a/src/settings/ProviderSection.ts
+++ b/src/ui/settings/ProviderSection.ts
@@ -1,8 +1,8 @@
import type { App } from 'obsidian';
-import type AutoClassifierPlugin from '../main';
-import { formatTokenExpiry, isTokenExpired } from '../provider/auth';
-import type { ProviderConfig } from '../types';
+import type AutoClassifierPlugin from '../../main';
+import { formatTokenExpiry, isTokenExpired } from '../auth';
+import type { ProviderConfig } from '../../types';
import { Setting } from './components/Setting';
import { ProviderModal } from './modals/ProviderModal';
diff --git a/src/settings/TagSection.ts b/src/ui/settings/TagSection.ts
similarity index 79%
rename from src/settings/TagSection.ts
rename to src/ui/settings/TagSection.ts
index ae489c3..f7af6d5 100644
--- a/src/settings/TagSection.ts
+++ b/src/ui/settings/TagSection.ts
@@ -1,4 +1,4 @@
-import type { FrontmatterActions, FrontmatterField, SettingsComponentOptions } from '../types';
+import type { FrontmatterActions, FrontmatterField, SettingsComponentOptions } from '../../types';
import { BaseSettingsComponent } from './components/BaseSettings';
export class Tag extends BaseSettingsComponent {
@@ -16,15 +16,15 @@ export class Tag extends BaseSettingsComponent {
return;
}
- const actions: FrontmatterActions = {
- onEdit: (setting: FrontmatterField) => this.handleEdit(setting),
+ const actions: FrontmatterActions = {
+ onEdit: (setting: FrontmatterField) => this.handleEdit(setting),
onDelete: () => {},
};
this.createFrontmatterSetting(this.containerEl, tagSetting, actions, false);
}
- private handleEdit(frontmatterSetting: FrontmatterField): void {
+ private handleEdit(frontmatterSetting: FrontmatterField): void {
this.openEditModal(frontmatterSetting, async (updatedFrontmatter) => {
const tagSettingIndex = this.plugin.settings.frontmatter.findIndex((f) => f.id === 0);
if (tagSettingIndex !== -1) {
diff --git a/src/ui/settings/components/BaseSettings.ts b/src/ui/settings/components/BaseSettings.ts
new file mode 100644
index 0000000..ed6d23b
--- /dev/null
+++ b/src/ui/settings/components/BaseSettings.ts
@@ -0,0 +1,64 @@
+import type { FrontmatterField } from '../../../types';
+import type AutoClassifierPlugin from '../../../main';
+import { ConfigurableSettingModal } from '../modals/FrontmatterEditorModal';
+import type {
+ FrontmatterActions,
+ FrontmatterEditorModalProps,
+ SettingsComponent,
+ SettingsComponentOptions,
+} from '../../../types';
+import { Setting } from './Setting';
+
+export abstract class BaseSettingsComponent implements SettingsComponent {
+ protected abstract readonly options: SettingsComponentOptions;
+
+ constructor(
+ protected readonly plugin: AutoClassifierPlugin,
+ protected readonly containerEl: HTMLElement
+ ) {}
+
+ abstract display(frontmatterId?: number): void;
+
+ protected createFrontmatterSetting(
+ containerEl: HTMLElement,
+ frontmatterSetting: FrontmatterField,
+ actions: FrontmatterActions,
+ showDeleteButton: boolean = false
+ ): void {
+ const button = [
+ {
+ icon: 'pencil',
+ tooltip: 'Edit Frontmatter',
+ onClick: () => actions.onEdit(frontmatterSetting),
+ },
+ ];
+ if (showDeleteButton) {
+ button.push({
+ icon: 'trash',
+ tooltip: 'Delete Frontmatter',
+ onClick: () => actions.onDelete(frontmatterSetting),
+ });
+ }
+
+ Setting.create(containerEl, {
+ name: frontmatterSetting.name || 'Please enter name',
+ desc: `Type: ${frontmatterSetting.linkType}, Max: ${frontmatterSetting.count?.max ?? 5}, Overwrite: ${frontmatterSetting.overwrite}`,
+ buttons: button,
+ });
+ }
+
+ // Common modal creation and opening logic
+ protected openEditModal(
+ frontmatterSetting: FrontmatterField,
+ onSave: (updatedFrontmatter: FrontmatterField) => Promise
+ ): void {
+ const modalProps: FrontmatterEditorModalProps = {
+ frontmatterSetting: frontmatterSetting,
+ options: this.options,
+ onSave: onSave,
+ };
+
+ const modal = new ConfigurableSettingModal(this.plugin.app, modalProps);
+ modal.open();
+ }
+}
diff --git a/src/settings/components/ModalAccessibilityHelper.ts b/src/ui/settings/components/ModalAccessibilityHelper.ts
similarity index 94%
rename from src/settings/components/ModalAccessibilityHelper.ts
rename to src/ui/settings/components/ModalAccessibilityHelper.ts
index 7d1a144..d26712f 100644
--- a/src/settings/components/ModalAccessibilityHelper.ts
+++ b/src/ui/settings/components/ModalAccessibilityHelper.ts
@@ -38,9 +38,7 @@ export class ModalAccessibilityHelper {
*/
focusFirstInput(contentEl: HTMLElement): void {
setTimeout(() => {
- const firstInput = contentEl.querySelector(
- 'input, select, button, textarea'
- ) as HTMLElement;
+ const firstInput = contentEl.querySelector('input, select, button, textarea') as HTMLElement;
if (firstInput) {
firstInput.focus();
}
diff --git a/src/settings/components/Notice.ts b/src/ui/settings/components/Notice.ts
similarity index 100%
rename from src/settings/components/Notice.ts
rename to src/ui/settings/components/Notice.ts
diff --git a/src/settings/components/Setting.ts b/src/ui/settings/components/Setting.ts
similarity index 98%
rename from src/settings/components/Setting.ts
rename to src/ui/settings/components/Setting.ts
index ad6ebb7..657bfe1 100644
--- a/src/settings/components/Setting.ts
+++ b/src/ui/settings/components/Setting.ts
@@ -1,4 +1,9 @@
-import { ExtraButtonComponent, Setting as ObsidianSetting, TextAreaComponent, TextComponent } from 'obsidian';
+import {
+ ExtraButtonComponent,
+ Setting as ObsidianSetting,
+ TextAreaComponent,
+ TextComponent,
+} from 'obsidian';
export interface ButtonProps {
text?: string;
diff --git a/src/settings/components/WikiLinkSelector.ts b/src/ui/settings/components/WikiLinkSelector.ts
similarity index 100%
rename from src/settings/components/WikiLinkSelector.ts
rename to src/ui/settings/components/WikiLinkSelector.ts
diff --git a/src/settings/index.ts b/src/ui/settings/index.ts
similarity index 90%
rename from src/settings/index.ts
rename to src/ui/settings/index.ts
index e09f96b..7412786 100644
--- a/src/settings/index.ts
+++ b/src/ui/settings/index.ts
@@ -1,9 +1,9 @@
-import type AutoClassifierPlugin from 'main';
+import type AutoClassifierPlugin from '../../main';
import { PluginSettingTab } from 'obsidian';
-import type { FrontmatterField } from '../types';
-import { generateId } from '../lib';
-import { DEFAULT_FRONTMATTER_SETTING } from '../constants';
+import type { FrontmatterField } from '../../types';
+import { generateId } from '../../utils/lib-utils';
+import { DEFAULT_FRONTMATTER_SETTING } from '../../domain/constants';
import { Setting } from './components/Setting';
import { Api } from './ApiSection';
import { Frontmatter } from './FrontmatterSection';
@@ -84,7 +84,7 @@ export class AutoClassifierSettingTab extends PluginSettingTab {
}
}
-export type { AutoClassifierSettings } from '../types';
+export type { AutoClassifierSettings } from '../../types';
export * from './components/WikiLinkSelector';
export * from './modals/FrontmatterEditorModal';
export * from './modals/FrontmatterSelectModal';
diff --git a/src/settings/modals/FrontmatterEditorModal.ts b/src/ui/settings/modals/FrontmatterEditorModal.ts
similarity index 96%
rename from src/settings/modals/FrontmatterEditorModal.ts
rename to src/ui/settings/modals/FrontmatterEditorModal.ts
index 78696dc..bfe1512 100644
--- a/src/settings/modals/FrontmatterEditorModal.ts
+++ b/src/ui/settings/modals/FrontmatterEditorModal.ts
@@ -1,8 +1,8 @@
import type { App, TextAreaComponent } from 'obsidian';
import { Modal, Setting, TextAreaComponent as ObsidianTextArea } from 'obsidian';
-import { deepCloneFrontmatterField } from '../../lib/frontmatter';
-import type { FrontmatterEditorModalProps, FrontmatterField, LinkType } from '../../types';
+import { deepCloneFrontmatterField } from '../../frontmatter';
+import type { FrontmatterEditorModalProps, FrontmatterField, LinkType } from '../../../types';
import { Notice } from '../components/Notice';
import { Setting as CommonSetting } from '../components/Setting';
import { WikiLinkSelector } from '../components/WikiLinkSelector';
@@ -119,9 +119,7 @@ export class ConfigurableSettingModal extends Modal {
const wikiLinkSelector = new WikiLinkSelector(this.app);
wikiLinkSelector.openFileSelector((selectedLink) => {
const formattedLink =
- this.localState.linkType === 'WikiLink'
- ? `[[${selectedLink}]]`
- : selectedLink;
+ this.localState.linkType === 'WikiLink' ? `[[${selectedLink}]]` : selectedLink;
const currentOptions = this.localState.refs || [];
this.localState.refs = [...currentOptions, formattedLink];
this.updateOptionsTextarea();
diff --git a/src/settings/modals/FrontmatterSelectModal.ts b/src/ui/settings/modals/FrontmatterSelectModal.ts
similarity index 92%
rename from src/settings/modals/FrontmatterSelectModal.ts
rename to src/ui/settings/modals/FrontmatterSelectModal.ts
index 8239a09..e5b15a4 100644
--- a/src/settings/modals/FrontmatterSelectModal.ts
+++ b/src/ui/settings/modals/FrontmatterSelectModal.ts
@@ -1,7 +1,7 @@
import type { App } from 'obsidian';
import { FuzzySuggestModal } from 'obsidian';
-import type { FrontmatterRef } from '../../types';
+import type { FrontmatterRef } from '../../../types';
export class FrontmatterSelectModal extends FuzzySuggestModal {
constructor(
diff --git a/src/settings/modals/ModelModal.ts b/src/ui/settings/modals/ModelModal.ts
similarity index 98%
rename from src/settings/modals/ModelModal.ts
rename to src/ui/settings/modals/ModelModal.ts
index fdd0b66..b28860e 100644
--- a/src/settings/modals/ModelModal.ts
+++ b/src/ui/settings/modals/ModelModal.ts
@@ -1,11 +1,11 @@
import type { App } from 'obsidian';
import { ButtonComponent, Modal } from 'obsidian';
-import { getProviderPresets } from '../../lib';
-import type { Model, ProviderConfig, ProviderPreset } from '../../types';
+import { getProviderPresets } from '../../../utils/lib-utils';
+import type { Model, ProviderConfig, ProviderPreset } from '../../../types';
import { ModalAccessibilityHelper } from '../components/ModalAccessibilityHelper';
import { Notice } from '../components/Notice';
-import type { PluginNotices } from '../../shared/plugin-notices';
+import type { PluginNotices } from '../../../shared/plugin-notices';
import type { DropdownOption } from '../components/Setting';
import { Setting as CommonSetting } from '../components/Setting';
diff --git a/src/settings/modals/ProviderModal.ts b/src/ui/settings/modals/ProviderModal.ts
similarity index 96%
rename from src/settings/modals/ProviderModal.ts
rename to src/ui/settings/modals/ProviderModal.ts
index d452c7d..e7953d1 100644
--- a/src/settings/modals/ProviderModal.ts
+++ b/src/ui/settings/modals/ProviderModal.ts
@@ -1,12 +1,12 @@
import type { App } from 'obsidian';
import { ButtonComponent, Modal, Platform } from 'obsidian';
-import { getProviderPreset, getProviderPresets } from '../../lib';
-import { CodexOAuth, formatTokenExpiry, isTokenExpired, CODEX_OAUTH } from '../../provider/auth';
-import type { OAuthTokens, ProviderConfig } from '../../types';
+import { getProviderPreset, getProviderPresets } from '../../../utils/lib-utils';
+import { CodexOAuth, formatTokenExpiry, isTokenExpired, CODEX_OAUTH } from '../../auth';
+import type { OAuthTokens, ProviderConfig } from '../../../types';
import { ModalAccessibilityHelper } from '../components/ModalAccessibilityHelper';
import { Notice } from '../components/Notice';
-import type { PluginNotices } from '../../shared/plugin-notices';
+import type { PluginNotices } from '../../../shared/plugin-notices';
import { Setting as CommonSetting, type DropdownOption } from '../components/Setting';
export class ProviderModal extends Modal {
@@ -207,8 +207,7 @@ export class ProviderModal extends Modal {
infoEl.style.marginBottom = '16px';
infoEl.style.paddingLeft = '16px';
infoEl.style.color = 'var(--text-muted)';
- infoEl.style.fontSize = '0.85em';
- infoEl.innerHTML = 'Requires ChatGPT Pro subscription';
+ infoEl.textContent = 'Requires ChatGPT Pro subscription';
}
}
diff --git a/src/settings/modals/WikiLinkSuggestModal.ts b/src/ui/settings/modals/WikiLinkSuggestModal.ts
similarity index 100%
rename from src/settings/modals/WikiLinkSuggestModal.ts
rename to src/ui/settings/modals/WikiLinkSuggestModal.ts
diff --git a/src/utils/.gitkeep b/src/utils/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/lib/ErrorHandler.ts b/src/utils/ErrorHandler.ts
similarity index 100%
rename from src/lib/ErrorHandler.ts
rename to src/utils/ErrorHandler.ts
diff --git a/src/lib/index.ts b/src/utils/lib-utils.ts
similarity index 96%
rename from src/lib/index.ts
rename to src/utils/lib-utils.ts
index 6753cb1..ea251e1 100644
--- a/src/lib/index.ts
+++ b/src/utils/lib-utils.ts
@@ -1,5 +1,5 @@
import type { ProviderPreset } from '../types';
-import providerPresetsData from '../provider/presets.json';
+import providerPresetsData from '../domain/presets.json';
export const PROVIDER_NAMES = {
OPENAI: providerPresetsData.openai.name,
diff --git a/src/lib/sanitizer.ts b/src/utils/sanitizer.ts
similarity index 100%
rename from src/lib/sanitizer.ts
rename to src/utils/sanitizer.ts
diff --git a/tsconfig.json b/tsconfig.json
index dfc51f9..c469b0d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -4,14 +4,15 @@
"outDir": "dist",
"paths": {
"main": ["src/main"],
- "provider": ["src/provider/index"],
- "provider/*": ["src/provider/*"],
- "lib": ["src/lib/index"],
- "lib/*": ["src/lib/*"],
- "settings": ["src/settings/index"],
- "settings/*": ["src/settings/*"],
- "classifier": ["src/classifier/index"],
- "classifier/*": ["src/classifier/*"]
+ "domain": ["src/domain"],
+ "domain/*": ["src/domain/*"],
+ "ui": ["src/ui"],
+ "ui/*": ["src/ui/*"],
+ "types": ["src/types"],
+ "types/*": ["src/types/*"],
+ "utils": ["src/utils"],
+ "utils/*": ["src/utils/*"],
+ "shared/*": ["src/shared/*"]
},
"inlineSourceMap": true,
"inlineSources": true,
diff --git a/vitest.config.ts b/vitest.config.ts
index d2d2cef..53018ce 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -17,13 +17,40 @@ export default defineConfig({
},
},
resolve: {
- alias: {
- obsidian: path.resolve(__dirname, '__mocks__/obsidian.ts'),
- main: path.resolve(__dirname, 'src/main'),
- provider: path.resolve(__dirname, 'src/provider'),
- lib: path.resolve(__dirname, 'src/lib'),
- settings: path.resolve(__dirname, 'src/settings'),
- classifier: path.resolve(__dirname, 'src/classifier'),
- },
+ alias: [
+ // Obsidian mock
+ { find: 'obsidian', replacement: path.resolve(__dirname, '__mocks__/obsidian.ts') },
+ // Main entry
+ { find: 'main', replacement: path.resolve(__dirname, 'src/main') },
+ // New layered architecture aliases
+ { find: 'domain', replacement: path.resolve(__dirname, 'src/domain') },
+ { find: 'ui', replacement: path.resolve(__dirname, 'src/ui') },
+ { find: 'types', replacement: path.resolve(__dirname, 'src/types') },
+ { find: 'utils', replacement: path.resolve(__dirname, 'src/utils') },
+ // Legacy aliases — map old paths to new locations for test backward compat
+ // More specific paths must come before less specific ones
+ { find: 'provider/UnifiedProvider', replacement: path.resolve(__dirname, 'src/ui/UnifiedProvider') },
+ { find: 'provider/request', replacement: path.resolve(__dirname, 'src/ui/request') },
+ { find: 'provider/prompt', replacement: path.resolve(__dirname, 'src/domain/prompt') },
+ { find: 'provider/auth/oauth', replacement: path.resolve(__dirname, 'src/ui/auth/oauth') },
+ { find: 'provider/auth', replacement: path.resolve(__dirname, 'src/ui/auth/index') },
+ { find: 'provider', replacement: path.resolve(__dirname, 'src/ui/provider-api') },
+ { find: 'lib/frontmatter', replacement: path.resolve(__dirname, 'src/ui/frontmatter') },
+ { find: 'lib/sanitizer', replacement: path.resolve(__dirname, 'src/utils/sanitizer') },
+ { find: 'lib/ErrorHandler', replacement: path.resolve(__dirname, 'src/utils/ErrorHandler') },
+ { find: 'lib', replacement: path.resolve(__dirname, 'src/utils/lib-utils') },
+ { find: 'settings/components/Notice', replacement: path.resolve(__dirname, 'src/ui/settings/components/Notice') },
+ { find: 'settings/modals/ProviderModal', replacement: path.resolve(__dirname, 'src/ui/settings/modals/ProviderModal') },
+ { find: 'settings/modals/ModelModal', replacement: path.resolve(__dirname, 'src/ui/settings/modals/ModelModal') },
+ { find: 'settings/ApiSection', replacement: path.resolve(__dirname, 'src/ui/settings/ApiSection') },
+ { find: 'settings/TagSection', replacement: path.resolve(__dirname, 'src/ui/settings/TagSection') },
+ { find: 'settings/FrontmatterSection', replacement: path.resolve(__dirname, 'src/ui/settings/FrontmatterSection') },
+ { find: 'settings/ProviderSection', replacement: path.resolve(__dirname, 'src/ui/settings/ProviderSection') },
+ { find: 'settings/ModelSection', replacement: path.resolve(__dirname, 'src/ui/settings/ModelSection') },
+ { find: 'settings', replacement: path.resolve(__dirname, 'src/ui/settings/index') },
+ { find: 'classifier/ClassificationService', replacement: path.resolve(__dirname, 'src/ui/ClassificationService') },
+ { find: 'classifier/CommandService', replacement: path.resolve(__dirname, 'src/ui/CommandService') },
+ { find: 'classifier', replacement: path.resolve(__dirname, 'src/ui/ClassificationService') },
+ ],
},
})