feat: add task comments feature with CRUD operations
- Introduced a new `task_comments` table in the database schema. - Implemented task comments API endpoints for listing, creating, and deleting comments. - Enhanced the task detail dialog to display comments and allow users to add new comments. - Updated task row component to handle click events for viewing task details. - Added a theme provider to manage light/dark mode across the application. - Refactored Milkdown editor to use Crepe for improved markdown editing experience. - Updated global styles to accommodate new editor and theme changes. - Enhanced task filtering and sorting functionality in the tasks page.
This commit is contained in:
26
src/main/ai/copilot.ts
Normal file
26
src/main/ai/copilot.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { registerProvider, type AIProvider } from './provider';
|
||||
|
||||
let token: string | null = null;
|
||||
|
||||
const copilotProvider: AIProvider = {
|
||||
name: 'copilot',
|
||||
displayName: 'GitHub Copilot',
|
||||
|
||||
async initialize(t: string): Promise<boolean> {
|
||||
token = t;
|
||||
// Actual GitHub Copilot SDK client creation will be added in US-019.
|
||||
// For now, having a token means the provider is ready.
|
||||
return true;
|
||||
},
|
||||
|
||||
isReady(): boolean {
|
||||
return token !== null;
|
||||
},
|
||||
};
|
||||
|
||||
/** Get the raw Copilot token (used by future chat/completion calls). */
|
||||
export function getCopilotToken(): string | null {
|
||||
return token;
|
||||
}
|
||||
|
||||
registerProvider(copilotProvider);
|
||||
80
src/main/ai/provider.ts
Normal file
80
src/main/ai/provider.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { getStore } from '../store';
|
||||
import { getToken, setToken as storeToken } from './token';
|
||||
|
||||
export interface AIProvider {
|
||||
/** Internal key, e.g. 'copilot', 'openai', 'anthropic' */
|
||||
name: string;
|
||||
/** Human-readable label shown in Settings UI */
|
||||
displayName: string;
|
||||
/** Initialize with a token. Returns true if the provider is ready. */
|
||||
initialize(token: string): Promise<boolean>;
|
||||
/** Whether the provider is initialized and ready to handle requests. */
|
||||
isReady(): boolean;
|
||||
}
|
||||
|
||||
const providers = new Map<string, AIProvider>();
|
||||
let activeProvider: AIProvider | null = null;
|
||||
|
||||
/** Register a provider implementation. Call at import time. */
|
||||
export function registerProvider(provider: AIProvider): void {
|
||||
providers.set(provider.name, provider);
|
||||
}
|
||||
|
||||
/** Get the currently active provider (may be null if none configured). */
|
||||
export function getActiveProvider(): AIProvider | null {
|
||||
return activeProvider;
|
||||
}
|
||||
|
||||
/** Get the active provider's name from electron-store. */
|
||||
export function getActiveProviderName(): string {
|
||||
return getStore().get('aiProvider');
|
||||
}
|
||||
|
||||
/** Switch to a different registered provider. */
|
||||
export function setActiveProviderName(name: string): void {
|
||||
const provider = providers.get(name);
|
||||
if (!provider) throw new Error(`Unknown AI provider: ${name}`);
|
||||
activeProvider = provider;
|
||||
getStore().set('aiProvider', name);
|
||||
}
|
||||
|
||||
/** Store token for the active provider and re-initialize it. */
|
||||
export async function saveTokenAndInit(token: string): Promise<void> {
|
||||
const name = getActiveProviderName();
|
||||
await storeToken(name, token);
|
||||
const provider = providers.get(name);
|
||||
if (provider) {
|
||||
await provider.initialize(token);
|
||||
activeProvider = provider;
|
||||
}
|
||||
}
|
||||
|
||||
/** Check whether the active provider has a stored token. */
|
||||
export async function hasActiveToken(): Promise<boolean> {
|
||||
const name = getActiveProviderName();
|
||||
const token = await getToken(name);
|
||||
return token !== null && token.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the AI subsystem on app startup.
|
||||
* Reads the active provider from settings, loads its token from keychain,
|
||||
* and calls provider.initialize() if a token exists.
|
||||
*/
|
||||
export async function initAI(): Promise<void> {
|
||||
const name = getActiveProviderName();
|
||||
const provider = providers.get(name);
|
||||
if (!provider) {
|
||||
console.log(`[AI] No provider registered for "${name}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await getToken(name);
|
||||
if (token) {
|
||||
const ready = await provider.initialize(token);
|
||||
activeProvider = provider;
|
||||
console.log(`[AI] Provider "${provider.displayName}" initialized: ready=${ready}`);
|
||||
} else {
|
||||
console.log(`[AI] No token stored for provider "${provider.displayName}"`);
|
||||
}
|
||||
}
|
||||
114
src/main/ai/token.ts
Normal file
114
src/main/ai/token.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { safeStorage } from 'electron';
|
||||
import { getStore } from '../store';
|
||||
|
||||
/**
|
||||
* Token storage with three-tier fallback:
|
||||
* 1. OS keychain via keytar (best — encrypted, per-user)
|
||||
* 2. Electron safeStorage + electron-store (encrypted at rest)
|
||||
* 3. Plain electron-store (last resort — e.g. WSL with no keyring)
|
||||
*/
|
||||
|
||||
let keytar: typeof import('keytar') | null = null;
|
||||
let keytarFailed = false;
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
keytar = require('keytar') as typeof import('keytar');
|
||||
} catch {
|
||||
keytarFailed = true;
|
||||
console.log('[Token] keytar native module unavailable');
|
||||
}
|
||||
|
||||
function useKeytar(): boolean {
|
||||
return keytar !== null && !keytarFailed;
|
||||
}
|
||||
|
||||
function canUseSafeStorage(): boolean {
|
||||
try {
|
||||
return safeStorage.isEncryptionAvailable();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const SERVICE_NAME = 'adiuva';
|
||||
|
||||
// --- electron-store helpers (with optional safeStorage encryption) ---
|
||||
|
||||
function readFromStore(providerName: string): string | null {
|
||||
const tokens = getStore().get('encryptedTokens');
|
||||
const stored = tokens[providerName];
|
||||
if (!stored) return null;
|
||||
|
||||
if (canUseSafeStorage()) {
|
||||
try {
|
||||
return safeStorage.decryptString(Buffer.from(stored, 'base64'));
|
||||
} catch {
|
||||
// Stored value might be plaintext from a previous fallback
|
||||
return stored;
|
||||
}
|
||||
}
|
||||
// No encryption available — value is stored as plaintext
|
||||
return stored;
|
||||
}
|
||||
|
||||
function writeToStore(providerName: string, token: string): void {
|
||||
let value: string;
|
||||
if (canUseSafeStorage()) {
|
||||
value = safeStorage.encryptString(token).toString('base64');
|
||||
} else {
|
||||
// Last resort: store plaintext (WSL with no keyring)
|
||||
value = token;
|
||||
}
|
||||
const tokens = getStore().get('encryptedTokens');
|
||||
getStore().set('encryptedTokens', { ...tokens, [providerName]: value });
|
||||
}
|
||||
|
||||
function removeFromStore(providerName: string): void {
|
||||
const tokens = getStore().get('encryptedTokens');
|
||||
const { [providerName]: _, ...rest } = tokens;
|
||||
getStore().set('encryptedTokens', rest);
|
||||
}
|
||||
|
||||
// --- public API ---
|
||||
|
||||
/** Read a stored token for the given provider. */
|
||||
export async function getToken(providerName: string): Promise<string | null> {
|
||||
if (useKeytar()) {
|
||||
try {
|
||||
return await keytar!.getPassword(SERVICE_NAME, providerName);
|
||||
} catch (err) {
|
||||
console.log('[Token] keytar runtime error, falling back:', (err as Error).message);
|
||||
keytarFailed = true;
|
||||
}
|
||||
}
|
||||
return readFromStore(providerName);
|
||||
}
|
||||
|
||||
/** Store a token for the given provider. */
|
||||
export async function setToken(providerName: string, token: string): Promise<void> {
|
||||
if (useKeytar()) {
|
||||
try {
|
||||
await keytar!.setPassword(SERVICE_NAME, providerName, token);
|
||||
return;
|
||||
} catch (err) {
|
||||
console.log('[Token] keytar runtime error, falling back:', (err as Error).message);
|
||||
keytarFailed = true;
|
||||
}
|
||||
}
|
||||
writeToStore(providerName, token);
|
||||
}
|
||||
|
||||
/** Delete a stored token for the given provider. */
|
||||
export async function deleteToken(providerName: string): Promise<boolean> {
|
||||
if (useKeytar()) {
|
||||
try {
|
||||
return await keytar!.deletePassword(SERVICE_NAME, providerName);
|
||||
} catch (err) {
|
||||
console.log('[Token] keytar runtime error, falling back:', (err as Error).message);
|
||||
keytarFailed = true;
|
||||
}
|
||||
}
|
||||
removeFromStore(providerName);
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user