style: auto-fix import/export sorting and formatting

Ran pnpm lint:fix to resolve 220 auto-fixable lint issues.
All changes are import/export reordering — no logic changes.
This commit is contained in:
iliya 2026-03-16 20:48:42 +02:00
parent f5d2b7e14f
commit 51f8f3545c
124 changed files with 672 additions and 552 deletions

View file

@ -16,17 +16,17 @@
// On Windows this saturates all threads, blocking the event loop. // On Windows this saturates all threads, blocking the event loop.
process.env.UV_THREADPOOL_SIZE ??= '16'; process.env.UV_THREADPOOL_SIZE ??= '16';
import { CrossTeamService } from '@main/services/team/CrossTeamService'; import { JsonScheduleRepository } from '@main/services/schedule/JsonScheduleRepository';
import { TeamConfigReader } from '@main/services/team/TeamConfigReader'; import { ScheduledTaskExecutor } from '@main/services/schedule/ScheduledTaskExecutor';
import { TeamInboxWriter } from '@main/services/team/TeamInboxWriter'; import { SchedulerService } from '@main/services/schedule/SchedulerService';
import { ChangeExtractorService } from '@main/services/team/ChangeExtractorService'; import { ChangeExtractorService } from '@main/services/team/ChangeExtractorService';
import { CrossTeamService } from '@main/services/team/CrossTeamService';
import { FileContentResolver } from '@main/services/team/FileContentResolver'; import { FileContentResolver } from '@main/services/team/FileContentResolver';
import { GitDiffFallback } from '@main/services/team/GitDiffFallback'; import { GitDiffFallback } from '@main/services/team/GitDiffFallback';
import { ReviewApplierService } from '@main/services/team/ReviewApplierService'; import { ReviewApplierService } from '@main/services/team/ReviewApplierService';
import { TeamBackupService } from '@main/services/team/TeamBackupService'; import { TeamBackupService } from '@main/services/team/TeamBackupService';
import { JsonScheduleRepository } from '@main/services/schedule/JsonScheduleRepository'; import { TeamConfigReader } from '@main/services/team/TeamConfigReader';
import { ScheduledTaskExecutor } from '@main/services/schedule/ScheduledTaskExecutor'; import { TeamInboxWriter } from '@main/services/team/TeamInboxWriter';
import { SchedulerService } from '@main/services/schedule/SchedulerService';
import { import {
CONTEXT_CHANGED, CONTEXT_CHANGED,
SCHEDULE_CHANGE, SCHEDULE_CHANGE,
@ -51,16 +51,32 @@ import { existsSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { cleanupEditorState, setEditorMainWindow } from './ipc/editor'; import { cleanupEditorState, setEditorMainWindow } from './ipc/editor';
import { setReviewMainWindow } from './ipc/review';
import { initializeIpcHandlers, removeIpcHandlers } from './ipc/handlers'; import { initializeIpcHandlers, removeIpcHandlers } from './ipc/handlers';
import { setReviewMainWindow } from './ipc/review';
import {
ApiKeyService,
ExtensionFacadeService,
GlamaMcpEnrichmentService,
McpCatalogAggregator,
McpHealthDiagnosticsService,
McpInstallationStateService,
McpInstallService,
OfficialMcpRegistryService,
PluginCatalogService,
PluginInstallationStateService,
PluginInstallService,
SkillsCatalogService,
SkillsMutationService,
SkillsWatcherService,
} from './services/extensions';
import { startEventLoopLagMonitor } from './services/infrastructure/EventLoopLagMonitor'; import { startEventLoopLagMonitor } from './services/infrastructure/EventLoopLagMonitor';
import { HttpServer } from './services/infrastructure/HttpServer'; import { HttpServer } from './services/infrastructure/HttpServer';
import { TeamInboxReader } from './services/team/TeamInboxReader';
import { import {
buildTeamControlApiBaseUrl, buildTeamControlApiBaseUrl,
clearTeamControlApiState, clearTeamControlApiState,
writeTeamControlApiState, writeTeamControlApiState,
} from './services/team/TeamControlApiState'; } from './services/team/TeamControlApiState';
import { TeamInboxReader } from './services/team/TeamInboxReader';
import { TeamSentMessagesStore } from './services/team/TeamSentMessagesStore'; import { TeamSentMessagesStore } from './services/team/TeamSentMessagesStore';
import { getAppIconPath } from './utils/appIcon'; import { getAppIconPath } from './utils/appIcon';
import { getProjectsBasePath, getTeamsBasePath, getTodosBasePath } from './utils/pathDecoder'; import { getProjectsBasePath, getTeamsBasePath, getTodosBasePath } from './utils/pathDecoder';
@ -80,22 +96,6 @@ import {
TeamProvisioningService, TeamProvisioningService,
UpdaterService, UpdaterService,
} from './services'; } from './services';
import {
ApiKeyService,
ExtensionFacadeService,
GlamaMcpEnrichmentService,
McpCatalogAggregator,
McpHealthDiagnosticsService,
McpInstallationStateService,
McpInstallService,
OfficialMcpRegistryService,
PluginCatalogService,
PluginInstallationStateService,
PluginInstallService,
SkillsCatalogService,
SkillsMutationService,
SkillsWatcherService,
} from './services/extensions';
import type { FileChangeEvent } from '@main/types'; import type { FileChangeEvent } from '@main/types';
import type { TeamChangeEvent } from '@shared/types'; import type { TeamChangeEvent } from '@shared/types';

View file

@ -7,10 +7,12 @@ import {
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import { isAgentActionMode } from '../services/team/actionModeInstructions'; import { isAgentActionMode } from '../services/team/actionModeInstructions';
import { validateTaskId, validateTeamName } from './guards'; import { validateTaskId, validateTeamName } from './guards';
import type { CrossTeamService } from '../services/team/CrossTeamService'; import type { CrossTeamService } from '../services/team/CrossTeamService';
import type { IpcMain, IpcMainInvokeEvent } from 'electron';
import type { IpcResult, TaskRef } from '@shared/types'; import type { IpcResult, TaskRef } from '@shared/types';
import type { IpcMain, IpcMainInvokeEvent } from 'electron';
const logger = createLogger('IPC:crossTeam'); const logger = createLogger('IPC:crossTeam');

View file

@ -6,30 +6,6 @@
* Phase 5: install/uninstall mutations. * Phase 5: install/uninstall mutations.
*/ */
import { createLogger } from '@shared/utils/logger';
import type {
ApiKeyEntry,
ApiKeyLookupResult,
ApiKeySaveRequest,
ApiKeyStorageStatus,
EnrichedPlugin,
InstalledMcpEntry,
McpCatalogItem,
McpCustomInstallRequest,
McpInstallRequest,
McpServerDiagnostic,
McpSearchResult,
OperationResult,
PluginInstallRequest,
} from '@shared/types/extensions';
import type { IpcMain, IpcMainInvokeEvent } from 'electron';
import type { ExtensionFacadeService } from '../services/extensions/ExtensionFacadeService';
import type { PluginInstallService } from '../services/extensions/install/PluginInstallService';
import type { McpInstallService } from '../services/extensions/install/McpInstallService';
import type { ApiKeyService } from '../services/extensions/apikeys/ApiKeyService';
import type { McpHealthDiagnosticsService } from '../services/extensions/state/McpHealthDiagnosticsService';
import { import {
API_KEYS_DELETE, API_KEYS_DELETE,
API_KEYS_LIST, API_KEYS_LIST,
@ -50,9 +26,32 @@ import {
PLUGIN_INSTALL, PLUGIN_INSTALL,
PLUGIN_UNINSTALL, PLUGIN_UNINSTALL,
} from '@preload/constants/ipcChannels'; } from '@preload/constants/ipcChannels';
import { createLogger } from '@shared/utils/logger';
import { GitHubStarsService } from '../services/extensions/catalog/GitHubStarsService'; import { GitHubStarsService } from '../services/extensions/catalog/GitHubStarsService';
import type { ApiKeyService } from '../services/extensions/apikeys/ApiKeyService';
import type { ExtensionFacadeService } from '../services/extensions/ExtensionFacadeService';
import type { McpInstallService } from '../services/extensions/install/McpInstallService';
import type { PluginInstallService } from '../services/extensions/install/PluginInstallService';
import type { McpHealthDiagnosticsService } from '../services/extensions/state/McpHealthDiagnosticsService';
import type {
ApiKeyEntry,
ApiKeyLookupResult,
ApiKeySaveRequest,
ApiKeyStorageStatus,
EnrichedPlugin,
InstalledMcpEntry,
McpCatalogItem,
McpCustomInstallRequest,
McpInstallRequest,
McpSearchResult,
McpServerDiagnostic,
OperationResult,
PluginInstallRequest,
} from '@shared/types/extensions';
import type { IpcMain, IpcMainInvokeEvent } from 'electron';
const logger = createLogger('IPC:extensions'); const logger = createLogger('IPC:extensions');
/** Allowed scope values */ /** Allowed scope values */

View file

@ -65,8 +65,8 @@ import {
registerSessionHandlers, registerSessionHandlers,
removeSessionHandlers, removeSessionHandlers,
} from './sessions'; } from './sessions';
import { initializeSshHandlers, registerSshHandlers, removeSshHandlers } from './ssh';
import { initializeSkillsHandlers, registerSkillsHandlers, removeSkillsHandlers } from './skills'; import { initializeSkillsHandlers, registerSkillsHandlers, removeSkillsHandlers } from './skills';
import { initializeSshHandlers, registerSshHandlers, removeSshHandlers } from './ssh';
import { import {
initializeSubagentHandlers, initializeSubagentHandlers,
registerSubagentHandlers, registerSubagentHandlers,
@ -103,18 +103,18 @@ import type {
TeamProvisioningService, TeamProvisioningService,
UpdaterService, UpdaterService,
} from '../services'; } from '../services';
import type { HttpServer } from '../services/infrastructure/HttpServer'; import type { ApiKeyService } from '../services/extensions/apikeys/ApiKeyService';
import type { CrossTeamService } from '../services/team/CrossTeamService';
import type { TeamBackupService } from '../services/team/TeamBackupService';
import type { ExtensionFacadeService } from '../services/extensions/ExtensionFacadeService'; import type { ExtensionFacadeService } from '../services/extensions/ExtensionFacadeService';
import type { McpInstallService } from '../services/extensions/install/McpInstallService'; import type { McpInstallService } from '../services/extensions/install/McpInstallService';
import type { PluginInstallService } from '../services/extensions/install/PluginInstallService'; import type { PluginInstallService } from '../services/extensions/install/PluginInstallService';
import type { ApiKeyService } from '../services/extensions/apikeys/ApiKeyService';
import type { McpHealthDiagnosticsService } from '../services/extensions/state/McpHealthDiagnosticsService';
import type { SkillsCatalogService } from '../services/extensions/skills/SkillsCatalogService'; import type { SkillsCatalogService } from '../services/extensions/skills/SkillsCatalogService';
import type { SkillsMutationService } from '../services/extensions/skills/SkillsMutationService'; import type { SkillsMutationService } from '../services/extensions/skills/SkillsMutationService';
import type { SkillsWatcherService } from '../services/extensions/skills/SkillsWatcherService'; import type { SkillsWatcherService } from '../services/extensions/skills/SkillsWatcherService';
import type { McpHealthDiagnosticsService } from '../services/extensions/state/McpHealthDiagnosticsService';
import type { HttpServer } from '../services/infrastructure/HttpServer';
import type { SchedulerService } from '../services/schedule/SchedulerService'; import type { SchedulerService } from '../services/schedule/SchedulerService';
import type { CrossTeamService } from '../services/team/CrossTeamService';
import type { TeamBackupService } from '../services/team/TeamBackupService';
/** /**
* Initializes IPC handlers with service registry. * Initializes IPC handlers with service registry.

View file

@ -1,18 +1,3 @@
import { createLogger } from '@shared/utils/logger';
import type {
SkillCatalogItem,
SkillDeleteRequest,
SkillDetail,
SkillImportRequest,
SkillReviewPreview,
SkillUpsertRequest,
} from '@shared/types/extensions';
import type { IpcMain, IpcMainInvokeEvent } from 'electron';
import type { SkillsCatalogService } from '../services/extensions/skills/SkillsCatalogService';
import type { SkillsMutationService } from '../services/extensions/skills/SkillsMutationService';
import type { SkillsWatcherService } from '../services/extensions/skills/SkillsWatcherService';
import { import {
SKILLS_APPLY_IMPORT, SKILLS_APPLY_IMPORT,
SKILLS_APPLY_UPSERT, SKILLS_APPLY_UPSERT,
@ -24,6 +9,20 @@ import {
SKILLS_START_WATCHING, SKILLS_START_WATCHING,
SKILLS_STOP_WATCHING, SKILLS_STOP_WATCHING,
} from '@preload/constants/ipcChannels'; } from '@preload/constants/ipcChannels';
import { createLogger } from '@shared/utils/logger';
import type { SkillsCatalogService } from '../services/extensions/skills/SkillsCatalogService';
import type { SkillsMutationService } from '../services/extensions/skills/SkillsMutationService';
import type { SkillsWatcherService } from '../services/extensions/skills/SkillsWatcherService';
import type {
SkillCatalogItem,
SkillDeleteRequest,
SkillDetail,
SkillImportRequest,
SkillReviewPreview,
SkillUpsertRequest,
} from '@shared/types/extensions';
import type { IpcMain, IpcMainInvokeEvent } from 'electron';
const logger = createLogger('IPC:skills'); const logger = createLogger('IPC:skills');

View file

@ -1,5 +1,3 @@
import crypto from 'crypto';
import { setCurrentMainOp } from '@main/services/infrastructure/EventLoopLagMonitor'; import { setCurrentMainOp } from '@main/services/infrastructure/EventLoopLagMonitor';
import { getAppIconPath } from '@main/utils/appIcon'; import { getAppIconPath } from '@main/utils/appIcon';
import { getAppDataPath } from '@main/utils/pathDecoder'; import { getAppDataPath } from '@main/utils/pathDecoder';
@ -29,8 +27,8 @@ import {
TEAM_LAUNCH, TEAM_LAUNCH,
TEAM_LEAD_ACTIVITY, TEAM_LEAD_ACTIVITY,
TEAM_LEAD_CONTEXT, TEAM_LEAD_CONTEXT,
TEAM_MEMBER_SPAWN_STATUSES,
TEAM_LIST, TEAM_LIST,
TEAM_MEMBER_SPAWN_STATUSES,
TEAM_PERMANENTLY_DELETE, TEAM_PERMANENTLY_DELETE,
TEAM_PREPARE_PROVISIONING, TEAM_PREPARE_PROVISIONING,
TEAM_PROCESS_ALIVE, TEAM_PROCESS_ALIVE,
@ -72,17 +70,18 @@ import {
} from '@shared/utils/cliArgsParser'; } from '@shared/utils/cliArgsParser';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import { isRateLimitMessage } from '@shared/utils/rateLimitDetector'; import { isRateLimitMessage } from '@shared/utils/rateLimitDetector';
import crypto from 'crypto';
import { BrowserWindow, type IpcMain, type IpcMainInvokeEvent, Notification } from 'electron'; import { BrowserWindow, type IpcMain, type IpcMainInvokeEvent, Notification } from 'electron';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { ConfigManager } from '../services/infrastructure/ConfigManager'; import { ConfigManager } from '../services/infrastructure/ConfigManager';
import { NotificationManager } from '../services/infrastructure/NotificationManager'; import { NotificationManager } from '../services/infrastructure/NotificationManager';
import { gitIdentityResolver } from '../services/parsing/GitIdentityResolver';
import { import {
buildActionModeAgentBlock, buildActionModeAgentBlock,
isAgentActionMode, isAgentActionMode,
} from '../services/team/actionModeInstructions'; } from '../services/team/actionModeInstructions';
import { gitIdentityResolver } from '../services/parsing/GitIdentityResolver';
import { TeamAttachmentStore } from '../services/team/TeamAttachmentStore'; import { TeamAttachmentStore } from '../services/team/TeamAttachmentStore';
import { buildAddMemberSpawnMessage } from '../services/team/TeamProvisioningService'; import { buildAddMemberSpawnMessage } from '../services/team/TeamProvisioningService';
import { TeamTaskAttachmentStore } from '../services/team/TeamTaskAttachmentStore'; import { TeamTaskAttachmentStore } from '../services/team/TeamTaskAttachmentStore';
@ -113,13 +112,13 @@ import type {
GlobalTask, GlobalTask,
IpcResult, IpcResult,
KanbanColumnId, KanbanColumnId,
LeadContextUsage,
LeadActivitySnapshot, LeadActivitySnapshot,
LeadContextUsage,
LeadContextUsageSnapshot, LeadContextUsageSnapshot,
MemberFullStats, MemberFullStats,
MemberSpawnStatusesSnapshot,
MemberLogSummary, MemberLogSummary,
MemberSpawnStatusEntry, MemberSpawnStatusEntry,
MemberSpawnStatusesSnapshot,
SendMessageRequest, SendMessageRequest,
SendMessageResult, SendMessageResult,
TaskAttachmentMeta, TaskAttachmentMeta,
@ -548,7 +547,10 @@ async function handlePermanentlyDeleteTeam(
.rm(path.join(appData, 'attachments', validated.value!), { recursive: true, force: true }) .rm(path.join(appData, 'attachments', validated.value!), { recursive: true, force: true })
.catch(() => undefined); .catch(() => undefined);
await fs.promises await fs.promises
.rm(path.join(appData, 'task-attachments', validated.value!), { recursive: true, force: true }) .rm(path.join(appData, 'task-attachments', validated.value!), {
recursive: true,
force: true,
})
.catch(() => undefined); .catch(() => undefined);
// Mark in backup registry AFTER successful deletion // Mark in backup registry AFTER successful deletion
if (teamBackupService) { if (teamBackupService) {

View file

@ -7,6 +7,12 @@
*/ */
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import { type McpCatalogAggregator } from './catalog/McpCatalogAggregator';
import { type PluginCatalogService } from './catalog/PluginCatalogService';
import { type McpInstallationStateService } from './state/McpInstallationStateService';
import { type PluginInstallationStateService } from './state/PluginInstallationStateService';
import type { import type {
EnrichedPlugin, EnrichedPlugin,
InstalledMcpEntry, InstalledMcpEntry,
@ -15,11 +21,6 @@ import type {
PluginCatalogItem, PluginCatalogItem,
} from '@shared/types/extensions'; } from '@shared/types/extensions';
import { PluginCatalogService } from './catalog/PluginCatalogService';
import { McpCatalogAggregator } from './catalog/McpCatalogAggregator';
import { PluginInstallationStateService } from './state/PluginInstallationStateService';
import { McpInstallationStateService } from './state/McpInstallationStateService';
const logger = createLogger('Extensions:Facade'); const logger = createLogger('Extensions:Facade');
export class ExtensionFacadeService { export class ExtensionFacadeService {

View file

@ -9,12 +9,14 @@
* Storage file: ~/.claude/api-keys.json * Storage file: ~/.claude/api-keys.json
*/ */
import { safeStorage } from 'electron';
import fs from 'node:fs/promises';
import path from 'node:path';
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import fs from 'node:fs/promises';
import os from 'node:os'; import os from 'node:os';
import path from 'node:path';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import { safeStorage } from 'electron';
import type { import type {
ApiKeyEntry, ApiKeyEntry,
ApiKeyLookupResult, ApiKeyLookupResult,

View file

@ -9,8 +9,8 @@
import https from 'node:https'; import https from 'node:https';
import { createLogger } from '@shared/utils/logger';
import { parseGitHubOwnerRepo } from '@shared/utils/extensionNormalizers'; import { parseGitHubOwnerRepo } from '@shared/utils/extensionNormalizers';
import { createLogger } from '@shared/utils/logger';
const logger = createLogger('Extensions:GitHubStars'); const logger = createLogger('Extensions:GitHubStars');
@ -39,7 +39,7 @@ export class GitHubStarsService {
*/ */
async fetchStars(repositoryUrls: string[]): Promise<Record<string, number>> { async fetchStars(repositoryUrls: string[]): Promise<Record<string, number>> {
const result: Record<string, number> = {}; const result: Record<string, number> = {};
const tasks: Array<{ url: string; owner: string; repo: string }> = []; const tasks: { url: string; owner: string; repo: string }[] = [];
for (const url of repositoryUrls) { for (const url of repositoryUrls) {
const parsed = parseGitHubOwnerRepo(url); const parsed = parseGitHubOwnerRepo(url);
@ -107,10 +107,10 @@ export class GitHubStarsService {
* Run async tasks with a concurrency limit. * Run async tasks with a concurrency limit.
*/ */
private async withConcurrencyLimit( private async withConcurrencyLimit(
tasks: Array<() => Promise<void>>, tasks: (() => Promise<void>)[],
limit: number limit: number
): Promise<Array<'ok' | 'error'>> { ): Promise<('ok' | 'error')[]> {
const results: Array<'ok' | 'error'> = []; const results: ('ok' | 'error')[] = [];
let index = 0; let index = 0;
const run = async (): Promise<void> => { const run = async (): Promise<void> => {

View file

@ -9,10 +9,11 @@
* Cursor-based pagination (after), no auth required. * Cursor-based pagination (after), no auth required.
*/ */
import https from 'node:https';
import http from 'node:http'; import http from 'node:http';
import https from 'node:https';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import type { McpCatalogItem, McpHostingType, McpToolDef } from '@shared/types/extensions'; import type { McpCatalogItem, McpHostingType, McpToolDef } from '@shared/types/extensions';
const logger = createLogger('Extensions:GlamaMcp'); const logger = createLogger('Extensions:GlamaMcp');

View file

@ -7,12 +7,13 @@
* - Provides getById() for secure install flow * - Provides getById() for secure install flow
*/ */
import { createLogger } from '@shared/utils/logger';
import type { McpCatalogItem, McpSearchResult } from '@shared/types/extensions';
import { normalizeRepoUrl } from '@shared/utils/extensionNormalizers'; import { normalizeRepoUrl } from '@shared/utils/extensionNormalizers';
import { createLogger } from '@shared/utils/logger';
import { OfficialMcpRegistryService } from './OfficialMcpRegistryService'; import { type GlamaMcpEnrichmentService } from './GlamaMcpEnrichmentService';
import { GlamaMcpEnrichmentService } from './GlamaMcpEnrichmentService'; import { type OfficialMcpRegistryService } from './OfficialMcpRegistryService';
import type { McpCatalogItem, McpSearchResult } from '@shared/types/extensions';
const logger = createLogger('Extensions:McpAggregator'); const logger = createLogger('Extensions:McpAggregator');

View file

@ -6,10 +6,11 @@
* Filters for _meta.isLatest to pick only latest versions. * Filters for _meta.isLatest to pick only latest versions.
*/ */
import https from 'node:https';
import http from 'node:http'; import http from 'node:http';
import https from 'node:https';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import type { import type {
McpAuthHeaderDef, McpAuthHeaderDef,
McpCatalogItem, McpCatalogItem,

View file

@ -8,12 +8,13 @@
* - Deduplicates concurrent requests * - Deduplicates concurrent requests
*/ */
import https from 'node:https';
import http from 'node:http'; import http from 'node:http';
import https from 'node:https';
import { createLogger } from '@shared/utils/logger';
import type { PluginCatalogItem } from '@shared/types/extensions';
import { buildPluginId } from '@shared/utils/extensionNormalizers'; import { buildPluginId } from '@shared/utils/extensionNormalizers';
import { createLogger } from '@shared/utils/logger';
import type { PluginCatalogItem } from '@shared/types/extensions';
const logger = createLogger('Extensions:PluginCatalog'); const logger = createLogger('Extensions:PluginCatalog');
@ -260,7 +261,7 @@ export class PluginCatalogService {
const json = JSON.parse(response.body) as MarketplaceJson; const json = JSON.parse(response.body) as MarketplaceJson;
const items = this.parseMarketplace(json); const items = this.parseMarketplace(json);
const etag = (response.headers['etag'] as string) ?? null; const etag = (response.headers.etag as string) ?? null;
this.cache = { items, etag, fetchedAt: Date.now() }; this.cache = { items, etag, fetchedAt: Date.now() };
logger.info(`Fetched ${items.length} plugins from marketplace "${json.name}"`); logger.info(`Fetched ${items.length} plugins from marketplace "${json.name}"`);
@ -311,7 +312,7 @@ export class PluginCatalogService {
* e.g. https://github.com/org/repo → https://raw.githubusercontent.com/org/repo/main/README.md * e.g. https://github.com/org/repo → https://raw.githubusercontent.com/org/repo/main/README.md
*/ */
private buildReadmeUrl(repoUrl: string): string | null { private buildReadmeUrl(repoUrl: string): string | null {
const match = repoUrl.match(/github\.com\/([^/]+)\/([^/]+)/); const match = /github\.com\/([^/]+)\/([^/]+)/.exec(repoUrl);
if (!match) return null; if (!match) return null;
const [, owner, repo] = match; const [, owner, repo] = match;
return `https://raw.githubusercontent.com/${owner}/${repo}/main/README.md`; return `https://raw.githubusercontent.com/${owner}/${repo}/main/README.md`;

View file

@ -2,26 +2,26 @@
* Extension services barrel export. * Extension services barrel export.
*/ */
export { PluginCatalogService } from './catalog/PluginCatalogService';
export { OfficialMcpRegistryService } from './catalog/OfficialMcpRegistryService';
export { GlamaMcpEnrichmentService } from './catalog/GlamaMcpEnrichmentService';
export { McpCatalogAggregator } from './catalog/McpCatalogAggregator';
export { PluginInstallationStateService } from './state/PluginInstallationStateService';
export { McpInstallationStateService } from './state/McpInstallationStateService';
export { McpHealthDiagnosticsService } from './state/McpHealthDiagnosticsService';
export { ExtensionFacadeService } from './ExtensionFacadeService';
export { PluginInstallService } from './install/PluginInstallService';
export { McpInstallService } from './install/McpInstallService';
export { ApiKeyService } from './apikeys/ApiKeyService'; export { ApiKeyService } from './apikeys/ApiKeyService';
export { GitHubStarsService } from './catalog/GitHubStarsService'; export { GitHubStarsService } from './catalog/GitHubStarsService';
export { SkillRootsResolver } from './skills/SkillRootsResolver'; export { GlamaMcpEnrichmentService } from './catalog/GlamaMcpEnrichmentService';
export { SkillScanner } from './skills/SkillScanner'; export { McpCatalogAggregator } from './catalog/McpCatalogAggregator';
export { SkillMetadataParser } from './skills/SkillMetadataParser'; export { OfficialMcpRegistryService } from './catalog/OfficialMcpRegistryService';
export { SkillValidator } from './skills/SkillValidator'; export { PluginCatalogService } from './catalog/PluginCatalogService';
export { SkillsCatalogService } from './skills/SkillsCatalogService'; export { ExtensionFacadeService } from './ExtensionFacadeService';
export { SkillScaffoldService } from './skills/SkillScaffoldService'; export { McpInstallService } from './install/McpInstallService';
export { PluginInstallService } from './install/PluginInstallService';
export { SkillImportService } from './skills/SkillImportService'; export { SkillImportService } from './skills/SkillImportService';
export { SkillMetadataParser } from './skills/SkillMetadataParser';
export { SkillPlanService } from './skills/SkillPlanService'; export { SkillPlanService } from './skills/SkillPlanService';
export { SkillReviewService } from './skills/SkillReviewService'; export { SkillReviewService } from './skills/SkillReviewService';
export { SkillRootsResolver } from './skills/SkillRootsResolver';
export { SkillScaffoldService } from './skills/SkillScaffoldService';
export { SkillScanner } from './skills/SkillScanner';
export { SkillsCatalogService } from './skills/SkillsCatalogService';
export { SkillsMutationService } from './skills/SkillsMutationService'; export { SkillsMutationService } from './skills/SkillsMutationService';
export { SkillsWatcherService } from './skills/SkillsWatcherService'; export { SkillsWatcherService } from './skills/SkillsWatcherService';
export { SkillValidator } from './skills/SkillValidator';
export { McpHealthDiagnosticsService } from './state/McpHealthDiagnosticsService';
export { McpInstallationStateService } from './state/McpInstallationStateService';
export { PluginInstallationStateService } from './state/PluginInstallationStateService';

View file

@ -10,12 +10,12 @@
import { execCli } from '@main/utils/childProcess'; import { execCli } from '@main/utils/childProcess';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import type { McpCatalogAggregator } from '../catalog/McpCatalogAggregator';
import type { import type {
McpCustomInstallRequest, McpCustomInstallRequest,
McpInstallRequest, McpInstallRequest,
OperationResult, OperationResult,
} from '@shared/types/extensions'; } from '@shared/types/extensions';
import type { McpCatalogAggregator } from '../catalog/McpCatalogAggregator';
const logger = createLogger('Extensions:McpInstall'); const logger = createLogger('Extensions:McpInstall');

View file

@ -8,8 +8,8 @@
import { execCli } from '@main/utils/childProcess'; import { execCli } from '@main/utils/childProcess';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import type { OperationResult, PluginInstallRequest } from '@shared/types/extensions';
import type { PluginCatalogService } from '../catalog/PluginCatalogService'; import type { PluginCatalogService } from '../catalog/PluginCatalogService';
import type { OperationResult, PluginInstallRequest } from '@shared/types/extensions';
const logger = createLogger('Extensions:PluginInstall'); const logger = createLogger('Extensions:PluginInstall');

View file

@ -101,13 +101,11 @@ export class SkillImportService {
} }
} }
private async walkDirectory( private async walkDirectory(rootDir: string): Promise<{
rootDir: string files: { absolutePath: string; relativePath: string }[];
): Promise<{
files: Array<{ absolutePath: string; relativePath: string }>;
hiddenEntriesSkipped: number; hiddenEntriesSkipped: number;
}> { }> {
const allFiles: Array<{ absolutePath: string; relativePath: string }> = []; const allFiles: { absolutePath: string; relativePath: string }[] = [];
let hiddenEntriesSkipped = 0; let hiddenEntriesSkipped = 0;
let totalBytes = 0; let totalBytes = 0;

View file

@ -1,6 +1,9 @@
import * as path from 'node:path'; import * as path from 'node:path';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import YAML from 'yaml';
import type { ResolvedSkillRoot } from './SkillRootsResolver';
import type { import type {
SkillCatalogItem, SkillCatalogItem,
SkillDetail, SkillDetail,
@ -8,9 +11,6 @@ import type {
SkillInvocationMode, SkillInvocationMode,
SkillValidationIssue, SkillValidationIssue,
} from '@shared/types/extensions'; } from '@shared/types/extensions';
import YAML from 'yaml';
import type { ResolvedSkillRoot } from './SkillRootsResolver';
const logger = createLogger('Extensions:SkillParser'); const logger = createLogger('Extensions:SkillParser');
@ -203,7 +203,7 @@ export class SkillMetadataParser {
}; };
} }
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/u); const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/u.exec(content);
if (!match) { if (!match) {
return { return {
rawFrontmatter: null, rawFrontmatter: null,

View file

@ -1,8 +1,11 @@
import { createHash } from 'node:crypto';
import * as fs from 'node:fs/promises'; import * as fs from 'node:fs/promises';
import * as os from 'node:os'; import * as os from 'node:os';
import * as path from 'node:path'; import * as path from 'node:path';
import { createHash } from 'node:crypto';
import { SkillScanner } from './SkillScanner';
import type { ImportedSkillSourceFile } from './SkillImportService';
import type { import type {
SkillDraftFile, SkillDraftFile,
SkillReviewFileChange, SkillReviewFileChange,
@ -10,10 +13,6 @@ import type {
SkillReviewSummary, SkillReviewSummary,
} from '@shared/types/extensions'; } from '@shared/types/extensions';
import type { ImportedSkillSourceFile } from './SkillImportService';
import { SkillScanner } from './SkillScanner';
type SkillPlanInputFile = type SkillPlanInputFile =
| { relativePath: string; isBinary: false; content: string } | { relativePath: string; isBinary: false; content: string }
| { relativePath: string; isBinary: true; sourceAbsolutePath: string }; | { relativePath: string; isBinary: true; sourceAbsolutePath: string };
@ -73,7 +72,7 @@ export class SkillPlanService {
async applyPlan(plan: SkillExecutionPlan): Promise<void> { async applyPlan(plan: SkillExecutionPlan): Promise<void> {
const backupRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'skill-plan-backup-')); const backupRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'skill-plan-backup-'));
const createdPaths: string[] = []; const createdPaths: string[] = [];
const backups: Array<{ absolutePath: string; backupPath: string }> = []; const backups: { absolutePath: string; backupPath: string }[] = [];
try { try {
for (const [index, change] of plan.changes.entries()) { for (const [index, change] of plan.changes.entries()) {
@ -200,7 +199,7 @@ export class SkillPlanService {
const summary = changes.reduce<SkillReviewSummary>( const summary = changes.reduce<SkillReviewSummary>(
(acc, change) => { (acc, change) => {
acc[`${change.action}d` as 'created' | 'updated' | 'deleted'] += 1; acc[`${change.action}d`] += 1;
if (change.isBinary) { if (change.isBinary) {
acc.binary += 1; acc.binary += 1;
} }

View file

@ -2,9 +2,9 @@ import * as fs from 'node:fs/promises';
import * as path from 'node:path'; import * as path from 'node:path';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import type { SkillDraftFile, SkillReviewFileChange } from '@shared/types/extensions';
import type { ImportedSkillSourceFile } from './SkillImportService'; import type { ImportedSkillSourceFile } from './SkillImportService';
import type { SkillDraftFile, SkillReviewFileChange } from '@shared/types/extensions';
const logger = createLogger('Extensions:SkillReview'); const logger = createLogger('Extensions:SkillReview');

View file

@ -1,6 +1,7 @@
import * as path from 'node:path'; import * as path from 'node:path';
import { getHomeDir } from '@main/utils/pathDecoder'; import { getHomeDir } from '@main/utils/pathDecoder';
import type { SkillRootKind, SkillScope } from '@shared/types/extensions'; import type { SkillRootKind, SkillScope } from '@shared/types/extensions';
export interface ResolvedSkillRoot { export interface ResolvedSkillRoot {
@ -10,7 +11,7 @@ export interface ResolvedSkillRoot {
rootPath: string; rootPath: string;
} }
const USER_ROOTS: Array<{ rootKind: SkillRootKind; segments: string[] }> = [ const USER_ROOTS: { rootKind: SkillRootKind; segments: string[] }[] = [
{ rootKind: 'claude', segments: ['.claude', 'skills'] }, { rootKind: 'claude', segments: ['.claude', 'skills'] },
{ rootKind: 'cursor', segments: ['.cursor', 'skills'] }, { rootKind: 'cursor', segments: ['.cursor', 'skills'] },
{ rootKind: 'agents', segments: ['.agents', 'skills'] }, { rootKind: 'agents', segments: ['.agents', 'skills'] },

View file

@ -2,10 +2,11 @@ import * as fs from 'node:fs/promises';
import * as path from 'node:path'; import * as path from 'node:path';
import { isPathWithinRoot, validateFileName } from '@main/utils/pathValidation'; import { isPathWithinRoot, validateFileName } from '@main/utils/pathValidation';
import type { SkillDraftFile, SkillRootKind, SkillScope } from '@shared/types/extensions';
import { SkillRootsResolver } from './SkillRootsResolver'; import { SkillRootsResolver } from './SkillRootsResolver';
import type { SkillDraftFile, SkillRootKind, SkillScope } from '@shared/types/extensions';
export class SkillScaffoldService { export class SkillScaffoldService {
constructor(private readonly rootsResolver = new SkillRootsResolver()) {} constructor(private readonly rootsResolver = new SkillRootsResolver()) {}

View file

@ -1,10 +1,10 @@
import * as fs from 'node:fs/promises'; import * as fs from 'node:fs/promises';
import * as path from 'node:path'; import * as path from 'node:path';
import type { SkillCatalogItem, SkillDirectoryFlags } from '@shared/types/extensions';
import { SkillMetadataParser, type SkillRelatedFiles } from './SkillMetadataParser'; import { SkillMetadataParser, type SkillRelatedFiles } from './SkillMetadataParser';
import type { ResolvedSkillRoot } from './SkillRootsResolver'; import type { ResolvedSkillRoot } from './SkillRootsResolver';
import type { SkillCatalogItem, SkillDirectoryFlags } from '@shared/types/extensions';
const SKILL_FILE_CANDIDATES = ['SKILL.md', 'Skill.md', 'skill.md'] as const; const SKILL_FILE_CANDIDATES = ['SKILL.md', 'Skill.md', 'skill.md'] as const;

View file

@ -2,13 +2,14 @@ import * as fs from 'node:fs/promises';
import * as path from 'node:path'; import * as path from 'node:path';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import type { SkillCatalogItem, SkillDetail } from '@shared/types/extensions';
import { SkillMetadataParser } from './SkillMetadataParser'; import { SkillMetadataParser } from './SkillMetadataParser';
import { SkillRootsResolver, type ResolvedSkillRoot } from './SkillRootsResolver'; import { type ResolvedSkillRoot, SkillRootsResolver } from './SkillRootsResolver';
import { SkillScanner } from './SkillScanner'; import { SkillScanner } from './SkillScanner';
import { SkillValidator } from './SkillValidator'; import { SkillValidator } from './SkillValidator';
import type { SkillCatalogItem, SkillDetail } from '@shared/types/extensions';
const logger = createLogger('Extensions:SkillsCatalog'); const logger = createLogger('Extensions:SkillsCatalog');
export class SkillsCatalogService { export class SkillsCatalogService {

View file

@ -1,6 +1,15 @@
import * as fs from 'node:fs/promises'; import * as fs from 'node:fs/promises';
import * as path from 'node:path'; import * as path from 'node:path';
import { isPathWithinRoot, validateFileName } from '@main/utils/pathValidation';
import { shell } from 'electron';
import { SkillImportService } from './SkillImportService';
import { SkillPlanService } from './SkillPlanService';
import { SkillRootsResolver } from './SkillRootsResolver';
import { SkillScaffoldService } from './SkillScaffoldService';
import { SkillsCatalogService } from './SkillsCatalogService';
import type { import type {
SkillDeleteRequest, SkillDeleteRequest,
SkillDetail, SkillDetail,
@ -8,15 +17,6 @@ import type {
SkillReviewPreview, SkillReviewPreview,
SkillUpsertRequest, SkillUpsertRequest,
} from '@shared/types/extensions'; } from '@shared/types/extensions';
import { shell } from 'electron';
import { isPathWithinRoot, validateFileName } from '@main/utils/pathValidation';
import { SkillImportService } from './SkillImportService';
import { SkillPlanService } from './SkillPlanService';
import { SkillScaffoldService } from './SkillScaffoldService';
import { SkillRootsResolver } from './SkillRootsResolver';
import { SkillsCatalogService } from './SkillsCatalogService';
export class SkillsMutationService { export class SkillsMutationService {
constructor( constructor(

View file

@ -1,10 +1,10 @@
import { createLogger } from '@shared/utils/logger';
import type { SkillWatcherEvent } from '@shared/types/extensions';
import { isPathWithinRoot } from '@main/utils/pathValidation'; import { isPathWithinRoot } from '@main/utils/pathValidation';
import { createLogger } from '@shared/utils/logger';
import { watch } from 'chokidar'; import { watch } from 'chokidar';
import { SkillRootsResolver } from './SkillRootsResolver'; import { SkillRootsResolver } from './SkillRootsResolver';
import type { SkillWatcherEvent } from '@shared/types/extensions';
import type { FSWatcher } from 'chokidar'; import type { FSWatcher } from 'chokidar';
const logger = createLogger('Extensions:SkillsWatcher'); const logger = createLogger('Extensions:SkillsWatcher');

View file

@ -12,9 +12,10 @@
import * as fs from 'node:fs/promises'; import * as fs from 'node:fs/promises';
import * as path from 'node:path'; import * as path from 'node:path';
import { createLogger } from '@shared/utils/logger';
import type { InstalledMcpEntry } from '@shared/types/extensions';
import { getHomeDir } from '@main/utils/pathDecoder'; import { getHomeDir } from '@main/utils/pathDecoder';
import { createLogger } from '@shared/utils/logger';
import type { InstalledMcpEntry } from '@shared/types/extensions';
const logger = createLogger('Extensions:McpState'); const logger = createLogger('Extensions:McpState');

View file

@ -11,10 +11,11 @@
import * as fs from 'node:fs/promises'; import * as fs from 'node:fs/promises';
import * as path from 'node:path'; import * as path from 'node:path';
import { getClaudeBasePath } from '@main/utils/pathDecoder';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import type { InstalledPluginEntry } from '@shared/types/extensions'; import type { InstalledPluginEntry } from '@shared/types/extensions';
import type { InstallScope } from '@shared/types/extensions'; import type { InstallScope } from '@shared/types/extensions';
import { getClaudeBasePath } from '@main/utils/pathDecoder';
const logger = createLogger('Extensions:PluginState'); const logger = createLogger('Extensions:PluginState');
@ -29,24 +30,24 @@ interface InstalledPluginsJson {
version: number; version: number;
plugins: Record< plugins: Record<
string, // qualifiedName string, // qualifiedName
Array<{ {
scope: string; scope: string;
installPath?: string; installPath?: string;
version?: string; version?: string;
installedAt?: string; installedAt?: string;
lastUpdated?: string; lastUpdated?: string;
gitCommitSha?: string; gitCommitSha?: string;
}> }[]
>; >;
} }
interface InstallCountsJson { interface InstallCountsJson {
version: number; version: number;
fetchedAt: string; fetchedAt: string;
counts: Array<{ counts: {
plugin: string; // qualifiedName format plugin: string; // qualifiedName format
unique_installs: number; unique_installs: number;
}>; }[];
} }
// ── Cache ────────────────────────────────────────────────────────────────── // ── Cache ──────────────────────────────────────────────────────────────────

View file

@ -12,8 +12,8 @@
export * from './analysis'; export * from './analysis';
export * from './discovery'; export * from './discovery';
export * from './error'; export * from './error';
export * from './extensions';
export * from './infrastructure'; export * from './infrastructure';
export * from './parsing'; export * from './parsing';
export * from './team';
export * from './schedule'; export * from './schedule';
export * from './extensions'; export * from './team';

View file

@ -15,8 +15,8 @@ import { createLogger } from '@shared/utils/logger';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import type { Schedule, ScheduleRun } from '@shared/types';
import type { ScheduleRepository } from './ScheduleRepository'; import type { ScheduleRepository } from './ScheduleRepository';
import type { Schedule, ScheduleRun } from '@shared/types';
const logger = createLogger('Service:JsonScheduleRepo'); const logger = createLogger('Service:JsonScheduleRepo');

View file

@ -41,7 +41,7 @@ function extractSummaryFromStreamJson(stdout: string): string {
const content = (parsed.content ?? const content = (parsed.content ??
(parsed.message as Record<string, unknown> | undefined)?.content) as (parsed.message as Record<string, unknown> | undefined)?.content) as
| Array<{ type?: string; text?: string }> | { type?: string; text?: string }[]
| undefined; | undefined;
if (!Array.isArray(content)) continue; if (!Array.isArray(content)) continue;

View file

@ -10,6 +10,8 @@ import { createLogger } from '@shared/utils/logger';
import { Cron } from 'croner'; import { Cron } from 'croner';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import type { ScheduledTaskExecutor } from './ScheduledTaskExecutor';
import type { ScheduleRepository } from './ScheduleRepository';
import type { import type {
CreateScheduleInput, CreateScheduleInput,
Schedule, Schedule,
@ -18,8 +20,6 @@ import type {
ScheduleRunStatus, ScheduleRunStatus,
UpdateSchedulePatch, UpdateSchedulePatch,
} from '@shared/types'; } from '@shared/types';
import type { ScheduleRepository } from './ScheduleRepository';
import type { ScheduledTaskExecutor } from './ScheduledTaskExecutor';
const logger = createLogger('Service:Scheduler'); const logger = createLogger('Service:Scheduler');
@ -511,7 +511,7 @@ export class SchedulerService {
private async onCronTick(scheduleId: string): Promise<void> { private async onCronTick(scheduleId: string): Promise<void> {
const schedule = await this.repository.getSchedule(scheduleId); const schedule = await this.repository.getSchedule(scheduleId);
if (!schedule || schedule.status !== 'active') { if (schedule?.status !== 'active') {
logger.debug(`Cron tick for ${scheduleId} skipped (not active)`); logger.debug(`Cron tick for ${scheduleId} skipped (not active)`);
return; return;
} }
@ -659,7 +659,7 @@ export class SchedulerService {
} }
const freshSchedule = await this.repository.getSchedule(schedule.id); const freshSchedule = await this.repository.getSchedule(schedule.id);
if (!freshSchedule || freshSchedule.status !== 'active') { if (freshSchedule?.status !== 'active') {
await this.completeRun(retryRun, 'failed', exitCode, undefined, error); await this.completeRun(retryRun, 'failed', exitCode, undefined, error);
return false; return false;
} }

View file

@ -3,12 +3,12 @@
*/ */
export { JsonScheduleRepository } from './JsonScheduleRepository'; export { JsonScheduleRepository } from './JsonScheduleRepository';
export type { ScheduleRepository } from './ScheduleRepository';
export { ScheduledTaskExecutor } from './ScheduledTaskExecutor';
export type { export type {
ExecutionRequest, ExecutionRequest,
InternalScheduleRun, InternalScheduleRun,
ScheduledTaskResult, ScheduledTaskResult,
} from './ScheduledTaskExecutor'; } from './ScheduledTaskExecutor';
export { SchedulerService } from './SchedulerService'; export { ScheduledTaskExecutor } from './ScheduledTaskExecutor';
export type { ScheduleRepository } from './ScheduleRepository';
export type { WarmUpFn } from './SchedulerService'; export type { WarmUpFn } from './SchedulerService';
export { SchedulerService } from './SchedulerService';

View file

@ -577,7 +577,7 @@ export class ChangeExtractorService {
private async parseJSONLFilesWithConcurrency( private async parseJSONLFilesWithConcurrency(
paths: string[] paths: string[]
): Promise<Array<{ snippets: SnippetDiff[]; mtime: number }>> { ): Promise<{ snippets: SnippetDiff[]; mtime: number }[]> {
if (paths.length === 0) return []; if (paths.length === 0) return [];
const results = new Array<{ snippets: SnippetDiff[]; mtime: number }>(paths.length); const results = new Array<{ snippets: SnippetDiff[]; mtime: number }>(paths.length);

View file

@ -1,5 +1,5 @@
import { CROSS_TEAM_SENT_SOURCE, CROSS_TEAM_SOURCE, formatCrossTeamText } from '@shared/constants';
import { getClaudeBasePath, getTeamsBasePath } from '@main/utils/pathDecoder'; import { getClaudeBasePath, getTeamsBasePath } from '@main/utils/pathDecoder';
import { CROSS_TEAM_SENT_SOURCE, CROSS_TEAM_SOURCE, formatCrossTeamText } from '@shared/constants';
import { isLeadMember } from '@shared/utils/leadDetection'; import { isLeadMember } from '@shared/utils/leadDetection';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import * as agentTeamsControllerModule from 'agent-teams-controller'; import * as agentTeamsControllerModule from 'agent-teams-controller';

View file

@ -300,7 +300,8 @@ export class TeamBackupService {
if (raw && isValidConfig(raw)) { if (raw && isValidConfig(raw)) {
const parsed = JSON.parse(raw) as Record<string, unknown>; const parsed = JSON.parse(raw) as Record<string, unknown>;
manifest.displayName = typeof parsed.name === 'string' ? parsed.name : undefined; manifest.displayName = typeof parsed.name === 'string' ? parsed.name : undefined;
manifest.projectPath = typeof parsed.projectPath === 'string' ? parsed.projectPath : undefined; manifest.projectPath =
typeof parsed.projectPath === 'string' ? parsed.projectPath : undefined;
} }
} catch { } catch {
// best-effort // best-effort
@ -392,7 +393,7 @@ export class TeamBackupService {
} }
const cached = manifest.fileStats[descriptor.relPath]; const cached = manifest.fileStats[descriptor.relPath];
if (cached && cached.mtime === stat.mtimeMs && cached.size === stat.size) { if (cached?.mtime === stat.mtimeMs && cached.size === stat.size) {
return false; // not dirty return false; // not dirty
} }
@ -431,7 +432,7 @@ export class TeamBackupService {
if (stat.size > MAX_FILE_SIZE_BYTES) return; // skip oversized silently during shutdown if (stat.size > MAX_FILE_SIZE_BYTES) return; // skip oversized silently during shutdown
const cached = manifest.fileStats[descriptor.relPath]; const cached = manifest.fileStats[descriptor.relPath];
if (cached && cached.mtime === stat.mtimeMs && cached.size === stat.size) return; if (cached?.mtime === stat.mtimeMs && cached.size === stat.size) return;
const destPath = path.join(backupDir, descriptor.relPath); const destPath = path.join(backupDir, descriptor.relPath);
@ -572,10 +573,7 @@ export class TeamBackupService {
return count > 0; return count > 0;
} }
private async restoreGenericPartial( private async restoreGenericPartial(teamName: string, manifest: BackupManifest): Promise<number> {
teamName: string,
manifest: BackupManifest
): Promise<number> {
const backupDir = this.getBackupDir(teamName); const backupDir = this.getBackupDir(teamName);
const backupFiles = await this.enumerateBackupFiles(teamName); const backupFiles = await this.enumerateBackupFiles(teamName);
let count = 0; let count = 0;
@ -892,10 +890,20 @@ export class TeamBackupService {
return path.join(getTasksBasePath(), teamName, relPath.slice('tasks/'.length)); return path.join(getTasksBasePath(), teamName, relPath.slice('tasks/'.length));
} }
if (relPath.startsWith('attachments/')) { if (relPath.startsWith('attachments/')) {
return path.join(getAppDataPath(), 'attachments', teamName, relPath.slice('attachments/'.length)); return path.join(
getAppDataPath(),
'attachments',
teamName,
relPath.slice('attachments/'.length)
);
} }
if (relPath.startsWith('task-attachments/')) { if (relPath.startsWith('task-attachments/')) {
return path.join(getAppDataPath(), 'task-attachments', teamName, relPath.slice('task-attachments/'.length)); return path.join(
getAppDataPath(),
'task-attachments',
teamName,
relPath.slice('task-attachments/'.length)
);
} }
return path.join(getTeamsBasePath(), teamName, relPath); return path.join(getTeamsBasePath(), teamName, relPath);
} }

View file

@ -1,12 +1,11 @@
import { getHomeDir } from '@main/utils/pathDecoder';
import { createLogger } from '@shared/utils/logger';
import { execFile } from 'child_process'; import { execFile } from 'child_process';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import { getHomeDir } from '@main/utils/pathDecoder';
import { createLogger } from '@shared/utils/logger';
import { atomicWriteAsync } from './atomicWrite'; import { atomicWriteAsync } from './atomicWrite';
interface McpLaunchSpec { interface McpLaunchSpec {

View file

@ -1,9 +1,7 @@
/* eslint-disable no-param-reassign -- ProvisioningRun object is intentionally mutated as a state tracker throughout the provisioning lifecycle */ /* eslint-disable no-param-reassign -- ProvisioningRun object is intentionally mutated as a state tracker throughout the provisioning lifecycle */
import { ConfigManager } from '@main/services/infrastructure/ConfigManager'; import { ConfigManager } from '@main/services/infrastructure/ConfigManager';
import { killProcessTree, spawnCli } from '@main/utils/childProcess'; import { killProcessTree, spawnCli } from '@main/utils/childProcess';
import { shouldAutoAllow } from '@main/utils/toolApprovalRules';
import { FileReadTimeoutError, readFileUtf8WithTimeout } from '@main/utils/fsRead'; import { FileReadTimeoutError, readFileUtf8WithTimeout } from '@main/utils/fsRead';
import { resolveInteractiveShellEnv } from '@main/utils/shellEnv';
import { import {
encodePath, encodePath,
extractBaseDir, extractBaseDir,
@ -14,6 +12,8 @@ import {
getTasksBasePath, getTasksBasePath,
getTeamsBasePath, getTeamsBasePath,
} from '@main/utils/pathDecoder'; } from '@main/utils/pathDecoder';
import { resolveInteractiveShellEnv } from '@main/utils/shellEnv';
import { shouldAutoAllow } from '@main/utils/toolApprovalRules';
import { import {
AGENT_BLOCK_CLOSE, AGENT_BLOCK_CLOSE,
AGENT_BLOCK_OPEN, AGENT_BLOCK_OPEN,
@ -21,8 +21,8 @@ import {
} from '@shared/constants/agentBlocks'; } from '@shared/constants/agentBlocks';
import { import {
CROSS_TEAM_PREFIX_TAG, CROSS_TEAM_PREFIX_TAG,
CROSS_TEAM_SOURCE,
CROSS_TEAM_SENT_SOURCE, CROSS_TEAM_SENT_SOURCE,
CROSS_TEAM_SOURCE,
parseCrossTeamPrefix, parseCrossTeamPrefix,
stripCrossTeamPrefix, stripCrossTeamPrefix,
} from '@shared/constants/crossTeam'; } from '@shared/constants/crossTeam';
@ -41,14 +41,14 @@ import {
import { createCliAutoSuffixNameGuard, parseNumericSuffixName } from '@shared/utils/teamMemberName'; import { createCliAutoSuffixNameGuard, parseNumericSuffixName } from '@shared/utils/teamMemberName';
import { extractToolPreview, formatToolSummaryFromCalls } from '@shared/utils/toolSummary'; import { extractToolPreview, formatToolSummaryFromCalls } from '@shared/utils/toolSummary';
import * as agentTeamsControllerModule from 'agent-teams-controller'; import * as agentTeamsControllerModule from 'agent-teams-controller';
import { spawn, type ChildProcess } from 'child_process'; import { type ChildProcess, type spawn } from 'child_process';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import { atomicWriteAsync } from './atomicWrite';
import { buildActionModeProtocol } from './actionModeInstructions'; import { buildActionModeProtocol } from './actionModeInstructions';
import { atomicWriteAsync } from './atomicWrite';
import { ClaudeBinaryResolver } from './ClaudeBinaryResolver'; import { ClaudeBinaryResolver } from './ClaudeBinaryResolver';
import { withFileLock } from './fileLock'; import { withFileLock } from './fileLock';
import { withInboxLock } from './inboxLock'; import { withInboxLock } from './inboxLock';
@ -231,10 +231,10 @@ interface ProvisioningRun {
rejectOnce: (error: string) => void; rejectOnce: (error: string) => void;
timeoutHandle: NodeJS.Timeout; timeoutHandle: NodeJS.Timeout;
} | null; } | null;
activeCrossTeamReplyHints: Array<{ activeCrossTeamReplyHints: {
toTeam: string; toTeam: string;
conversationId: string; conversationId: string;
}>; }[];
/** Monotonic counter for individual lead assistant messages. */ /** Monotonic counter for individual lead assistant messages. */
leadMsgSeq: number; leadMsgSeq: number;
/** Accumulated tool_use details between text messages. */ /** Accumulated tool_use details between text messages. */
@ -1122,11 +1122,11 @@ interface CachedProbeResult {
cachedAtMs: number; cachedAtMs: number;
} }
type ProbeResult = { interface ProbeResult {
claudePath: string; claudePath: string;
authSource: ProvisioningAuthSource; authSource: ProvisioningAuthSource;
warning?: string; warning?: string;
}; }
type AuthWarningSource = 'probe' | 'stdout' | 'stderr' | 'assistant' | 'pre-complete'; type AuthWarningSource = 'probe' | 'stdout' | 'stderr' | 'assistant' | 'pre-complete';
@ -1607,21 +1607,21 @@ export class TeamProvisioningService {
private async matchCrossTeamLeadInboxMessages( private async matchCrossTeamLeadInboxMessages(
teamName: string, teamName: string,
leadName: string, leadName: string,
deliveredBlocks: Array<{ deliveredBlocks: {
teammateId: string; teammateId: string;
content: string; content: string;
toTeam: string; toTeam: string;
conversationId: string; conversationId: string;
}> }[]
): Promise< ): Promise<
Array<{ {
teammateId: string; teammateId: string;
content: string; content: string;
toTeam: string; toTeam: string;
conversationId: string; conversationId: string;
messageId: string; messageId: string;
wasRead: boolean; wasRead: boolean;
}> }[]
> { > {
if (deliveredBlocks.length === 0) return []; if (deliveredBlocks.length === 0) return [];
@ -1633,14 +1633,14 @@ export class TeamProvisioningService {
} }
const usedMessageIds = new Set<string>(); const usedMessageIds = new Set<string>();
const matches: Array<{ const matches: {
teammateId: string; teammateId: string;
content: string; content: string;
toTeam: string; toTeam: string;
conversationId: string; conversationId: string;
messageId: string; messageId: string;
wasRead: boolean; wasRead: boolean;
}> = []; }[] = [];
for (const block of deliveredBlocks) { for (const block of deliveredBlocks) {
const matchesBlock = (message: InboxMessage, requireExactText: boolean): boolean => { const matchesBlock = (message: InboxMessage, requireExactText: boolean): boolean => {
if (message.source !== CROSS_TEAM_SOURCE) return false; if (message.source !== CROSS_TEAM_SOURCE) return false;
@ -1816,7 +1816,7 @@ export class TeamProvisioningService {
private rememberPendingInboxRelayCandidates( private rememberPendingInboxRelayCandidates(
run: ProvisioningRun, run: ProvisioningRun,
recipient: string, recipient: string,
messages: Array<Pick<InboxMessage, 'messageId' | 'text' | 'summary'>> messages: Pick<InboxMessage, 'messageId' | 'text' | 'summary'>[]
): string[] { ): string[] {
const candidates = this.prunePendingInboxRelayCandidates(run); const candidates = this.prunePendingInboxRelayCandidates(run);
const queuedAtMs = Date.now(); const queuedAtMs = Date.now();
@ -4167,7 +4167,7 @@ export class TeamProvisioningService {
(mistakenToolHint ? { teamName: mistakenToolHint.toTeam, memberName: 'team-lead' } : null); (mistakenToolHint ? { teamName: mistakenToolHint.toTeam, memberName: 'team-lead' } : null);
if (crossTeamRecipient && this.crossTeamSender) { if (crossTeamRecipient && this.crossTeamSender) {
const inferredReplyMeta = const inferredReplyMeta =
mistakenToolHint && mistakenToolHint.toTeam === crossTeamRecipient.teamName mistakenToolHint?.toTeam === crossTeamRecipient.teamName
? { ? {
conversationId: mistakenToolHint.conversationId, conversationId: mistakenToolHint.conversationId,
replyToConversationId: mistakenToolHint.conversationId, replyToConversationId: mistakenToolHint.conversationId,

View file

@ -1,5 +1,5 @@
import { getTaskChangeSummariesBasePath } from '@main/utils/pathDecoder';
import { atomicWriteAsync } from '@main/utils/atomicWrite'; import { atomicWriteAsync } from '@main/utils/atomicWrite';
import { getTaskChangeSummariesBasePath } from '@main/utils/pathDecoder';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';

View file

@ -1,7 +1,7 @@
import { TASK_CHANGE_SUMMARY_CACHE_SCHEMA_VERSION } from './taskChangeSummaryCacheTypes'; import { TASK_CHANGE_SUMMARY_CACHE_SCHEMA_VERSION } from './taskChangeSummaryCacheTypes';
import type { FileChangeSummary, TaskChangeSetV2 } from '@shared/types';
import type { PersistedTaskChangeSummaryEntry } from './taskChangeSummaryCacheTypes'; import type { PersistedTaskChangeSummaryEntry } from './taskChangeSummaryCacheTypes';
import type { FileChangeSummary, TaskChangeSetV2 } from '@shared/types';
function normalizeIsoString(value: unknown): string | null { function normalizeIsoString(value: unknown): string | null {
if (typeof value !== 'string' || value.trim() === '') return null; if (typeof value !== 'string' || value.trim() === '') return null;

View file

@ -707,8 +707,8 @@ async function readTasksDirForTeam(
? (parsed.attachments as unknown[]) ? (parsed.attachments as unknown[])
: undefined, : undefined,
sourceMessageId: sourceMessageId:
typeof parsed.sourceMessageId === 'string' && (parsed.sourceMessageId as string).trim() typeof parsed.sourceMessageId === 'string' && parsed.sourceMessageId.trim()
? (parsed.sourceMessageId as string).trim() ? parsed.sourceMessageId.trim()
: undefined, : undefined,
sourceMessage: sourceMessage:
parsed.sourceMessage && parsed.sourceMessage &&

View file

@ -2,6 +2,11 @@ import { WINDOW_ZOOM_FACTOR_CHANGED_CHANNEL } from '@shared/constants';
import { contextBridge, ipcRenderer } from 'electron'; import { contextBridge, ipcRenderer } from 'electron';
import { import {
API_KEYS_DELETE,
API_KEYS_LIST,
API_KEYS_LOOKUP,
API_KEYS_SAVE,
API_KEYS_STORAGE_STATUS,
APP_RELAUNCH, APP_RELAUNCH,
CLI_INSTALLER_GET_STATUS, CLI_INSTALLER_GET_STATUS,
CLI_INSTALLER_INSTALL, CLI_INSTALLER_INSTALL,
@ -34,28 +39,30 @@ import {
HTTP_SERVER_GET_STATUS, HTTP_SERVER_GET_STATUS,
HTTP_SERVER_START, HTTP_SERVER_START,
HTTP_SERVER_STOP, HTTP_SERVER_STOP,
MCP_GITHUB_STARS,
MCP_REGISTRY_BROWSE,
MCP_REGISTRY_DIAGNOSE,
MCP_REGISTRY_GET_BY_ID,
MCP_REGISTRY_GET_INSTALLED,
MCP_REGISTRY_INSTALL,
MCP_REGISTRY_INSTALL_CUSTOM,
MCP_REGISTRY_SEARCH,
MCP_REGISTRY_UNINSTALL,
PLUGIN_GET_ALL,
PLUGIN_GET_README,
PLUGIN_INSTALL,
PLUGIN_UNINSTALL,
PROJECT_LIST_FILES, PROJECT_LIST_FILES,
RENDERER_BOOT, RENDERER_BOOT,
RENDERER_HEARTBEAT, RENDERER_HEARTBEAT,
RENDERER_LOG, RENDERER_LOG,
SCHEDULE_CHANGE,
SCHEDULE_CREATE,
SCHEDULE_DELETE,
SCHEDULE_GET,
SCHEDULE_GET_RUN_LOGS,
SCHEDULE_GET_RUNS,
SCHEDULE_LIST,
SCHEDULE_PAUSE,
SCHEDULE_RESUME,
SCHEDULE_TRIGGER_NOW,
SCHEDULE_UPDATE,
REVIEW_APPLY_DECISIONS, REVIEW_APPLY_DECISIONS,
REVIEW_CHECK_CONFLICT, REVIEW_CHECK_CONFLICT,
REVIEW_CLEAR_DECISIONS, REVIEW_CLEAR_DECISIONS,
REVIEW_FILE_CHANGE,
REVIEW_GET_AGENT_CHANGES, REVIEW_GET_AGENT_CHANGES,
REVIEW_GET_CHANGE_STATS, REVIEW_GET_CHANGE_STATS,
REVIEW_GET_FILE_CONTENT, REVIEW_GET_FILE_CONTENT,
REVIEW_FILE_CHANGE,
REVIEW_GET_GIT_FILE_LOG, REVIEW_GET_GIT_FILE_LOG,
REVIEW_GET_TASK_CHANGES, REVIEW_GET_TASK_CHANGES,
REVIEW_INVALIDATE_TASK_CHANGE_SUMMARIES, REVIEW_INVALIDATE_TASK_CHANGE_SUMMARIES,
@ -67,6 +74,27 @@ import {
REVIEW_SAVE_EDITED_FILE, REVIEW_SAVE_EDITED_FILE,
REVIEW_UNWATCH_FILES, REVIEW_UNWATCH_FILES,
REVIEW_WATCH_FILES, REVIEW_WATCH_FILES,
SCHEDULE_CHANGE,
SCHEDULE_CREATE,
SCHEDULE_DELETE,
SCHEDULE_GET,
SCHEDULE_GET_RUN_LOGS,
SCHEDULE_GET_RUNS,
SCHEDULE_LIST,
SCHEDULE_PAUSE,
SCHEDULE_RESUME,
SCHEDULE_TRIGGER_NOW,
SCHEDULE_UPDATE,
SKILLS_APPLY_IMPORT,
SKILLS_APPLY_UPSERT,
SKILLS_CHANGED,
SKILLS_DELETE,
SKILLS_GET_DETAIL,
SKILLS_LIST,
SKILLS_PREVIEW_IMPORT,
SKILLS_PREVIEW_UPSERT,
SKILLS_START_WATCHING,
SKILLS_STOP_WATCHING,
SSH_CONNECT, SSH_CONNECT,
SSH_DISCONNECT, SSH_DISCONNECT,
SSH_GET_CONFIG_HOSTS, SSH_GET_CONFIG_HOSTS,
@ -101,8 +129,8 @@ import {
TEAM_LAUNCH, TEAM_LAUNCH,
TEAM_LEAD_ACTIVITY, TEAM_LEAD_ACTIVITY,
TEAM_LEAD_CONTEXT, TEAM_LEAD_CONTEXT,
TEAM_MEMBER_SPAWN_STATUSES,
TEAM_LIST, TEAM_LIST,
TEAM_MEMBER_SPAWN_STATUSES,
TEAM_PERMANENTLY_DELETE, TEAM_PERMANENTLY_DELETE,
TEAM_PREPARE_PROVISIONING, TEAM_PREPARE_PROVISIONING,
TEAM_PROCESS_ALIVE, TEAM_PROCESS_ALIVE,
@ -149,34 +177,6 @@ import {
WINDOW_IS_MAXIMIZED, WINDOW_IS_MAXIMIZED,
WINDOW_MAXIMIZE, WINDOW_MAXIMIZE,
WINDOW_MINIMIZE, WINDOW_MINIMIZE,
PLUGIN_GET_ALL,
PLUGIN_GET_README,
PLUGIN_INSTALL,
PLUGIN_UNINSTALL,
MCP_REGISTRY_SEARCH,
MCP_REGISTRY_BROWSE,
MCP_REGISTRY_DIAGNOSE,
MCP_REGISTRY_GET_BY_ID,
MCP_REGISTRY_GET_INSTALLED,
MCP_REGISTRY_INSTALL,
MCP_REGISTRY_INSTALL_CUSTOM,
MCP_REGISTRY_UNINSTALL,
MCP_GITHUB_STARS,
SKILLS_APPLY_IMPORT,
SKILLS_APPLY_UPSERT,
SKILLS_CHANGED,
SKILLS_DELETE,
SKILLS_GET_DETAIL,
SKILLS_LIST,
SKILLS_PREVIEW_IMPORT,
SKILLS_PREVIEW_UPSERT,
SKILLS_START_WATCHING,
SKILLS_STOP_WATCHING,
API_KEYS_LIST,
API_KEYS_SAVE,
API_KEYS_DELETE,
API_KEYS_LOOKUP,
API_KEYS_STORAGE_STATUS,
} from './constants/ipcChannels'; } from './constants/ipcChannels';
import { import {
CONFIG_ADD_CUSTOM_PROJECT_PATH, CONFIG_ADD_CUSTOM_PROJECT_PATH,
@ -236,9 +236,9 @@ import type {
KanbanColumnId, KanbanColumnId,
LeadActivitySnapshot, LeadActivitySnapshot,
LeadContextUsageSnapshot, LeadContextUsageSnapshot,
MemberSpawnStatusesSnapshot,
MemberFullStats, MemberFullStats,
MemberLogSummary, MemberLogSummary,
MemberSpawnStatusesSnapshot,
NotificationTrigger, NotificationTrigger,
RejectResult, RejectResult,
ReplaceMembersRequest, ReplaceMembersRequest,
@ -281,28 +281,6 @@ import type {
UpdateSchedulePatch, UpdateSchedulePatch,
WslClaudeRootCandidate, WslClaudeRootCandidate,
} from '@shared/types'; } from '@shared/types';
import type {
ApiKeyEntry,
ApiKeyLookupResult,
ApiKeySaveRequest,
ApiKeyStorageStatus,
EnrichedPlugin,
InstalledMcpEntry,
McpCatalogItem,
McpCustomInstallRequest,
McpInstallRequest,
McpServerDiagnostic,
McpSearchResult,
OperationResult,
PluginInstallRequest,
SkillCatalogItem,
SkillDeleteRequest,
SkillDetail,
SkillImportRequest,
SkillReviewPreview,
SkillUpsertRequest,
SkillWatcherEvent,
} from '@shared/types/extensions';
import type { import type {
BinaryPreviewResult, BinaryPreviewResult,
CreateDirResponse, CreateDirResponse,
@ -318,6 +296,28 @@ import type {
SearchInFilesResult, SearchInFilesResult,
WriteFileResponse, WriteFileResponse,
} from '@shared/types/editor'; } from '@shared/types/editor';
import type {
ApiKeyEntry,
ApiKeyLookupResult,
ApiKeySaveRequest,
ApiKeyStorageStatus,
EnrichedPlugin,
InstalledMcpEntry,
McpCatalogItem,
McpCustomInstallRequest,
McpInstallRequest,
McpSearchResult,
McpServerDiagnostic,
OperationResult,
PluginInstallRequest,
SkillCatalogItem,
SkillDeleteRequest,
SkillDetail,
SkillImportRequest,
SkillReviewPreview,
SkillUpsertRequest,
SkillWatcherEvent,
} from '@shared/types/extensions';
import type { PtySpawnOptions } from '@shared/types/terminal'; import type { PtySpawnOptions } from '@shared/types/terminal';
import type { CliArgsValidationResult } from '@shared/utils/cliArgsParser'; import type { CliArgsValidationResult } from '@shared/utils/cliArgsParser';

View file

@ -27,7 +27,6 @@ import {
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors'; import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme'; import { useTheme } from '@renderer/hooks/useTheme';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import type { SearchMatch } from '@renderer/store/types';
import { REHYPE_PLUGINS, REHYPE_PLUGINS_NO_HIGHLIGHT } from '@renderer/utils/markdownPlugins'; import { REHYPE_PLUGINS, REHYPE_PLUGINS_NO_HIGHLIGHT } from '@renderer/utils/markdownPlugins';
import { nameColorSet } from '@renderer/utils/projectColor'; import { nameColorSet } from '@renderer/utils/projectColor';
import { parseTaskLinkHref } from '@renderer/utils/taskReferenceUtils'; import { parseTaskLinkHref } from '@renderer/utils/taskReferenceUtils';
@ -44,6 +43,8 @@ import {
import { FileLink, isRelativeUrl } from './FileLink'; import { FileLink, isRelativeUrl } from './FileLink';
import { MermaidDiagram } from './MermaidDiagram'; import { MermaidDiagram } from './MermaidDiagram';
import type { SearchMatch } from '@renderer/store/types';
// ============================================================================= // =============================================================================
// Types // Types
// ============================================================================= // =============================================================================
@ -69,7 +70,7 @@ interface MarkdownViewerProps {
onTeamClick?: (teamName: string) => void; onTeamClick?: (teamName: string) => void;
} }
const EMPTY_TEAMS: Array<{ teamName?: string; displayName?: string; color?: string }> = []; const EMPTY_TEAMS: { teamName?: string; displayName?: string; color?: string }[] = [];
const EMPTY_TEAM_COLOR_MAP = new Map<string, string>(); const EMPTY_TEAM_COLOR_MAP = new Map<string, string>();
const EMPTY_SEARCH_MATCHES: SearchMatch[] = []; const EMPTY_SEARCH_MATCHES: SearchMatch[] = [];
const NOOP_TEAM_CLICK = (): void => undefined; const NOOP_TEAM_CLICK = (): void => undefined;
@ -94,31 +95,136 @@ function allowCustomProtocols(url: string): string {
* that appear in agent messages and cause React "unrecognized tag" warnings. * that appear in agent messages and cause React "unrecognized tag" warnings.
*/ */
const STANDARD_HTML_TAGS = new Set([ const STANDARD_HTML_TAGS = new Set([
'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'a',
'b', 'base', 'bdi', 'bdo', 'blockquote', 'body', 'br', 'button', 'abbr',
'canvas', 'caption', 'cite', 'code', 'col', 'colgroup', 'address',
'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'area',
'em', 'embed', 'article',
'fieldset', 'figcaption', 'figure', 'footer', 'form', 'aside',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'audio',
'i', 'iframe', 'img', 'input', 'ins', 'b',
'base',
'bdi',
'bdo',
'blockquote',
'body',
'br',
'button',
'canvas',
'caption',
'cite',
'code',
'col',
'colgroup',
'data',
'datalist',
'dd',
'del',
'details',
'dfn',
'dialog',
'div',
'dl',
'dt',
'em',
'embed',
'fieldset',
'figcaption',
'figure',
'footer',
'form',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'head',
'header',
'hgroup',
'hr',
'html',
'i',
'iframe',
'img',
'input',
'ins',
'kbd', 'kbd',
'label', 'legend', 'li', 'link', 'label',
'main', 'map', 'mark', 'menu', 'meta', 'meter', 'legend',
'nav', 'noscript', 'li',
'object', 'ol', 'optgroup', 'option', 'output', 'link',
'p', 'picture', 'pre', 'progress', 'main',
'map',
'mark',
'menu',
'meta',
'meter',
'nav',
'noscript',
'object',
'ol',
'optgroup',
'option',
'output',
'p',
'picture',
'pre',
'progress',
'q', 'q',
'rp', 'rt', 'ruby', 'rp',
's', 'samp', 'script', 'search', 'section', 'select', 'slot', 'small', 'source', 'span', 'rt',
'strong', 'style', 'sub', 'summary', 'sup', 'ruby',
'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 's',
'u', 'ul', 'samp',
'var', 'video', 'script',
'search',
'section',
'select',
'slot',
'small',
'source',
'span',
'strong',
'style',
'sub',
'summary',
'sup',
'table',
'tbody',
'td',
'template',
'textarea',
'tfoot',
'th',
'thead',
'time',
'title',
'tr',
'track',
'u',
'ul',
'var',
'video',
'wbr', 'wbr',
// SVG elements commonly used inline // SVG elements commonly used inline
'svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'g', 'defs', 'use', 'svg',
'text', 'tspan', 'clippath', 'mask', 'pattern', 'image', 'foreignobject', 'path',
'circle',
'rect',
'line',
'polyline',
'polygon',
'g',
'defs',
'use',
'text',
'tspan',
'clippath',
'mask',
'pattern',
'image',
'foreignobject',
]); ]);
/** /**

View file

@ -1,14 +1,13 @@
import React, { Component, type ErrorInfo, type ReactNode } from 'react'; import React, { Component, type ErrorInfo, type ReactNode } from 'react';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { createLogger } from '@shared/utils/logger';
import { AlertTriangle, Bug, Check, Copy, RefreshCw } from 'lucide-react';
import { import {
type BugReportContext,
buildBugReportText, buildBugReportText,
buildGitHubBugReportUrl, buildGitHubBugReportUrl,
type BugReportContext,
} from '@renderer/utils/bugReportUtils'; } from '@renderer/utils/bugReportUtils';
import { createLogger } from '@shared/utils/logger';
import { AlertTriangle, Bug, Check, Copy, RefreshCw } from 'lucide-react';
const logger = createLogger('Component:ErrorBoundary'); const logger = createLogger('Component:ErrorBoundary');

View file

@ -8,9 +8,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { api } from '@renderer/api'; import { api } from '@renderer/api';
import { Button } from '@renderer/components/ui/button'; import { Button } from '@renderer/components/ui/button';
import { useTabIdOptional } from '@renderer/contexts/useTabUIContext';
import { useExtensionsTabState } from '@renderer/hooks/useExtensionsTabState';
import { useStore } from '@renderer/store';
import { Tabs, TabsContent, TabsList } from '@renderer/components/ui/tabs'; import { Tabs, TabsContent, TabsList } from '@renderer/components/ui/tabs';
import { import {
Tooltip, Tooltip,
@ -18,14 +15,17 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from '@renderer/components/ui/tooltip'; } from '@renderer/components/ui/tooltip';
import { useTabIdOptional } from '@renderer/contexts/useTabUIContext';
import { useExtensionsTabState } from '@renderer/hooks/useExtensionsTabState';
import { useStore } from '@renderer/store';
import { AlertTriangle, BookOpen, Info, Key, Plus, Puzzle, RefreshCw, Server } from 'lucide-react'; import { AlertTriangle, BookOpen, Info, Key, Plus, Puzzle, RefreshCw, Server } from 'lucide-react';
import { ApiKeysPanel } from './apikeys/ApiKeysPanel'; import { ApiKeysPanel } from './apikeys/ApiKeysPanel';
import { ExtensionsSubTabTrigger } from './ExtensionsSubTabTrigger';
import { CustomMcpServerDialog } from './mcp/CustomMcpServerDialog'; import { CustomMcpServerDialog } from './mcp/CustomMcpServerDialog';
import { McpServersPanel } from './mcp/McpServersPanel'; import { McpServersPanel } from './mcp/McpServersPanel';
import { PluginsPanel } from './plugins/PluginsPanel'; import { PluginsPanel } from './plugins/PluginsPanel';
import { SkillsPanel } from './skills/SkillsPanel'; import { SkillsPanel } from './skills/SkillsPanel';
import { ExtensionsSubTabTrigger } from './ExtensionsSubTabTrigger';
export const ExtensionStoreView = (): React.JSX.Element => { export const ExtensionStoreView = (): React.JSX.Element => {
const tabId = useTabIdOptional(); const tabId = useTabIdOptional();

View file

@ -1,10 +1,9 @@
import type { LucideIcon } from 'lucide-react';
import { TabsTrigger } from '@renderer/components/ui/tabs'; import { TabsTrigger } from '@renderer/components/ui/tabs';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { Info } from 'lucide-react'; import { Info } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
interface ExtensionsSubTabTriggerProps { interface ExtensionsSubTabTriggerProps {
value: 'plugins' | 'mcp-servers' | 'skills' | 'api-keys'; value: 'plugins' | 'mcp-servers' | 'skills' | 'api-keys';
label: string; label: string;
@ -21,7 +20,7 @@ export const ExtensionsSubTabTrigger = ({
return ( return (
<TabsTrigger <TabsTrigger
value={value} value={value}
className="relative gap-1.5 rounded-b-none pr-7 data-[state=active]:z-10 data-[state=active]:-mb-px data-[state=active]:bg-[var(--color-surface)] data-[state=active]:shadow-none data-[state=active]:after:absolute data-[state=active]:after:-bottom-px data-[state=active]:after:left-0 data-[state=active]:after:right-0 data-[state=active]:after:h-1 data-[state=active]:after:bg-[var(--color-surface)] data-[state=active]:after:content-['']" className="relative gap-1.5 rounded-b-none pr-7 data-[state=active]:z-10 data-[state=active]:-mb-px data-[state=active]:bg-[var(--color-surface)] data-[state=active]:shadow-none data-[state=active]:after:absolute data-[state=active]:after:inset-x-0 data-[state=active]:after:-bottom-px data-[state=active]:after:h-1 data-[state=active]:after:bg-[var(--color-surface)] data-[state=active]:after:content-['']"
> >
<Icon className="size-3.5" /> <Icon className="size-3.5" />
{label} {label}

View file

@ -13,7 +13,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from '@renderer/components/ui/tooltip'; } from '@renderer/components/ui/tooltip';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { Copy, Check, Pencil, Trash2 } from 'lucide-react'; import { Check, Copy, Pencil, Trash2 } from 'lucide-react';
import type { ApiKeyEntry } from '@shared/types/extensions'; import type { ApiKeyEntry } from '@shared/types/extensions';

View file

@ -5,8 +5,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Check, Loader2, Trash2 } from 'lucide-react';
import { Button } from '@renderer/components/ui/button'; import { Button } from '@renderer/components/ui/button';
import { import {
Tooltip, Tooltip,
@ -15,6 +13,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from '@renderer/components/ui/tooltip'; } from '@renderer/components/ui/tooltip';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { Check, Loader2, Trash2 } from 'lucide-react';
import type { ExtensionOperationState } from '@shared/types/extensions'; import type { ExtensionOperationState } from '@shared/types/extensions';
@ -28,7 +27,7 @@ interface InstallButtonProps {
errorMessage?: string; errorMessage?: string;
} }
export function InstallButton({ export const InstallButton = ({
state, state,
isInstalled, isInstalled,
onInstall, onInstall,
@ -36,7 +35,7 @@ export function InstallButton({
disabled, disabled,
size = 'sm', size = 'sm',
errorMessage, errorMessage,
}: InstallButtonProps) { }: InstallButtonProps) => {
const cliStatus = useStore((s) => s.cliStatus); const cliStatus = useStore((s) => s.cliStatus);
const cliMissing = cliStatus !== null && !cliStatus.installed; const cliMissing = cliStatus !== null && !cliStatus.installed;
const isDisabled = disabled || cliMissing; const isDisabled = disabled || cliMissing;
@ -52,7 +51,7 @@ export function InstallButton({
if (state === 'pending') { if (state === 'pending') {
return ( return (
<Button size={size} variant="outline" disabled> <Button size={size} variant="outline" disabled>
<Loader2 className="h-3.5 w-3.5 animate-spin" /> <Loader2 className="size-3.5 animate-spin" />
<span className="ml-1.5"> <span className="ml-1.5">
{pendingAction === 'uninstall' ? 'Removing...' : 'Installing...'} {pendingAction === 'uninstall' ? 'Removing...' : 'Installing...'}
</span> </span>
@ -63,7 +62,7 @@ export function InstallButton({
if (state === 'success') { if (state === 'success') {
return ( return (
<Button size={size} variant="outline" disabled className="text-green-400"> <Button size={size} variant="outline" disabled className="text-green-400">
<Check className="h-3.5 w-3.5" /> <Check className="size-3.5" />
<span className="ml-1.5">Done</span> <span className="ml-1.5">Done</span>
</Button> </Button>
); );
@ -121,7 +120,7 @@ export function InstallButton({
}} }}
disabled={isDisabled} disabled={isDisabled}
> >
<Trash2 className="h-3.5 w-3.5" /> <Trash2 className="size-3.5" />
<span className="ml-1.5">Uninstall</span> <span className="ml-1.5">Uninstall</span>
</Button> </Button>
) : ( ) : (
@ -153,4 +152,4 @@ export function InstallButton({
} }
return button; return button;
} };

View file

@ -2,9 +2,8 @@
* InstallCountBadge formatted download count with icon. * InstallCountBadge formatted download count with icon.
*/ */
import { Download } from 'lucide-react';
import { formatInstallCount } from '@shared/utils/extensionNormalizers'; import { formatInstallCount } from '@shared/utils/extensionNormalizers';
import { Download } from 'lucide-react';
interface InstallCountBadgeProps { interface InstallCountBadgeProps {
count: number; count: number;

View file

@ -5,6 +5,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { api } from '@renderer/api';
import { Button } from '@renderer/components/ui/button'; import { Button } from '@renderer/components/ui/button';
import { import {
Dialog, Dialog,
@ -23,7 +24,6 @@ import {
SelectValue, SelectValue,
} from '@renderer/components/ui/select'; } from '@renderer/components/ui/select';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { api } from '@renderer/api';
import { Plus, Server, Trash2 } from 'lucide-react'; import { Plus, Server, Trash2 } from 'lucide-react';
import type { import type {

View file

@ -5,20 +5,18 @@
import { useState } from 'react'; import { useState } from 'react';
import { api } from '@renderer/api';
import { Badge } from '@renderer/components/ui/badge'; import { Badge } from '@renderer/components/ui/badge';
import { Button } from '@renderer/components/ui/button'; import { Button } from '@renderer/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { api } from '@renderer/api';
import { formatCompactNumber, formatRelativeTime } from '@renderer/utils/formatters'; import { formatCompactNumber, formatRelativeTime } from '@renderer/utils/formatters';
import { Cloud, Clock, Globe, KeyRound, Lock, Monitor, Star, Tag, Wrench } from 'lucide-react'; import { sanitizeMcpServerName } from '@shared/utils/extensionNormalizers';
import { Clock, Cloud, Globe, KeyRound, Lock, Monitor, Star, Tag, Wrench } from 'lucide-react';
// eslint-disable-next-line @typescript-eslint/no-deprecated -- lucide naming migration, alias is stable
import { Github as GithubIcon } from 'lucide-react'; import { Github as GithubIcon } from 'lucide-react';
import { InstallButton } from '../common/InstallButton'; import { InstallButton } from '../common/InstallButton';
import { SourceBadge } from '../common/SourceBadge'; import { SourceBadge } from '../common/SourceBadge';
import { sanitizeMcpServerName } from '@shared/utils/extensionNormalizers';
import type { McpCatalogItem, McpServerDiagnostic } from '@shared/types/extensions'; import type { McpCatalogItem, McpServerDiagnostic } from '@shared/types/extensions';
@ -82,7 +80,7 @@ export const McpServerCard = ({
{hasIcon && ( {hasIcon && (
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg border border-border bg-surface-raised"> <div className="flex size-9 shrink-0 items-center justify-center rounded-lg border border-border bg-surface-raised">
<img <img
src={server.iconUrl!} src={server.iconUrl}
alt="" alt=""
className="size-7 rounded object-contain" className="size-7 rounded object-contain"
onError={() => setImgError(true)} onError={() => setImgError(true)}

View file

@ -5,6 +5,9 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { api } from '@renderer/api';
import { Badge } from '@renderer/components/ui/badge';
import { Button } from '@renderer/components/ui/button';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -12,8 +15,6 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@renderer/components/ui/dialog'; } from '@renderer/components/ui/dialog';
import { Badge } from '@renderer/components/ui/badge';
import { Button } from '@renderer/components/ui/button';
import { Input } from '@renderer/components/ui/input'; import { Input } from '@renderer/components/ui/input';
import { Label } from '@renderer/components/ui/label'; import { Label } from '@renderer/components/ui/label';
import { import {
@ -24,12 +25,11 @@ import {
SelectValue, SelectValue,
} from '@renderer/components/ui/select'; } from '@renderer/components/ui/select';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { api } from '@renderer/api'; import { sanitizeMcpServerName } from '@shared/utils/extensionNormalizers';
import { ExternalLink, Lock, Plus, Star, Trash2, Wrench } from 'lucide-react'; import { ExternalLink, Lock, Plus, Star, Trash2, Wrench } from 'lucide-react';
import { InstallButton } from '../common/InstallButton'; import { InstallButton } from '../common/InstallButton';
import { SourceBadge } from '../common/SourceBadge'; import { SourceBadge } from '../common/SourceBadge';
import { sanitizeMcpServerName } from '@shared/utils/extensionNormalizers';
import type { McpCatalogItem, McpHeaderDef, McpServerDiagnostic } from '@shared/types/extensions'; import type { McpCatalogItem, McpHeaderDef, McpServerDiagnostic } from '@shared/types/extensions';
@ -214,7 +214,7 @@ export const McpServerDetailDialog = ({
{hasIcon && ( {hasIcon && (
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg border border-border bg-surface-raised"> <div className="flex size-10 shrink-0 items-center justify-center rounded-lg border border-border bg-surface-raised">
<img <img
src={server.iconUrl!} src={server.iconUrl}
alt="" alt=""
className="size-8 rounded object-contain" className="size-8 rounded object-contain"
onError={() => setImgError(true)} onError={() => setImgError(true)}

View file

@ -15,6 +15,7 @@ import {
} from '@renderer/components/ui/select'; } from '@renderer/components/ui/select';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { formatRelativeTime } from '@renderer/utils/formatters'; import { formatRelativeTime } from '@renderer/utils/formatters';
import { sanitizeMcpServerName } from '@shared/utils/extensionNormalizers';
import { AlertTriangle, RefreshCw, Search, Server } from 'lucide-react'; import { AlertTriangle, RefreshCw, Search, Server } from 'lucide-react';
import { SearchInput } from '../common/SearchInput'; import { SearchInput } from '../common/SearchInput';
@ -27,7 +28,6 @@ import type {
McpCatalogItem, McpCatalogItem,
McpServerDiagnostic, McpServerDiagnostic,
} from '@shared/types/extensions'; } from '@shared/types/extensions';
import { sanitizeMcpServerName } from '@shared/utils/extensionNormalizers';
type McpSortValue = 'name-asc' | 'name-desc' | 'tools-desc'; type McpSortValue = 'name-asc' | 'name-desc' | 'tools-desc';

View file

@ -53,8 +53,8 @@ export const PluginCard = ({ plugin, index, onClick }: PluginCardProps): React.J
}`} }`}
> >
{plugin.source === 'official' && ( {plugin.source === 'official' && (
<div className="pointer-events-none absolute -left-[1px] -top-[1px] size-16 overflow-hidden"> <div className="pointer-events-none absolute -left-px -top-px size-16 overflow-hidden">
<div className="absolute left-[-24px] top-[4px] w-[80px] rotate-[-45deg] bg-blue-500/90 text-center text-[9px] font-semibold leading-[18px] text-white shadow-sm"> <div className="absolute left-[-24px] top-[4px] w-[80px] -rotate-45 bg-blue-500/90 text-center text-[9px] font-semibold leading-[18px] text-white shadow-sm">
Official Official
</div> </div>
</div> </div>

View file

@ -4,6 +4,10 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { api } from '@renderer/api';
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
import { Badge } from '@renderer/components/ui/badge';
import { Button } from '@renderer/components/ui/button';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -11,8 +15,6 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@renderer/components/ui/dialog'; } from '@renderer/components/ui/dialog';
import { Badge } from '@renderer/components/ui/badge';
import { Button } from '@renderer/components/ui/button';
import { Label } from '@renderer/components/ui/label'; import { Label } from '@renderer/components/ui/label';
import { import {
Select, Select,
@ -22,7 +24,6 @@ import {
SelectValue, SelectValue,
} from '@renderer/components/ui/select'; } from '@renderer/components/ui/select';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { api } from '@renderer/api';
import { import {
getCapabilityLabel, getCapabilityLabel,
inferCapabilities, inferCapabilities,
@ -34,8 +35,6 @@ import { InstallButton } from '../common/InstallButton';
import { InstallCountBadge } from '../common/InstallCountBadge'; import { InstallCountBadge } from '../common/InstallCountBadge';
import { SourceBadge } from '../common/SourceBadge'; import { SourceBadge } from '../common/SourceBadge';
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
import type { EnrichedPlugin, InstallScope } from '@shared/types/extensions'; import type { EnrichedPlugin, InstallScope } from '@shared/types/extensions';
interface PluginDetailDialogProps { interface PluginDetailDialogProps {

View file

@ -18,8 +18,8 @@ import {
keymap, keymap,
lineNumbers, lineNumbers,
} from '@codemirror/view'; } from '@codemirror/view';
import { baseEditorTheme } from '@renderer/utils/codemirrorTheme';
import { getSyncLanguageExtension } from '@renderer/utils/codemirrorLanguages'; import { getSyncLanguageExtension } from '@renderer/utils/codemirrorLanguages';
import { baseEditorTheme } from '@renderer/utils/codemirrorTheme';
const skillEditorTheme = EditorView.theme({ const skillEditorTheme = EditorView.theme({
'&': { '&': {

View file

@ -26,13 +26,13 @@ import { useStore } from '@renderer/store';
import { FileSearch, RotateCcw, X } from 'lucide-react'; import { FileSearch, RotateCcw, X } from 'lucide-react';
import { SkillCodeEditor } from './SkillCodeEditor'; import { SkillCodeEditor } from './SkillCodeEditor';
import { SkillReviewDialog } from './SkillReviewDialog';
import { import {
buildSkillDraftFiles, buildSkillDraftFiles,
buildSkillTemplate, buildSkillTemplate,
readSkillTemplateContent, readSkillTemplateContent,
updateSkillTemplateFrontmatter, updateSkillTemplateFrontmatter,
} from './skillDraftUtils'; } from './skillDraftUtils';
import { SkillReviewDialog } from './SkillReviewDialog';
import type { import type {
SkillDetail, SkillDetail,
@ -792,7 +792,7 @@ export const SkillEditorDialog = ({
<X className="mr-1.5 size-3.5" /> <X className="mr-1.5 size-3.5" />
Cancel Cancel
</Button> </Button>
<div className="min-w-[16rem] flex-1"> <div className="min-w-64 flex-1">
<p className="text-sm text-text-muted"> <p className="text-sm text-text-muted">
Review the file changes first, then confirm save in the next step. Review the file changes first, then confirm save in the next step.
</p> </p>

View file

@ -255,7 +255,7 @@ export const SkillImportDialog = ({
<X className="mr-1.5 size-3.5" /> <X className="mr-1.5 size-3.5" />
Cancel Cancel
</Button> </Button>
<p className="min-w-[16rem] flex-1 text-sm text-text-muted"> <p className="min-w-64 flex-1 text-sm text-text-muted">
Review the copied files first, then confirm the import in the next step. Review the copied files first, then confirm the import in the next step.
</p> </p>
<Button <Button

View file

@ -4,8 +4,8 @@ import { api } from '@renderer/api';
import { Badge } from '@renderer/components/ui/badge'; import { Badge } from '@renderer/components/ui/badge';
import { Button } from '@renderer/components/ui/button'; import { Button } from '@renderer/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover';
import { useStore } from '@renderer/store';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { useStore } from '@renderer/store';
import { import {
AlertTriangle, AlertTriangle,
ArrowUpAZ, ArrowUpAZ,
@ -25,8 +25,8 @@ import { SkillDetailDialog } from './SkillDetailDialog';
import { SkillEditorDialog } from './SkillEditorDialog'; import { SkillEditorDialog } from './SkillEditorDialog';
import { SkillImportDialog } from './SkillImportDialog'; import { SkillImportDialog } from './SkillImportDialog';
import type { SkillCatalogItem, SkillDetail } from '@shared/types/extensions';
import type { SkillsSortState } from '@renderer/hooks/useExtensionsTabState'; import type { SkillsSortState } from '@renderer/hooks/useExtensionsTabState';
import type { SkillCatalogItem, SkillDetail } from '@shared/types/extensions';
const SUCCESS_BANNER_MS = 2500; const SUCCESS_BANNER_MS = 2500;
const NEW_SKILL_HIGHLIGHT_MS = 4000; const NEW_SKILL_HIGHLIGHT_MS = 4000;
@ -233,7 +233,7 @@ export const SkillsPanel = ({
<div className="flex w-full flex-col gap-3 xl:w-auto xl:min-w-[32rem] xl:max-w-[40rem]"> <div className="flex w-full flex-col gap-3 xl:w-auto xl:min-w-[32rem] xl:max-w-[40rem]">
<div className="flex flex-col gap-3 lg:flex-row lg:flex-wrap lg:items-center xl:justify-end"> <div className="flex flex-col gap-3 lg:flex-row lg:flex-wrap lg:items-center xl:justify-end">
<div className="w-full lg:min-w-[18rem] lg:flex-1 xl:w-80 xl:flex-none"> <div className="w-full lg:min-w-72 lg:flex-1 xl:w-80 xl:flex-none">
<SearchInput <SearchInput
value={skillsSearchQuery} value={skillsSearchQuery}
onChange={setSkillsSearchQuery} onChange={setSkillsSearchQuery}
@ -318,7 +318,7 @@ export const SkillsPanel = ({
['personal', 'Personal'], ['personal', 'Personal'],
['needs-attention', 'Needs attention'], ['needs-attention', 'Needs attention'],
['has-scripts', 'Has scripts'], ['has-scripts', 'Has scripts'],
] as Array<[SkillsQuickFilter, string]> ] as [SkillsQuickFilter, string][]
).map(([value, label]) => ( ).map(([value, label]) => (
<Button <Button
key={value} key={value}

View file

@ -65,7 +65,7 @@ export function buildSkillTemplate(input: SkillDraftTemplateInput): string {
export function readSkillTemplateContent(rawContent: string): SkillTemplateParseResult { export function readSkillTemplateContent(rawContent: string): SkillTemplateParseResult {
const content = rawContent.replace(/^\uFEFF/u, ''); const content = rawContent.replace(/^\uFEFF/u, '');
const match = content.match(SKILL_FRONTMATTER_PATTERN); const match = SKILL_FRONTMATTER_PATTERN.exec(content);
if (!match) { if (!match) {
return { return {
hasStructuredSections: false, hasStructuredSections: false,
@ -130,7 +130,7 @@ export function updateSkillTemplateFrontmatter(
input: SkillDraftTemplateInput input: SkillDraftTemplateInput
): string { ): string {
const content = rawContent.replace(/^\uFEFF/u, ''); const content = rawContent.replace(/^\uFEFF/u, '');
const match = content.match(SKILL_FRONTMATTER_PATTERN); const match = SKILL_FRONTMATTER_PATTERN.exec(content);
const body = match ? (match[2] ?? '') : content; const body = match ? (match[2] ?? '') : content;
let data: Record<string, unknown> = {}; let data: Record<string, unknown> = {};

View file

@ -8,7 +8,7 @@ import { useMemo, useState } from 'react';
import { isElectronMode } from '@renderer/api'; import { isElectronMode } from '@renderer/api';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { Bell, Puzzle, Settings, Users, PanelRight } from 'lucide-react'; import { Bell, PanelRight, Puzzle, Settings, Users } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { MoreMenu } from './MoreMenu'; import { MoreMenu } from './MoreMenu';

View file

@ -23,10 +23,10 @@ import {
Zap, Zap,
} from 'lucide-react'; } from 'lucide-react';
import { LaunchTeamDialog } from '../team/dialogs/LaunchTeamDialog';
import { ScheduleRunLogDialog } from '../team/schedule/ScheduleRunLogDialog'; import { ScheduleRunLogDialog } from '../team/schedule/ScheduleRunLogDialog';
import { ScheduleRunRow } from '../team/schedule/ScheduleRunRow'; import { ScheduleRunRow } from '../team/schedule/ScheduleRunRow';
import { ScheduleStatusBadge } from '../team/schedule/ScheduleStatusBadge'; import { ScheduleStatusBadge } from '../team/schedule/ScheduleStatusBadge';
import { LaunchTeamDialog } from '../team/dialogs/LaunchTeamDialog';
import type { Schedule, ScheduleRun, ScheduleStatus } from '@shared/types'; import type { Schedule, ScheduleRun, ScheduleStatus } from '@shared/types';
@ -34,7 +34,7 @@ import type { Schedule, ScheduleRun, ScheduleStatus } from '@shared/types';
// Constants // Constants
// ============================================================================= // =============================================================================
const STATUS_OPTIONS: Array<{ value: ScheduleStatus | 'all'; label: string }> = [ const STATUS_OPTIONS: { value: ScheduleStatus | 'all'; label: string }[] = [
{ value: 'all', label: 'All' }, { value: 'all', label: 'All' },
{ value: 'active', label: 'Active' }, { value: 'active', label: 'Active' },
{ value: 'paused', label: 'Paused' }, { value: 'paused', label: 'Paused' },

View file

@ -181,7 +181,7 @@ export const ConfigEditorDialog = ({
keymap.of([...defaultKeymap, ...historyKeymap, ...foldKeymap, ...searchKeymap]), keymap.of([...defaultKeymap, ...historyKeymap, ...foldKeymap, ...searchKeymap]),
baseEditorTheme, baseEditorTheme,
configEditorTheme, configEditorTheme,
// eslint-disable-next-line sonarjs/no-nested-functions -- CodeMirror listener callback within useEffect setup
EditorView.updateListener.of((update) => { EditorView.updateListener.of((update) => {
if (update.docChanged) { if (update.docChanged) {
const text = update.state.doc.toString(); const text = update.state.doc.toString();

View file

@ -14,18 +14,18 @@ import {
ArrowRightLeft, ArrowRightLeft,
Bell, Bell,
BellRing, BellRing,
CheckCircle2,
CirclePlus,
Clock, Clock,
ExternalLink, ExternalLink,
EyeOff, EyeOff,
GitBranch,
HelpCircle, HelpCircle,
Inbox, Inbox,
Info, Info,
Mail, Mail,
MessageSquare, MessageSquare,
PartyPopper, PartyPopper,
CirclePlus,
CheckCircle2,
GitBranch,
Send, Send,
Users, Users,
Volume2, Volume2,
@ -39,7 +39,9 @@ import type { NotificationTrigger } from '@renderer/types/data';
import type { TeamReviewState, TeamTaskStatus } from '@shared/types'; import type { TeamReviewState, TeamTaskStatus } from '@shared/types';
/** Notification targets span workflow status plus the explicit review axis. */ /** Notification targets span workflow status plus the explicit review axis. */
type NotifiableStatus = TeamTaskStatus | Extract<TeamReviewState, 'review' | 'needsFix' | 'approved'>; type NotifiableStatus =
| TeamTaskStatus
| Extract<TeamReviewState, 'review' | 'needsFix' | 'approved'>;
// Snooze duration options // Snooze duration options
const SNOOZE_OPTIONS = [ const SNOOZE_OPTIONS = [

View file

@ -358,7 +358,6 @@ export const GlobalTaskList = ({
// Reset showArchived when archive becomes empty // Reset showArchived when archive becomes empty
useEffect(() => { useEffect(() => {
if (showArchived && !hasArchivedTasks) { if (showArchived && !hasArchivedTasks) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional sync on prop change
setShowArchived(false); setShowArchived(false);
} }
}, [showArchived, hasArchivedTasks]); }, [showArchived, hasArchivedTasks]);

View file

@ -5,7 +5,7 @@ import { getTeamColorSet } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme'; import { useTheme } from '@renderer/hooks/useTheme';
import { useUnreadCommentCount } from '@renderer/hooks/useUnreadCommentCount'; import { useUnreadCommentCount } from '@renderer/hooks/useUnreadCommentCount';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { REVIEW_STATE_DISPLAY, buildMemberColorMap } from '@renderer/utils/memberHelpers'; import { buildMemberColorMap, REVIEW_STATE_DISPLAY } from '@renderer/utils/memberHelpers';
import { nameColorSet } from '@renderer/utils/projectColor'; import { nameColorSet } from '@renderer/utils/projectColor';
import { projectColor } from '@renderer/utils/projectColor'; import { projectColor } from '@renderer/utils/projectColor';
import { projectLabelFromPath } from '@renderer/utils/taskGrouping'; import { projectLabelFromPath } from '@renderer/utils/taskGrouping';

View file

@ -4,7 +4,7 @@ import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer
import { MemberBadge } from '@renderer/components/team/MemberBadge'; import { MemberBadge } from '@renderer/components/team/MemberBadge';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { REVIEW_STATE_DISPLAY, buildMemberColorMap } from '@renderer/utils/memberHelpers'; import { buildMemberColorMap, REVIEW_STATE_DISPLAY } from '@renderer/utils/memberHelpers';
import { linkifyTaskIdsInMarkdown } from '@renderer/utils/taskReferenceUtils'; import { linkifyTaskIdsInMarkdown } from '@renderer/utils/taskReferenceUtils';
import { getTaskKanbanColumn } from '@shared/utils/reviewState'; import { getTaskKanbanColumn } from '@shared/utils/reviewState';
import { formatTaskDisplayLabel, taskMatchesRef } from '@shared/utils/taskIdentity'; import { formatTaskDisplayLabel, taskMatchesRef } from '@shared/utils/taskIdentity';

View file

@ -32,6 +32,7 @@ import {
type TaskChangeRequestOptions, type TaskChangeRequestOptions,
} from '@renderer/utils/taskChangeRequest'; } from '@renderer/utils/taskChangeRequest';
import { stripAgentBlocks } from '@shared/constants/agentBlocks'; import { stripAgentBlocks } from '@shared/constants/agentBlocks';
import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection';
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import { import {
AlertTriangle, AlertTriangle,
@ -50,11 +51,9 @@ import {
UserPlus, UserPlus,
Users, Users,
} from 'lucide-react'; } from 'lucide-react';
import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { AddMemberDialog } from './dialogs/AddMemberDialog'; import { AddMemberDialog } from './dialogs/AddMemberDialog';
import type { AddMemberEntry } from './dialogs/AddMemberDialog';
import { CreateTaskDialog } from './dialogs/CreateTaskDialog'; import { CreateTaskDialog } from './dialogs/CreateTaskDialog';
import { EditTeamDialog } from './dialogs/EditTeamDialog'; import { EditTeamDialog } from './dialogs/EditTeamDialog';
import { LaunchTeamDialog } from './dialogs/LaunchTeamDialog'; import { LaunchTeamDialog } from './dialogs/LaunchTeamDialog';
@ -67,16 +66,18 @@ import { KanbanSearchInput } from './kanban/KanbanSearchInput';
import { TrashDialog } from './kanban/TrashDialog'; import { TrashDialog } from './kanban/TrashDialog';
import { MemberDetailDialog } from './members/MemberDetailDialog'; import { MemberDetailDialog } from './members/MemberDetailDialog';
import type { AddMemberEntry } from './dialogs/AddMemberDialog';
const ProjectEditorOverlay = lazy(() => const ProjectEditorOverlay = lazy(() =>
import('./editor/ProjectEditorOverlay').then((m) => ({ default: m.ProjectEditorOverlay })) import('./editor/ProjectEditorOverlay').then((m) => ({ default: m.ProjectEditorOverlay }))
); );
import { MemberList } from './members/MemberList'; import { MemberList } from './members/MemberList';
import { MessagesPanel } from './messages/MessagesPanel'; import { MessagesPanel } from './messages/MessagesPanel';
import { ChangeReviewDialog } from './review/ChangeReviewDialog'; import { ChangeReviewDialog } from './review/ChangeReviewDialog';
import { ScheduleSection } from './schedule/ScheduleSection';
import { ClaudeLogsSection } from './ClaudeLogsSection'; import { ClaudeLogsSection } from './ClaudeLogsSection';
import { CollapsibleTeamSection } from './CollapsibleTeamSection'; import { CollapsibleTeamSection } from './CollapsibleTeamSection';
import { ProcessesSection } from './ProcessesSection'; import { ProcessesSection } from './ProcessesSection';
import { ScheduleSection } from './schedule/ScheduleSection';
import { TeamProvisioningBanner } from './TeamProvisioningBanner'; import { TeamProvisioningBanner } from './TeamProvisioningBanner';
import { TeamSessionsSection } from './TeamSessionsSection'; import { TeamSessionsSection } from './TeamSessionsSection';

View file

@ -99,7 +99,7 @@ export const TeamProvisioningBanner = ({
if (isReady) { if (isReady) {
const readyMessage = allTeammatesOnline const readyMessage = allTeammatesOnline
? `Team launched — all ${teamMembers!.length} teammates online` ? `Team launched — all ${teamMembers.length} teammates online`
: 'Team launched — teammates may still be starting'; : 'Team launched — teammates may still be starting';
return ( return (

View file

@ -5,10 +5,10 @@ import { useTheme } from '@renderer/hooks/useTheme';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { FileText, Search, Terminal } from 'lucide-react'; import { FileText, Search, Terminal } from 'lucide-react';
import type { ToolApprovalRequest } from '@shared/types';
import { ToolApprovalSettingsPanel } from './dialogs/ToolApprovalSettingsPanel'; import { ToolApprovalSettingsPanel } from './dialogs/ToolApprovalSettingsPanel';
import type { ToolApprovalRequest } from '@shared/types';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Tool icon mapping // Tool icon mapping
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -93,7 +93,7 @@ export const ActiveTasksBlock = ({
className="size-5 rounded-full bg-[var(--color-surface-raised)]" className="size-5 rounded-full bg-[var(--color-surface-raised)]"
loading="lazy" loading="lazy"
/> />
<span className="absolute -bottom-0.5 -right-0.5 flex h-2.5 w-2.5"> <span className="absolute -bottom-0.5 -right-0.5 flex size-2.5">
<span <span
className={`absolute inline-flex size-full animate-ping rounded-full ${dotPing} opacity-70`} className={`absolute inline-flex size-full animate-ping rounded-full ${dotPing} opacity-70`}
/> />

View file

@ -99,13 +99,13 @@ export function getCrossTeamSentMemberName(value: string | undefined): string |
return parseQualifiedRecipient(value)?.memberName ?? null; return parseQualifiedRecipient(value)?.memberName ?? null;
} }
function CrossTeamTeamBadge({ const CrossTeamTeamBadge = ({
teamName, teamName,
onClick, onClick,
}: { }: {
teamName: string; teamName: string;
onClick?: (teamName: string) => void; onClick?: (teamName: string) => void;
}): React.JSX.Element { }): React.JSX.Element => {
if (onClick) { if (onClick) {
return ( return (
<button <button
@ -135,7 +135,7 @@ function CrossTeamTeamBadge({
{teamName} {teamName}
</span> </span>
); );
} };
interface ActivityItemProps { interface ActivityItemProps {
message: InboxMessage; message: InboxMessage;

View file

@ -8,9 +8,8 @@ import {
import { toMessageKey } from '@renderer/utils/teamMessageKey'; import { toMessageKey } from '@renderer/utils/teamMessageKey';
import { Layers } from 'lucide-react'; import { Layers } from 'lucide-react';
import { buildMessageContext, resolveMessageRenderProps } from './activityMessageContext';
import { ActivityItem, isNoiseMessage } from './ActivityItem'; import { ActivityItem, isNoiseMessage } from './ActivityItem';
import { buildMessageContext, resolveMessageRenderProps } from './activityMessageContext';
import { AnimatedHeightReveal } from './AnimatedHeightReveal'; import { AnimatedHeightReveal } from './AnimatedHeightReveal';
import { findNewestMessageIndex, resolveTimelineCollapseState } from './collapseState'; import { findNewestMessageIndex, resolveTimelineCollapseState } from './collapseState';
import { import {

View file

@ -17,9 +17,10 @@ import {
areThoughtMessagesEquivalentForRender, areThoughtMessagesEquivalentForRender,
} from '@renderer/utils/messageRenderEquality'; } from '@renderer/utils/messageRenderEquality';
import { toMessageKey } from '@renderer/utils/teamMessageKey'; import { toMessageKey } from '@renderer/utils/teamMessageKey';
import { formatToolSummary, parseToolSummary } from '@shared/utils/toolSummary';
import { extractMarkdownPlainText } from '@shared/utils/markdownTextSearch'; import { extractMarkdownPlainText } from '@shared/utils/markdownTextSearch';
import { formatToolSummary, parseToolSummary } from '@shared/utils/toolSummary';
import { ChevronDown, ChevronRight, ChevronUp, Maximize2 } from 'lucide-react'; import { ChevronDown, ChevronRight, ChevronUp, Maximize2 } from 'lucide-react';
import { import {
AnimatedHeightReveal, AnimatedHeightReveal,
ENTRY_REVEAL_ANIMATION_MS, ENTRY_REVEAL_ANIMATION_MS,

View file

@ -11,12 +11,13 @@ import { CARD_ICON_MUTED } from '@renderer/constants/cssVariables';
import { getTeamColorSet } from '@renderer/constants/teamColors'; import { getTeamColorSet } from '@renderer/constants/teamColors';
import { agentAvatarUrl } from '@renderer/utils/memberHelpers'; import { agentAvatarUrl } from '@renderer/utils/memberHelpers';
import { MemberBadge } from '../MemberBadge';
import { ActivityItem } from './ActivityItem'; import { ActivityItem } from './ActivityItem';
import { buildMessageContext, resolveMessageRenderProps } from './activityMessageContext'; import { buildMessageContext, resolveMessageRenderProps } from './activityMessageContext';
import { MemberBadge } from '../MemberBadge';
import { ThoughtBodyContent } from './ThoughtBodyContent'; import { ThoughtBodyContent } from './ThoughtBodyContent';
import type { TimelineItem, LeadThoughtGroup } from './LeadThoughtsGroup'; import type { LeadThoughtGroup, TimelineItem } from './LeadThoughtsGroup';
import type { InboxMessage, ResolvedTeamMember } from '@shared/types'; import type { InboxMessage, ResolvedTeamMember } from '@shared/types';
function formatTime(timestamp: string): string { function formatTime(timestamp: string): string {

View file

@ -86,7 +86,7 @@ export const PendingRepliesBlock = ({
className="size-5 rounded-full bg-[var(--color-surface-raised)]" className="size-5 rounded-full bg-[var(--color-surface-raised)]"
loading="lazy" loading="lazy"
/> />
<span className="absolute -bottom-0.5 -right-0.5 flex h-2.5 w-2.5"> <span className="absolute -bottom-0.5 -right-0.5 flex size-2.5">
<span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-70" /> <span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-70" />
<span className="relative inline-flex size-full rounded-full bg-emerald-500" /> <span className="relative inline-flex size-full rounded-full bg-emerald-500" />
</span> </span>
@ -151,7 +151,7 @@ export const PendingRepliesBlock = ({
<div className="flex items-center gap-2 px-3 py-2"> <div className="flex items-center gap-2 px-3 py-2">
<span className="relative inline-flex shrink-0 items-center justify-center rounded-full bg-[var(--color-surface-raised)] p-1"> <span className="relative inline-flex shrink-0 items-center justify-center rounded-full bg-[var(--color-surface-raised)] p-1">
<Users size={12} style={{ color: colors.border }} /> <Users size={12} style={{ color: colors.border }} />
<span className="absolute -bottom-0.5 -right-0.5 flex h-2.5 w-2.5"> <span className="absolute -bottom-0.5 -right-0.5 flex size-2.5">
<span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-70" /> <span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-70" />
<span className="relative inline-flex size-full rounded-full bg-emerald-500" /> <span className="relative inline-flex size-full rounded-full bg-emerald-500" />
</span> </span>

View file

@ -4,12 +4,12 @@ import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer
import { CopyButton } from '@renderer/components/common/CopyButton'; import { CopyButton } from '@renderer/components/common/CopyButton';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { CARD_ICON_MUTED, CARD_TEXT_LIGHT } from '@renderer/constants/cssVariables'; import { CARD_ICON_MUTED, CARD_TEXT_LIGHT } from '@renderer/constants/cssVariables';
import { linkifyAllMentionsInMarkdown } from '@renderer/utils/mentionLinkify';
import { import {
areStringArraysEqual, areStringArraysEqual,
areStringMapsEqual, areStringMapsEqual,
areThoughtMessagesEquivalentForRender, areThoughtMessagesEquivalentForRender,
} from '@renderer/utils/messageRenderEquality'; } from '@renderer/utils/messageRenderEquality';
import { linkifyAllMentionsInMarkdown } from '@renderer/utils/mentionLinkify';
import { linkifyTaskIdsInMarkdown, parseTaskLinkHref } from '@renderer/utils/taskReferenceUtils'; import { linkifyTaskIdsInMarkdown, parseTaskLinkHref } from '@renderer/utils/taskReferenceUtils';
import { Reply } from 'lucide-react'; import { Reply } from 'lucide-react';

View file

@ -1,7 +1,7 @@
import 'yet-another-react-lightbox/styles.css'; import 'yet-another-react-lightbox/styles.css';
import 'yet-another-react-lightbox/plugins/counter.css'; import 'yet-another-react-lightbox/plugins/counter.css';
import { createContext, useContext, useEffect, useRef, useMemo } from 'react'; import { createContext, useContext, useEffect, useMemo, useRef } from 'react';
import Lightbox from 'yet-another-react-lightbox'; import Lightbox from 'yet-another-react-lightbox';
import Counter from 'yet-another-react-lightbox/plugins/counter'; import Counter from 'yet-another-react-lightbox/plugins/counter';

View file

@ -1,6 +1,5 @@
import { Info } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { Info } from 'lucide-react';
import { AttachmentDisplay } from './AttachmentDisplay'; import { AttachmentDisplay } from './AttachmentDisplay';
@ -27,9 +26,7 @@ export const SourceMessageAttachments = ({
})); }));
const truncatedText = const truncatedText =
sourceMessage.text.length > 300 sourceMessage.text.length > 300 ? sourceMessage.text.slice(0, 297) + '...' : sourceMessage.text;
? sourceMessage.text.slice(0, 297) + '...'
: sourceMessage.text;
const formattedDate = (() => { const formattedDate = (() => {
try { try {

View file

@ -1,12 +1,12 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { getNextSuggestedMemberName } from '@renderer/components/team/members/memberNameSets';
import { import {
buildMembersFromDrafts, buildMembersFromDrafts,
createMemberDraft, createMemberDraft,
MembersEditorSection, MembersEditorSection,
validateMemberNameInline, validateMemberNameInline,
} from '@renderer/components/team/members/MembersEditorSection'; } from '@renderer/components/team/members/MembersEditorSection';
import { getNextSuggestedMemberName } from '@renderer/components/team/members/memberNameSets';
import { Button } from '@renderer/components/ui/button'; import { Button } from '@renderer/components/ui/button';
import { import {
Dialog, Dialog,

View file

@ -40,8 +40,8 @@ import { LimitContextCheckbox } from './LimitContextCheckbox';
import { OptionalSettingsSection } from './OptionalSettingsSection'; import { OptionalSettingsSection } from './OptionalSettingsSection';
import { ProjectPathSelector } from './ProjectPathSelector'; import { ProjectPathSelector } from './ProjectPathSelector';
import { SkipPermissionsCheckbox } from './SkipPermissionsCheckbox'; import { SkipPermissionsCheckbox } from './SkipPermissionsCheckbox';
import { getNextSuggestedTeamName } from './teamNameSets';
import { computeEffectiveTeamModel, TeamModelSelector } from './TeamModelSelector'; import { computeEffectiveTeamModel, TeamModelSelector } from './TeamModelSelector';
import { getNextSuggestedTeamName } from './teamNameSets';
import type { MemberDraft } from '@renderer/components/team/members/membersEditorTypes'; import type { MemberDraft } from '@renderer/components/team/members/membersEditorTypes';
@ -694,7 +694,7 @@ export const CreateTeamDialog = ({
if (!validation.valid) { if (!validation.valid) {
const errors = validation.errors ?? {}; const errors = validation.errors ?? {};
setFieldErrors(errors); setFieldErrors(errors);
const messages = Object.values(errors).filter(Boolean) as string[]; const messages = Object.values(errors).filter(Boolean);
setLocalError(messages.join(' · ') || 'Check form fields'); setLocalError(messages.join(' · ') || 'Check form fields');
return; return;
} }
@ -745,7 +745,7 @@ export const CreateTeamDialog = ({
if (!prev.teamName) return prev; if (!prev.teamName) return prev;
// eslint-disable-next-line sonarjs/no-unused-vars -- destructured to omit teamName from rest // eslint-disable-next-line sonarjs/no-unused-vars -- destructured to omit teamName from rest
const { teamName: _teamName, ...rest } = prev; const { teamName: _teamName, ...rest } = prev;
const remaining = Object.values(rest).filter(Boolean) as string[]; const remaining = Object.values(rest).filter(Boolean);
if (remaining.length === 0) { if (remaining.length === 0) {
setLocalError(null); setLocalError(null);
} else { } else {

View file

@ -17,12 +17,12 @@ import {
import { Input } from '@renderer/components/ui/input'; import { Input } from '@renderer/components/ui/input';
import { Label } from '@renderer/components/ui/label'; import { Label } from '@renderer/components/ui/label';
import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea'; import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { useChipDraftPersistence } from '@renderer/hooks/useChipDraftPersistence'; import { useChipDraftPersistence } from '@renderer/hooks/useChipDraftPersistence';
import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence'; import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence';
import { useFileListCacheWarmer } from '@renderer/hooks/useFileListCacheWarmer'; import { useFileListCacheWarmer } from '@renderer/hooks/useFileListCacheWarmer';
import { useTheme } from '@renderer/hooks/useTheme'; import { useTheme } from '@renderer/hooks/useTheme';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { formatAgentRole } from '@renderer/utils/formatAgentRole'; import { formatAgentRole } from '@renderer/utils/formatAgentRole';
import { buildMemberColorMap } from '@renderer/utils/memberHelpers'; import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
import { normalizePath } from '@renderer/utils/pathNormalize'; import { normalizePath } from '@renderer/utils/pathNormalize';
@ -38,12 +38,13 @@ import {
X, X,
} from 'lucide-react'; } from 'lucide-react';
import { CronScheduleInput } from '../schedule/CronScheduleInput';
import { AdvancedCliSection } from './AdvancedCliSection'; import { AdvancedCliSection } from './AdvancedCliSection';
import { EffortLevelSelector } from './EffortLevelSelector'; import { EffortLevelSelector } from './EffortLevelSelector';
import { OptionalSettingsSection } from './OptionalSettingsSection'; import { OptionalSettingsSection } from './OptionalSettingsSection';
import { ProjectPathSelector } from './ProjectPathSelector'; import { ProjectPathSelector } from './ProjectPathSelector';
import { computeEffectiveTeamModel, TeamModelSelector } from './TeamModelSelector'; import { computeEffectiveTeamModel, TeamModelSelector } from './TeamModelSelector';
import { CronScheduleInput } from '../schedule/CronScheduleInput';
import type { ActiveTeamRef } from './CreateTeamDialog'; import type { ActiveTeamRef } from './CreateTeamDialog';
import type { MentionSuggestion } from '@renderer/types/mention'; import type { MentionSuggestion } from '@renderer/types/mention';
@ -112,7 +113,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
const { isLight } = useTheme(); const { isLight } = useTheme();
const isLaunch = props.mode === 'launch'; const isLaunch = props.mode === 'launch';
const isSchedule = props.mode === 'schedule'; const isSchedule = props.mode === 'schedule';
const schedule = isSchedule ? ((props as LaunchDialogScheduleMode).schedule ?? null) : null; const schedule = isSchedule ? (props.schedule ?? null) : null;
const isEditing = isSchedule && !!schedule; const isEditing = isSchedule && !!schedule;
// Team name: always present for launch mode, may be absent in schedule mode (standalone page) // Team name: always present for launch mode, may be absent in schedule mode (standalone page)
@ -338,7 +339,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
// Clear stale provisioning error when dialog opens // Clear stale provisioning error when dialog opens
useEffect(() => { useEffect(() => {
if (!open || !isLaunch) return; if (!open || !isLaunch) return;
(props as LaunchDialogLaunchMode).clearProvisioningError?.(effectiveTeamName); props.clearProvisioningError?.(effectiveTeamName);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, isLaunch, effectiveTeamName]); }, [open, isLaunch, effectiveTeamName]);
@ -443,9 +444,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
}, [open, repositoryGroups]); }, [open, repositoryGroups]);
// Pre-select defaultProjectPath (launch mode) or first project // Pre-select defaultProjectPath (launch mode) or first project
const defaultProjectPath = isLaunch const defaultProjectPath = isLaunch ? props.defaultProjectPath : undefined;
? (props as LaunchDialogLaunchMode).defaultProjectPath
: undefined;
useEffect(() => { useEffect(() => {
if (!open || cwdMode !== 'project' || selectedProjectPath || projects.length === 0) return; if (!open || cwdMode !== 'project' || selectedProjectPath || projects.length === 0) return;
@ -466,7 +465,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
// Launch-only: conflict detection // Launch-only: conflict detection
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const activeTeams = isLaunch ? (props as LaunchDialogLaunchMode).activeTeams : undefined; const activeTeams = isLaunch ? props.activeTeams : undefined;
const conflictingTeam = useMemo(() => { const conflictingTeam = useMemo(() => {
if (!isLaunch || !activeTeams?.length || !effectiveCwd) return null; if (!isLaunch || !activeTeams?.length || !effectiveCwd) return null;
@ -487,7 +486,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const storeMembers = useStore((s) => s.selectedTeamData?.members ?? []); const storeMembers = useStore((s) => s.selectedTeamData?.members ?? []);
const members = isLaunch ? (props as LaunchDialogLaunchMode).members : storeMembers; const members = isLaunch ? props.members : storeMembers;
const colorMap = useMemo(() => buildMemberColorMap(members), [members]); const colorMap = useMemo(() => buildMemberColorMap(members), [members]);
const mentionSuggestions = useMemo<MentionSuggestion[]>( const mentionSuggestions = useMemo<MentionSuggestion[]>(
@ -564,7 +563,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
// Error // Error
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const provisioningError = isLaunch ? (props as LaunchDialogLaunchMode).provisioningError : null; const provisioningError = isLaunch ? props.provisioningError : null;
const activeError = localError ?? provisioningError; const activeError = localError ?? provisioningError;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -586,7 +585,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
void (async () => { void (async () => {
try { try {
if (isLaunch) { if (isLaunch) {
await (props as LaunchDialogLaunchMode).onLaunch({ await props.onLaunch({
teamName: effectiveTeamName, teamName: effectiveTeamName,
cwd: effectiveCwd, cwd: effectiveCwd,
prompt: promptDraft.value.trim() || undefined, prompt: promptDraft.value.trim() || undefined,

View file

@ -1,7 +1,5 @@
import React from 'react'; import React from 'react';
import { Info } from 'lucide-react';
import { Checkbox } from '@renderer/components/ui/checkbox'; import { Checkbox } from '@renderer/components/ui/checkbox';
import { Label } from '@renderer/components/ui/label'; import { Label } from '@renderer/components/ui/label';
import { import {
@ -10,6 +8,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from '@renderer/components/ui/tooltip'; } from '@renderer/components/ui/tooltip';
import { Info } from 'lucide-react';
interface LimitContextCheckboxProps { interface LimitContextCheckboxProps {
id: string; id: string;

View file

@ -65,7 +65,7 @@ export const OptionalSettingsSection = ({
> >
<button <button
type="button" type="button"
className="flex w-full items-start justify-between gap-3 px-3 py-3 text-left transition-colors hover:bg-[var(--color-surface-raised)]" className="flex w-full items-start justify-between gap-3 p-3 text-left transition-colors hover:bg-[var(--color-surface-raised)]"
onClick={() => setIsOpen((prev) => !prev)} onClick={() => setIsOpen((prev) => !prev)}
aria-expanded={isOpen} aria-expanded={isOpen}
> >

View file

@ -42,7 +42,7 @@ export const WorkflowTimeline = ({ events, memberColorMap }: WorkflowTimelinePro
{/* Content */} {/* Content */}
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div className="flex w-full items-center gap-2 rounded px-1.5 py-1.5 text-xs text-[var(--color-text-secondary)]"> <div className="flex w-full items-center gap-2 rounded p-1.5 text-xs text-[var(--color-text-secondary)]">
<span className="shrink-0 font-mono text-[10px] text-[var(--color-text-muted)]"> <span className="shrink-0 font-mono text-[10px] text-[var(--color-text-muted)]">
{time} {time}
</span> </span>

View file

@ -1,5 +1,8 @@
import { useCallback, useMemo, useRef, useState } from 'react'; import { useCallback, useMemo, useRef, useState } from 'react';
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
import { ImageLightbox } from '@renderer/components/team/attachments/ImageLightbox';
import { MemberBadge } from '@renderer/components/team/MemberBadge';
import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea'; import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { useChipDraftPersistence } from '@renderer/hooks/useChipDraftPersistence'; import { useChipDraftPersistence } from '@renderer/hooks/useChipDraftPersistence';
@ -7,10 +10,10 @@ import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence';
import { useTaskSuggestions } from '@renderer/hooks/useTaskSuggestions'; import { useTaskSuggestions } from '@renderer/hooks/useTaskSuggestions';
import { useTeamSuggestions } from '@renderer/hooks/useTeamSuggestions'; import { useTeamSuggestions } from '@renderer/hooks/useTeamSuggestions';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { serializeChipsWithText } from '@renderer/types/inlineChip';
import { buildReplyBlock } from '@renderer/utils/agentMessageFormatting'; import { buildReplyBlock } from '@renderer/utils/agentMessageFormatting';
import { formatAgentRole } from '@renderer/utils/formatAgentRole'; import { formatAgentRole } from '@renderer/utils/formatAgentRole';
import { buildMemberColorMap } from '@renderer/utils/memberHelpers'; import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
import { serializeChipsWithText } from '@renderer/types/inlineChip';
import { import {
extractTaskRefsFromText, extractTaskRefsFromText,
stripEncodedTaskReferenceMetadata, stripEncodedTaskReferenceMetadata,
@ -18,10 +21,6 @@ import {
import { MAX_TEXT_LENGTH } from '@shared/constants'; import { MAX_TEXT_LENGTH } from '@shared/constants';
import { ImagePlus, Mic, Send, Trash2, X } from 'lucide-react'; import { ImagePlus, Mic, Send, Trash2, X } from 'lucide-react';
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
import { ImageLightbox } from '@renderer/components/team/attachments/ImageLightbox';
import { MemberBadge } from '@renderer/components/team/MemberBadge';
import type { MentionSuggestion } from '@renderer/types/mention'; import type { MentionSuggestion } from '@renderer/types/mention';
import type { CommentAttachmentPayload, ResolvedTeamMember } from '@shared/types'; import type { CommentAttachmentPayload, ResolvedTeamMember } from '@shared/types';

View file

@ -6,12 +6,6 @@ import {
ImageLightbox, ImageLightbox,
LightboxLockProvider, LightboxLockProvider,
} from '@renderer/components/team/attachments/ImageLightbox'; } from '@renderer/components/team/attachments/ImageLightbox';
import {
getTeamColorSet,
getThemedBadge,
getThemedBorder,
getThemedText,
} from '@renderer/constants/teamColors';
import { CollapsibleTeamSection } from '@renderer/components/team/CollapsibleTeamSection'; import { CollapsibleTeamSection } from '@renderer/components/team/CollapsibleTeamSection';
import { FileIcon } from '@renderer/components/team/editor/FileIcon'; import { FileIcon } from '@renderer/components/team/editor/FileIcon';
import { MemberBadge } from '@renderer/components/team/MemberBadge'; import { MemberBadge } from '@renderer/components/team/MemberBadge';
@ -30,19 +24,25 @@ import { Input } from '@renderer/components/ui/input';
import { MemberSelect } from '@renderer/components/ui/MemberSelect'; import { MemberSelect } from '@renderer/components/ui/MemberSelect';
import { TiptapEditor } from '@renderer/components/ui/tiptap'; import { TiptapEditor } from '@renderer/components/ui/tiptap';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { getLegacyCutoff, getReadCommentIds } from '@renderer/services/commentReadStorage'; import {
import { useStore } from '@renderer/store'; getTeamColorSet,
getThemedBadge,
getThemedBorder,
getThemedText,
} from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme'; import { useTheme } from '@renderer/hooks/useTheme';
import { useViewportCommentRead } from '@renderer/hooks/useViewportCommentRead'; import { useViewportCommentRead } from '@renderer/hooks/useViewportCommentRead';
import { getLegacyCutoff, getReadCommentIds } from '@renderer/services/commentReadStorage';
import { useStore } from '@renderer/store';
import { isImageMimeType } from '@renderer/utils/attachmentUtils'; import { isImageMimeType } from '@renderer/utils/attachmentUtils';
import { import {
agentAvatarUrl,
buildMemberColorMap, buildMemberColorMap,
displayMemberName,
KANBAN_COLUMN_DISPLAY, KANBAN_COLUMN_DISPLAY,
REVIEW_STATE_DISPLAY, REVIEW_STATE_DISPLAY,
TASK_STATUS_LABELS, TASK_STATUS_LABELS,
TASK_STATUS_STYLES, TASK_STATUS_STYLES,
agentAvatarUrl,
displayMemberName,
} from '@renderer/utils/memberHelpers'; } from '@renderer/utils/memberHelpers';
import { buildTaskChangeRequestOptions, deriveTaskSince } from '@renderer/utils/taskChangeRequest'; import { buildTaskChangeRequestOptions, deriveTaskSince } from '@renderer/utils/taskChangeRequest';
import { linkifyTaskIdsInMarkdown, parseTaskLinkHref } from '@renderer/utils/taskReferenceUtils'; import { linkifyTaskIdsInMarkdown, parseTaskLinkHref } from '@renderer/utils/taskReferenceUtils';
@ -78,9 +78,10 @@ import {
X, X,
} from 'lucide-react'; } from 'lucide-react';
import { SourceMessageAttachments } from '../attachments/SourceMessageAttachments';
import { WorkflowTimeline } from './StatusHistoryTimeline'; import { WorkflowTimeline } from './StatusHistoryTimeline';
import { TaskAttachments } from './TaskAttachments'; import { TaskAttachments } from './TaskAttachments';
import { SourceMessageAttachments } from '../attachments/SourceMessageAttachments';
import { TaskCommentAwaitingReply } from './TaskCommentAwaitingReply'; import { TaskCommentAwaitingReply } from './TaskCommentAwaitingReply';
import { TaskCommentInput } from './TaskCommentInput'; import { TaskCommentInput } from './TaskCommentInput';
import { TaskCommentsSection } from './TaskCommentsSection'; import { TaskCommentsSection } from './TaskCommentsSection';

View file

@ -4,9 +4,8 @@ import { Button } from '@renderer/components/ui/button';
import { Checkbox } from '@renderer/components/ui/checkbox'; import { Checkbox } from '@renderer/components/ui/checkbox';
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { Crown, Filter } from 'lucide-react';
import { displayMemberName } from '@renderer/utils/memberHelpers'; import { displayMemberName } from '@renderer/utils/memberHelpers';
import { Crown, Filter } from 'lucide-react';
import type { Session } from '@renderer/types/data'; import type { Session } from '@renderer/types/data';
import type { KanbanColumnId, ResolvedTeamMember } from '@shared/types'; import type { KanbanColumnId, ResolvedTeamMember } from '@shared/types';

View file

@ -39,7 +39,7 @@ export const KanbanSearchInput = ({
// Detect `#` trigger and extract filter text after it // Detect `#` trigger and extract filter text after it
const hashMatch = useMemo(() => { const hashMatch = useMemo(() => {
const match = value.match(/#(\S*)$/); const match = /#(\S*)$/.exec(value);
return match ? match[1] : null; return match ? match[1] : null;
}, [value]); }, [value]);

View file

@ -7,7 +7,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { useUnreadCommentCount } from '@renderer/hooks/useUnreadCommentCount'; import { useUnreadCommentCount } from '@renderer/hooks/useUnreadCommentCount';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { REVIEW_STATE_DISPLAY, buildMemberColorMap } from '@renderer/utils/memberHelpers'; import { buildMemberColorMap, REVIEW_STATE_DISPLAY } from '@renderer/utils/memberHelpers';
import { import {
buildTaskChangePresenceKey, buildTaskChangePresenceKey,
buildTaskChangeRequestOptions, buildTaskChangeRequestOptions,

View file

@ -197,7 +197,7 @@ export const MemberDraftRow = ({
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="h-8 w-8 shrink-0 border-red-500/40 px-0 text-red-300 hover:bg-red-500/10 hover:text-red-200" className="size-8 shrink-0 border-red-500/40 px-0 text-red-300 hover:bg-red-500/10 hover:text-red-200"
aria-label={`Remove ${member.name || `member ${index + 1}`}`} aria-label={`Remove ${member.name || `member ${index + 1}`}`}
title="Remove member" title="Remove member"
onClick={() => onRemove(member.id)} onClick={() => onRemove(member.id)}

View file

@ -7,12 +7,12 @@ import {
SubagentRecentMessagesPreview, SubagentRecentMessagesPreview,
} from '@renderer/components/team/members/SubagentRecentMessagesPreview'; } from '@renderer/components/team/members/SubagentRecentMessagesPreview';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { asEnhancedChunkArray } from '@renderer/types/data'; import { asEnhancedChunkArray } from '@renderer/types/data';
import { enhanceAIGroup } from '@renderer/utils/aiGroupEnhancer'; import { enhanceAIGroup } from '@renderer/utils/aiGroupEnhancer';
import { formatDuration } from '@renderer/utils/formatters'; import { formatDuration } from '@renderer/utils/formatters';
import { transformChunksToConversation } from '@renderer/utils/groupTransformer'; import { transformChunksToConversation } from '@renderer/utils/groupTransformer';
import { getMemberColorByName } from '@shared/constants/memberColors'; import { getMemberColorByName } from '@shared/constants/memberColors';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { import {
AlertCircle, AlertCircle,
ChevronDown, ChevronDown,
@ -271,7 +271,12 @@ export const MemberLogsTab = ({
if (!previewLog) return []; if (!previewLog) return [];
// Use task-scoped recentPreviews when available // Use task-scoped recentPreviews when available
if (previewLog.recentPreviews && previewLog.recentPreviews.length > 0 && taskWorkIntervals && taskWorkIntervals.length > 0) { if (
previewLog.recentPreviews &&
previewLog.recentPreviews.length > 0 &&
taskWorkIntervals &&
taskWorkIntervals.length > 0
) {
const GRACE_BEFORE = 30_000; const GRACE_BEFORE = 30_000;
const GRACE_AFTER = 15_000; const GRACE_AFTER = 15_000;
const now = Date.now(); const now = Date.now();
@ -280,7 +285,10 @@ export const MemberLogsTab = ({
const s = Date.parse(i.startedAt); const s = Date.parse(i.startedAt);
if (!Number.isFinite(s)) return null; if (!Number.isFinite(s)) return null;
const e = typeof i.completedAt === 'string' ? Date.parse(i.completedAt) : null; const e = typeof i.completedAt === 'string' ? Date.parse(i.completedAt) : null;
return { startMs: s - GRACE_BEFORE, endMs: e != null && Number.isFinite(e) ? e + GRACE_AFTER : now + GRACE_AFTER }; return {
startMs: s - GRACE_BEFORE,
endMs: e != null && Number.isFinite(e) ? e + GRACE_AFTER : now + GRACE_AFTER,
};
}) })
.filter((v): v is { startMs: number; endMs: number } => v !== null); .filter((v): v is { startMs: number; endMs: number } => v !== null);
@ -291,9 +299,7 @@ export const MemberLogsTab = ({
return intervals.some((i) => ms >= i.startMs && ms <= i.endMs); return intervals.some((i) => ms >= i.startMs && ms <= i.endMs);
}); });
if (scoped.length > 0) { if (scoped.length > 0) {
return scoped return scoped.reverse().map((p, idx) => ({
.reverse()
.map((p, idx) => ({
id: `${previewLog.sessionId}:recent:${idx}`, id: `${previewLog.sessionId}:recent:${idx}`,
timestamp: new Date(p.timestamp), timestamp: new Date(p.timestamp),
kind: 'output' as const, kind: 'output' as const,

View file

@ -1,6 +1,6 @@
import { CUSTOM_ROLE, NO_ROLE } from '@renderer/constants/teamRoles'; import { CUSTOM_ROLE, NO_ROLE } from '@renderer/constants/teamRoles';
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
import { serializeChipsWithText } from '@renderer/types/inlineChip'; import { serializeChipsWithText } from '@renderer/types/inlineChip';
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
import type { MemberDraft } from './membersEditorTypes'; import type { MemberDraft } from './membersEditorTypes';
import type { MentionSuggestion } from '@renderer/types/mention'; import type { MentionSuggestion } from '@renderer/types/mention';
@ -37,7 +37,7 @@ export function createMemberDraft(initial?: Partial<MemberDraft>): MemberDraft {
} }
export function buildMemberDraftColorMap( export function buildMemberDraftColorMap(
members: ReadonlyArray<Pick<MemberDraft, 'name'>> members: readonly Pick<MemberDraft, 'name'>[]
): Map<string, string> { ): Map<string, string> {
return buildMemberColorMap( return buildMemberColorMap(
members members

View file

@ -1,10 +1,11 @@
import { cn } from '@renderer/lib/utils';
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from '@renderer/components/ui/tooltip'; } from '@renderer/components/ui/tooltip';
import { cn } from '@renderer/lib/utils';
import type { AgentActionMode } from '@shared/types'; import type { AgentActionMode } from '@shared/types';
export type ActionMode = AgentActionMode; export type ActionMode = AgentActionMode;

View file

@ -3,11 +3,12 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { api } from '@renderer/api'; import { api } from '@renderer/api';
import { AttachmentPreviewList } from '@renderer/components/team/attachments/AttachmentPreviewList'; import { AttachmentPreviewList } from '@renderer/components/team/attachments/AttachmentPreviewList';
import { DropZoneOverlay } from '@renderer/components/team/attachments/DropZoneOverlay'; import { DropZoneOverlay } from '@renderer/components/team/attachments/DropZoneOverlay';
import { ActionModeSelector } from '@renderer/components/team/messages/ActionModeSelector';
import { MemberBadge } from '@renderer/components/team/MemberBadge'; import { MemberBadge } from '@renderer/components/team/MemberBadge';
import { ActionModeSelector } from '@renderer/components/team/messages/ActionModeSelector';
import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea'; import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea';
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { useComposerDraft } from '@renderer/hooks/useComposerDraft'; import { useComposerDraft } from '@renderer/hooks/useComposerDraft';
import { useTaskSuggestions } from '@renderer/hooks/useTaskSuggestions'; import { useTaskSuggestions } from '@renderer/hooks/useTaskSuggestions';
import { useTeamSuggestions } from '@renderer/hooks/useTeamSuggestions'; import { useTeamSuggestions } from '@renderer/hooks/useTeamSuggestions';
@ -17,7 +18,6 @@ import { isTeamProvisioningActive } from '@renderer/store/slices/teamSlice';
import { serializeChipsWithText } from '@renderer/types/inlineChip'; import { serializeChipsWithText } from '@renderer/types/inlineChip';
import { formatAgentRole } from '@renderer/utils/formatAgentRole'; import { formatAgentRole } from '@renderer/utils/formatAgentRole';
import { buildMemberColorMap } from '@renderer/utils/memberHelpers'; import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { nameColorSet } from '@renderer/utils/projectColor'; import { nameColorSet } from '@renderer/utils/projectColor';
import { import {
extractTaskRefsFromText, extractTaskRefsFromText,
@ -27,8 +27,8 @@ import { MAX_TEXT_LENGTH } from '@shared/constants';
import { isLeadMember } from '@shared/utils/leadDetection'; import { isLeadMember } from '@shared/utils/leadDetection';
import { AlertCircle, Check, ChevronDown, ImagePlus, Mic, Search, Send } from 'lucide-react'; import { AlertCircle, Check, ChevronDown, ImagePlus, Mic, Search, Send } from 'lucide-react';
import type { MentionSuggestion } from '@renderer/types/mention';
import type { ActionMode } from '@renderer/components/team/messages/ActionModeSelector'; import type { ActionMode } from '@renderer/components/team/messages/ActionModeSelector';
import type { MentionSuggestion } from '@renderer/types/mention';
import type { import type {
AttachmentPayload, AttachmentPayload,
ResolvedTeamMember, ResolvedTeamMember,
@ -84,7 +84,6 @@ export const MessageComposer = ({
(externalTextareaRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = node; (externalTextareaRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;
} }
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [externalTextareaRef]); }, [externalTextareaRef]);
const [recipient, setRecipient] = useState<string>(() => { const [recipient, setRecipient] = useState<string>(() => {
const lead = members.find((m) => isLeadMember(m)); const lead = members.find((m) => isLeadMember(m));

View file

@ -3,9 +3,9 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Badge } from '@renderer/components/ui/badge'; import { Badge } from '@renderer/components/ui/badge';
import { Button } from '@renderer/components/ui/button'; import { Button } from '@renderer/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { useStableTeamMentionMeta } from '@renderer/hooks/useStableTeamMentionMeta';
import { useTeamMessagesExpanded } from '@renderer/hooks/useTeamMessagesExpanded'; import { useTeamMessagesExpanded } from '@renderer/hooks/useTeamMessagesExpanded';
import { useTeamMessagesRead } from '@renderer/hooks/useTeamMessagesRead'; import { useTeamMessagesRead } from '@renderer/hooks/useTeamMessagesRead';
import { useStableTeamMentionMeta } from '@renderer/hooks/useStableTeamMentionMeta';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering'; import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
import { toMessageKey } from '@renderer/utils/teamMessageKey'; import { toMessageKey } from '@renderer/utils/teamMessageKey';
@ -16,8 +16,8 @@ import {
ChevronsDownUp, ChevronsDownUp,
ChevronsUpDown, ChevronsUpDown,
MessageSquare, MessageSquare,
PanelLeftClose,
PanelLeft, PanelLeft,
PanelLeftClose,
Search, Search,
X, X,
} from 'lucide-react'; } from 'lucide-react';
@ -26,13 +26,14 @@ import { ActivityTimeline } from '../activity/ActivityTimeline';
import { getThoughtGroupKey, groupTimelineItems } from '../activity/LeadThoughtsGroup'; import { getThoughtGroupKey, groupTimelineItems } from '../activity/LeadThoughtsGroup';
import { MessageExpandDialog } from '../activity/MessageExpandDialog'; import { MessageExpandDialog } from '../activity/MessageExpandDialog';
import { CollapsibleTeamSection } from '../CollapsibleTeamSection'; import { CollapsibleTeamSection } from '../CollapsibleTeamSection';
import { MessageComposer } from './MessageComposer'; import { MessageComposer } from './MessageComposer';
import { MessagesFilterPopover } from './MessagesFilterPopover'; import { MessagesFilterPopover } from './MessagesFilterPopover';
import { StatusBlock } from './StatusBlock'; import { StatusBlock } from './StatusBlock';
import type { MessagesFilterState } from './MessagesFilterPopover';
import type { ActionMode } from './ActionModeSelector';
import type { TimelineItem } from '../activity/LeadThoughtsGroup'; import type { TimelineItem } from '../activity/LeadThoughtsGroup';
import type { ActionMode } from './ActionModeSelector';
import type { MessagesFilterState } from './MessagesFilterPopover';
import type { InboxMessage, ResolvedTeamMember, TaskRef, TeamTaskWithKanban } from '@shared/types'; import type { InboxMessage, ResolvedTeamMember, TaskRef, TeamTaskWithKanban } from '@shared/types';
interface TimeWindow { interface TimeWindow {
@ -631,7 +632,7 @@ export const MessagesPanel = memo(function MessagesPanel({
</> </>
} }
defaultOpen defaultOpen
action={<div className="flex items-center gap-2 pl-2 pr-2">{searchAndFilterBar}</div>} action={<div className="flex items-center gap-2 px-2">{searchAndFilterBar}</div>}
> >
{messagesContent} {messagesContent}
</CollapsibleTeamSection> </CollapsibleTeamSection>

View file

@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { Check, FileCode, FileDiff, FileText, GitBranch, GitCommit, Search } from 'lucide-react'; import { Check, FileCode, FileDiff, FileText, GitBranch, GitCommit, Search } from 'lucide-react';
/* ── Fake diff lines for the mini-terminal ─────────────────────────── */ /* ── Fake diff lines for the mini-terminal ─────────────────────────── */
@ -156,10 +157,7 @@ export const ChangesLoadingAnimation = (): React.JSX.Element => {
</svg> </svg>
{/* Rotating dashed orbit ring */} {/* Rotating dashed orbit ring */}
<svg <svg className="clda-orbit-ring pointer-events-none absolute size-40" viewBox="0 0 160 160">
className="clda-orbit-ring pointer-events-none absolute h-40 w-40"
viewBox="0 0 160 160"
>
<circle <circle
cx="80" cx="80"
cy="80" cy="80"

View file

@ -32,7 +32,7 @@ export const FullDiffLoadingBanner = ({
return ( return (
<div className="bg-surface/95 border-b border-border px-4 py-3"> <div className="bg-surface/95 border-b border-border px-4 py-3">
<div className="bg-surface-raised/80 rounded-xl border border-border shadow-sm"> <div className="bg-surface-raised/80 rounded-xl border border-border shadow-sm">
<div className="flex items-start gap-3 px-3 py-3"> <div className="flex items-start gap-3 p-3">
<div className="relative mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-xl border border-border bg-surface-sidebar"> <div className="relative mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-xl border border-border bg-surface-sidebar">
<div className="absolute inset-1 rounded-lg bg-emerald-500/10 blur-sm" /> <div className="absolute inset-1 rounded-lg bg-emerald-500/10 blur-sm" />
<LoaderCircle <LoaderCircle

View file

@ -13,6 +13,7 @@ import { useStore } from '@renderer/store';
import { AlertTriangle, Clock, Loader2, Terminal } from 'lucide-react'; import { AlertTriangle, Clock, Loader2, Terminal } from 'lucide-react';
import { CliLogsRichView } from '../CliLogsRichView'; import { CliLogsRichView } from '../CliLogsRichView';
import { RunStatusBadge } from './ScheduleStatusBadge'; import { RunStatusBadge } from './ScheduleStatusBadge';
import type { ScheduleRun } from '@shared/types'; import type { ScheduleRun } from '@shared/types';

View file

@ -18,6 +18,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { LaunchTeamDialog } from '../dialogs/LaunchTeamDialog'; import { LaunchTeamDialog } from '../dialogs/LaunchTeamDialog';
import { ScheduleEmptyState } from './ScheduleEmptyState'; import { ScheduleEmptyState } from './ScheduleEmptyState';
import { ScheduleRunLogDialog } from './ScheduleRunLogDialog'; import { ScheduleRunLogDialog } from './ScheduleRunLogDialog';
import { ScheduleRunRow } from './ScheduleRunRow'; import { ScheduleRunRow } from './ScheduleRunRow';
@ -241,7 +242,6 @@ export const ScheduleSection = ({ teamName }: ScheduleSectionProps): React.JSX.E
try { try {
await deleteSchedule(id); await deleteSchedule(id);
} catch (err) { } catch (err) {
// eslint-disable-next-line no-console
console.error('Failed to delete schedule:', err); console.error('Failed to delete schedule:', err);
} }
}, },
@ -253,7 +253,6 @@ export const ScheduleSection = ({ teamName }: ScheduleSectionProps): React.JSX.E
try { try {
await triggerNow(id); await triggerNow(id);
} catch (err) { } catch (err) {
// eslint-disable-next-line no-console
console.error('Failed to trigger schedule:', err); console.error('Failed to trigger schedule:', err);
} }
}, },

Some files were not shown because too many files have changed in this diff Show more