Compare commits

...

5 Commits

Author SHA1 Message Date
Roberto
5cd895f04e refactor: rename CloudAgentConfig type and agentIds WS field to scout
- shared/api-types.ts: LocalAgentConfig → LocalScoutConfig,
  CloudAgentConfig → CloudScoutConfig (schema + type),
  agentIds → scoutIds in WsDeviceHelloSchema
- backend-client.ts: agentIds local var → scoutIds, wire key
  agent_ids → scout_ids via toSnakeCase
- router/index.ts: import + generic type params updated
- Settings renderer: CloudScoutConfigPanel, ScoutRow, ScoutsSection
  import updated to CloudScoutConfig
- .claude/CLAUDE.md: route path /api/v1/scouts/notes/summarize

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:50:33 +02:00
Roberto
49b1d60fca i18n: rename agents keys to scouts across all 5 languages
- settings.agents* → settings.scouts* (bucket 1)
- top-level agents namespace → scouts, all child keys renamed
  (noAgentsYet→noScoutsYet, createAgent→createScout, etc.) (bucket 2)
- toast.agent → toast.scout, values updated to say scout (bucket 3)
- Per-language translations applied consistently (en/it/es/fr/de)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:30:21 +02:00
Roberto
b258ec3de5 refactor(renderer): rename Agent components and types to Scout
- git mv AgentsSection → ScoutsSection, AgentRow → ScoutRow,
  LocalAgentConfigPanel → LocalScoutConfigPanel,
  CloudAgentConfigPanel → CloudScoutConfigPanel,
  InlineAgentCreationStepper → InlineScoutCreationStepper,
  AgentRunHistorySheet → ScoutRunHistorySheet,
  AgentRunLog → ScoutRunLog
- Update all exported function names, internal vars, toast i18n keys
  (toast.agent.* → toast.scout.*, scouts.* i18n keys)
- Replace all trpc.agent.* calls with trpc.scout.* in renderer
- Rename LocalAgentConfig → LocalScoutConfig in types.ts;
  update SectionId 'agents' → 'scouts' and SECTIONS entry
- Update settings.tsx: import ScoutsSection, render on section 'scouts'
- Update ScoutRunHistorySheet RunSummary type: agentId → scoutId
- Rewrite ScoutRunLog to use new ScoutRunSummary shape (actionCounts
  instead of deprecated itemsProcessed/itemsCreated/errors)
- Fix JourneyDialog and PromptBuilderChat: trpc.agent.journey.* → scout
- Fix import paths for shared/api-types (../../../../ → ../../../)

Typecheck: 96 errors before → 53 after (43 errors resolved, all 43
were broken trpc.agent.* refs from Task 8). Remaining 53 are
pre-existing unrelated issues.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:26:08 +02:00
Roberto
f0a18d7011 refactor(main): rename agent-scheduler/store/router symbols to scout
- Move src/main/agents/agent-scheduler.ts → src/main/scouts/scout-scheduler.ts
- Rename exported functions: startAgentScheduler/stopAgentScheduler/tickAgentScheduler → startScoutScheduler/stopScoutScheduler/tickScoutScheduler
- Update URL: /api/v1/agents/trigger → /api/v1/scouts/trigger; /api/v1/agents/can-create → /api/v1/scouts/can-create; /api/v1/agents/catalog → /api/v1/scouts/catalog
- store.ts: LocalAgentLocalConfig → LocalScoutConfig; getLocalAgents/saveLocalAgent/deleteLocalAgent/getLocalAgent → getLocalScouts/saveLocalScout/deleteLocalScout/getLocalScout; storage key localAgents → localScouts
- router/index.ts: agentRouter → scoutRouter (all sub-vars too); appRouter key agent → scout
- index.ts: update scheduler import path and start/stop call sites
- backend-client.ts: getLocalAgents → getLocalScouts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:13:07 +02:00
Roberto
9b66dc3329 refactor(db): rename agent_runs/agent_run_actions to scout_*
Rename Drizzle table definitions: agentRuns → scoutRuns,
agentRunActions → scoutRunActions. Column agentId → scoutId.
Hand-crafted migration 0007_scouts_rename.sql uses ALTER TABLE RENAME
+ CREATE/INSERT/DROP for column rename (SQLite limitation). Updated
all main-process consumers (backend-client, agent-scheduler, router).
Renderer-side type/component rename deferred to Tasks 8-9.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:06:21 +02:00
28 changed files with 1633 additions and 542 deletions

View File

@@ -110,7 +110,7 @@ All use `temperature: 0.3`, streaming enabled. Provider management in `provider.
### Notes AI Navigation (aiSummary index)
Notes have `aiSummary` (≤250 char, nullable) and `aiSummaryUpdatedAt` columns. Generated by backend `POST /api/v1/agents/notes/summarize` (gpt-4o-mini, Langfuse `note_summary` prompt).
Notes have `aiSummary` (≤250 char, nullable) and `aiSummaryUpdatedAt` columns. Generated by backend `POST /api/v1/scouts/notes/summarize` (gpt-4o-mini, Langfuse `note_summary` prompt).
- `list_notes` tool output includes the summary per note so AI can navigate without reading full content.
- `notes-backfill.ts` generates missing summaries on startup (throttled 1 req/s, skipped when offline).

View File

@@ -20,7 +20,7 @@
import { app } from 'electron';
import WebSocket from 'ws';
import { eq } from 'drizzle-orm';
import { getStore, getDeviceId, getLocalAgents, getFormatPrefs } from '../store';
import { getStore, getDeviceId, getLocalScouts, getFormatPrefs } from '../store';
import { detectFormatPrefs } from '../auth/locale-defaults';
import { getAuthManager } from '../auth/auth-manager';
import { toSnakeCase, toCamelCase } from '../../shared/casing';
@@ -32,7 +32,7 @@ import type {
} from '../../shared/api-types';
import { DrizzleExecutor } from './drizzle-executor';
import { getDb } from '../db';
import { agentRuns, agentRunActions } from '../db/schema';
import { scoutRuns, scoutRunActions } from '../db/schema';
// ---------------------------------------------------------------------------
// Agent run logging helpers
@@ -60,10 +60,10 @@ async function recordRunAction(
entityTitle: string | null,
): Promise<void> {
try {
await getDb().insert(agentRunActions).values({
await getDb().insert(scoutRunActions).values({
id: crypto.randomUUID(),
runId,
agentId,
scoutId: agentId,
verb,
entityType,
entityId: entityId ?? null,
@@ -901,14 +901,14 @@ export class BackendClient {
this.reconnectAttempt = 0;
console.log('[DeviceWS] Connected.');
// Read enabled local agent IDs from local storage
// Read enabled local scout IDs from local storage
const deviceId = getDeviceId();
const agentIds = getLocalAgents()
.filter((a) => a.enabled)
.map((a) => a.id);
const scoutIds = getLocalScouts()
.filter((s) => s.enabled)
.map((s) => s.id);
ws.send(JSON.stringify(toSnakeCase({ type: 'device_hello', deviceId, agentIds })));
console.log(`[DeviceWS] Sent device_hello (deviceId=${deviceId}, agents=${agentIds.length}).`);
ws.send(JSON.stringify(toSnakeCase({ type: 'device_hello', deviceId, scoutIds })));
console.log(`[DeviceWS] Sent device_hello (deviceId=${deviceId}, scouts=${scoutIds.length}).`);
this.startHeartbeat(ws);
});
@@ -989,9 +989,9 @@ export class BackendClient {
void (async () => {
try {
const db = getDb();
await db.update(agentRuns)
await db.update(scoutRuns)
.set({ status: status === 'success' ? 'completed' : status === 'partial' ? 'partial' : 'failed', completedAt: Date.now() })
.where(eq(agentRuns.id, runContext.runId));
.where(eq(scoutRuns.id, runContext.runId));
} catch (err) {
console.warn('[RunLog] Failed to close run:', err);
}

View File

@@ -0,0 +1,44 @@
-- Rename agent_runs → scout_runs and agent_run_actions → scout_run_actions
-- SQLite supports ALTER TABLE RENAME TO; column rename (agent_id → scout_id) requires recreate.
-- Step 1: rename agent_runs table
ALTER TABLE `agent_runs` RENAME TO `scout_runs`;
--> statement-breakpoint
-- Step 2: rename agent_run_actions table
ALTER TABLE `agent_run_actions` RENAME TO `scout_run_actions`;
--> statement-breakpoint
-- Step 3: rename agent_id column in scout_runs (SQLite requires full table recreate for column rename)
CREATE TABLE `__new_scout_runs` (
`id` text PRIMARY KEY NOT NULL,
`scout_id` text NOT NULL,
`status` text DEFAULT 'running' NOT NULL,
`started_at` integer NOT NULL,
`completed_at` integer
);
--> statement-breakpoint
INSERT INTO `__new_scout_runs` SELECT `id`, `agent_id`, `status`, `started_at`, `completed_at` FROM `scout_runs`;
--> statement-breakpoint
DROP TABLE `scout_runs`;
--> statement-breakpoint
ALTER TABLE `__new_scout_runs` RENAME TO `scout_runs`;
--> statement-breakpoint
-- Step 4: rename agent_id column in scout_run_actions
CREATE TABLE `__new_scout_run_actions` (
`id` text PRIMARY KEY NOT NULL,
`run_id` text NOT NULL,
`scout_id` text NOT NULL,
`verb` text NOT NULL,
`entity_type` text NOT NULL,
`entity_id` text,
`entity_title` text,
`created_at` integer NOT NULL
);
--> statement-breakpoint
INSERT INTO `__new_scout_run_actions` SELECT `id`, `run_id`, `agent_id`, `verb`, `entity_type`, `entity_id`, `entity_title`, `created_at` FROM `scout_run_actions`;
--> statement-breakpoint
DROP TABLE `scout_run_actions`;
--> statement-breakpoint
ALTER TABLE `__new_scout_run_actions` RENAME TO `scout_run_actions`;

File diff suppressed because it is too large Load Diff

View File

@@ -50,6 +50,13 @@
"when": 1778777130582,
"tag": "0006_misty_cammi",
"breakpoints": true
},
{
"idx": 7,
"version": "6",
"when": 1747353600000,
"tag": "0007_scouts_rename",
"breakpoints": true
}
]
}

View File

@@ -169,18 +169,18 @@ export const taskBriefChats = sqliteTable('task_brief_chats', {
export type TaskBriefChat = InferSelectModel<typeof taskBriefChats>;
export type NewTaskBriefChat = InferInsertModel<typeof taskBriefChats>;
export const agentRuns = sqliteTable('agent_runs', {
export const scoutRuns = sqliteTable('scout_runs', {
id: text('id').primaryKey(),
agentId: text('agent_id').notNull(),
scoutId: text('scout_id').notNull(),
status: text('status', { enum: ['running', 'completed', 'failed', 'partial'] }).notNull().default('running'),
startedAt: integer('started_at', { mode: 'number' }).notNull(),
completedAt: integer('completed_at', { mode: 'number' }),
});
export const agentRunActions = sqliteTable('agent_run_actions', {
export const scoutRunActions = sqliteTable('scout_run_actions', {
id: text('id').primaryKey(),
runId: text('run_id').notNull(),
agentId: text('agent_id').notNull(),
scoutId: text('scout_id').notNull(),
/** 'created' | 'updated' | 'deleted' | 'commented' */
verb: text('verb').notNull(),
/** 'task' | 'note' | 'project' | 'timeline' | 'comment' */
@@ -190,10 +190,10 @@ export const agentRunActions = sqliteTable('agent_run_actions', {
createdAt: integer('created_at', { mode: 'number' }).notNull(),
});
export type AgentRun = InferSelectModel<typeof agentRuns>;
export type NewAgentRun = InferInsertModel<typeof agentRuns>;
export type AgentRunAction = InferSelectModel<typeof agentRunActions>;
export type NewAgentRunAction = InferInsertModel<typeof agentRunActions>;
export type ScoutRun = InferSelectModel<typeof scoutRuns>;
export type NewScoutRun = InferInsertModel<typeof scoutRuns>;
export type ScoutRunAction = InferSelectModel<typeof scoutRunActions>;
export type NewScoutRunAction = InferInsertModel<typeof scoutRunActions>;
export type NoteEdit = InferSelectModel<typeof noteEdits>;
export type NewNoteEdit = InferInsertModel<typeof noteEdits>;

View File

@@ -8,7 +8,7 @@ import { getAuthManager } from './auth/auth-manager';
import { getBackendClient } from './api/backend-client';
import { getStore } from './store';
import { startBriefScheduler, stopBriefScheduler } from './ai/orchestrator';
import { startAgentScheduler, stopAgentScheduler } from './agents/agent-scheduler';
import { startScoutScheduler, stopScoutScheduler } from './scouts/scout-scheduler';
import { backfillNoteSummaries } from './db/notes-backfill';
import { runDailyRescan } from './files/daily-rescan';
@@ -141,7 +141,7 @@ app.on('ready', () => {
.catch((err) => console.error('[DeviceWS] Startup connect failed:', err));
startBriefScheduler();
startAgentScheduler();
startScoutScheduler();
// Delay so WS connection is likely up before triggering rescans
setTimeout(() => { void runDailyRescan(); }, 10_000);
});
@@ -149,7 +149,7 @@ app.on('ready', () => {
// Clean up the persistent WS and backup timers before the app exits
app.on('will-quit', () => {
stopBriefScheduler();
stopAgentScheduler();
stopScoutScheduler();
getBackendClient().disconnectPersistent();
});

View File

@@ -6,13 +6,13 @@ import { dialog, shell } from 'electron';
import { randomUUID } from 'node:crypto';
import { stat } from 'node:fs/promises';
import { getDb } from '../db';
import { clients, projects, tasks, timelineEvents, timelineEventDependencies, notes, noteEdits, taskComments, taskAttachments, agentRuns, agentRunActions, taskBriefings, taskBriefChats } from '../db/schema';
import { clients, projects, tasks, timelineEvents, timelineEventDependencies, notes, noteEdits, taskComments, taskAttachments, scoutRuns, scoutRunActions, taskBriefings, taskBriefChats } from '../db/schema';
import { copyIntoTask, deleteStored, absolutePath, deleteTaskDir } from '../attachments/storage';
import { createHash } from 'crypto';
import { getStore, getDeviceId, getLocalAgents, getLocalAgent, saveLocalAgent, deleteLocalAgent, getFormatPrefs, setFormatPrefs, getUiLanguage, setUiLanguage, getTimelineZoom, setTimelineZoom } from '../store';
import type { LocalAgentLocalConfig } from '../store';
import { getStore, getDeviceId, getLocalScouts, getLocalScout, saveLocalScout, deleteLocalScout, getFormatPrefs, setFormatPrefs, getUiLanguage, setUiLanguage, getTimelineZoom, setTimelineZoom } from '../store';
import type { LocalScoutConfig } from '../store';
import { getBackendClient } from '../api/backend-client';
import type { AgentCatalogItem, CloudAgentConfig, AgentRunLog } from '../../shared/api-types';
import type { AgentCatalogItem, CloudScoutConfig, AgentRunLog } from '../../shared/api-types';
import { orchestrate, orchestrateContextual, orchestrateTaskBriefResearch, dailyBrief, getCachedBrief, invalidateBriefCache } from '../ai/orchestrator';
import { getAuthManager, AuthError } from '../auth/auth-manager';
import { detectFormatPrefs, detectLanguage } from '../auth/locale-defaults';
@@ -1085,12 +1085,12 @@ const aiRouter = router({
});
// ---------------------------------------------------------------------------
// Agent router — proxy to backend agent management API
// Scout router — proxy to backend scout management API
// ---------------------------------------------------------------------------
const agentLocalRouter = router({
const scoutLocalRouter = router({
list: publicProcedure.query(() => {
return getLocalAgents();
return getLocalScouts();
}),
create: publicProcedure
@@ -1102,7 +1102,7 @@ const agentLocalRouter = router({
scheduleCron: z.string(),
}))
.mutation(({ input }) => {
const agent: LocalAgentLocalConfig = {
const scout: LocalScoutConfig = {
id: crypto.randomUUID(),
name: input.name,
directory: input.directory,
@@ -1112,8 +1112,8 @@ const agentLocalRouter = router({
enabled: true,
lastRunAt: null,
};
saveLocalAgent(agent);
return { data: agent, error: null };
saveLocalScout(scout);
return { data: scout, error: null };
}),
update: publicProcedure
@@ -1127,11 +1127,11 @@ const agentLocalRouter = router({
enabled: z.boolean().optional(),
}))
.mutation(({ input }) => {
const existing = getLocalAgent(input.id);
const existing = getLocalScout(input.id);
if (!existing) {
return { data: null, error: 'Agent not found' };
return { data: null, error: 'Scout not found' };
}
const updated: LocalAgentLocalConfig = {
const updated: LocalScoutConfig = {
...existing,
...(input.name !== undefined && { name: input.name }),
...(input.directory !== undefined && { directory: input.directory }),
@@ -1140,22 +1140,22 @@ const agentLocalRouter = router({
...(input.scheduleCron !== undefined && { scheduleCron: input.scheduleCron }),
...(input.enabled !== undefined && { enabled: input.enabled }),
};
saveLocalAgent(updated);
saveLocalScout(updated);
return { data: updated, error: null };
}),
delete: publicProcedure
.input(z.object({ id: z.string() }))
.mutation(({ input }) => {
deleteLocalAgent(input.id);
deleteLocalScout(input.id);
return { success: true as const, error: null };
}),
});
const agentCloudRouter = router({
const scoutCloudRouter = router({
list: publicProcedure.query(async () => {
try {
return await getBackendClient().proxyGet<CloudAgentConfig[]>('/api/v1/agents/cloud');
return await getBackendClient().proxyGet<CloudScoutConfig[]>('/api/v1/agents/cloud');
} catch (err) {
const msg = err instanceof Error ? err.message : 'Failed to list cloud agents';
console.error('[Agent] cloud.list error:', msg);
@@ -1174,7 +1174,7 @@ const agentCloudRouter = router({
}))
.mutation(async ({ input }) => {
try {
const result = await getBackendClient().proxyPost<CloudAgentConfig>(
const result = await getBackendClient().proxyPost<CloudScoutConfig>(
'/api/v1/agents/cloud',
input as Record<string, unknown>,
);
@@ -1198,7 +1198,7 @@ const agentCloudRouter = router({
.mutation(async ({ input }) => {
const { id, ...updates } = input;
try {
const result = await getBackendClient().proxyPut<CloudAgentConfig>(
const result = await getBackendClient().proxyPut<CloudScoutConfig>(
`/api/v1/agents/cloud/${id}`,
updates as Record<string, unknown>,
);
@@ -1222,7 +1222,7 @@ const agentCloudRouter = router({
}),
});
const agentJourneyRouter = router({
const scoutJourneyRouter = router({
start: publicProcedure
.input(z.object({
agentType: z.enum(['local_directory', 'gmail', 'teams', 'outlook']),
@@ -1260,20 +1260,20 @@ const agentJourneyRouter = router({
}),
});
const agentRouter = router({
/** Agent catalog — available agent types from the backend. */
const scoutRouter = router({
/** Scout catalog — available scout types from the backend. */
catalog: publicProcedure.query(async () => {
try {
return await getBackendClient().proxyGet<AgentCatalogItem[]>('/api/v1/agents/catalog');
return await getBackendClient().proxyGet<AgentCatalogItem[]>('/api/v1/scouts/catalog');
} catch (err) {
const msg = err instanceof Error ? err.message : 'Failed to load catalog';
console.error('[Agent] catalog error:', msg);
console.error('[Scout] catalog error:', msg);
return [];
}
}),
local: agentLocalRouter,
cloud: agentCloudRouter,
local: scoutLocalRouter,
cloud: scoutCloudRouter,
/** Run history — queries local SQLite (data written by backend-client on tool_call/run_complete). */
runs: publicProcedure
@@ -1289,18 +1289,18 @@ const agentRouter = router({
const offset = input.offset ?? 0;
const rows = await db
.select()
.from(agentRuns)
.where(eq(agentRuns.agentId, input.agentId))
.orderBy(desc(agentRuns.startedAt))
.from(scoutRuns)
.where(eq(scoutRuns.scoutId, input.agentId))
.orderBy(desc(scoutRuns.startedAt))
.limit(limit)
.offset(offset);
// Compute per-run action counts in one query
const runIds = rows.map(r => r.id);
const actionRows = runIds.length > 0
? await db.select({ runId: agentRunActions.runId, verb: agentRunActions.verb, entityType: agentRunActions.entityType })
.from(agentRunActions)
.where(inArray(agentRunActions.runId, runIds))
? await db.select({ runId: scoutRunActions.runId, verb: scoutRunActions.verb, entityType: scoutRunActions.entityType })
.from(scoutRunActions)
.where(inArray(scoutRunActions.runId, runIds))
: [];
type ActionCounts = { created: number; updated: number; deleted: number };
@@ -1319,7 +1319,7 @@ const agentRouter = router({
}));
} catch (err) {
const msg = err instanceof Error ? err.message : 'Failed to load run history';
console.error('[Agent] runs error:', msg);
console.error('[Scout] runs error:', msg);
return [];
}
}),
@@ -1332,60 +1332,60 @@ const agentRouter = router({
const db = getDb();
return await db
.select()
.from(agentRunActions)
.where(eq(agentRunActions.runId, input.runId))
.orderBy(asc(agentRunActions.createdAt));
.from(scoutRunActions)
.where(eq(scoutRunActions.runId, input.runId))
.orderBy(asc(scoutRunActions.createdAt));
} catch (err) {
console.error('[Agent] runActions error:', err);
console.error('[Scout] runActions error:', err);
return [];
}
}),
/** Check whether the user's plan allows creating a new agent. */
/** Check whether the user's plan allows creating a new scout. */
canCreate: publicProcedure.mutation(async () => {
try {
const activeAgents = getLocalAgents().length;
const activeScouts = getLocalScouts().length;
const result = await getBackendClient().proxyPost<{ allowed: boolean; tier: string; activeAgents: number; limit: number }>(
'/api/v1/agents/can-create',
{ activeAgents },
'/api/v1/scouts/can-create',
{ activeAgents: activeScouts },
);
return { data: result, error: null };
} catch (err) {
const msg = err instanceof Error ? err.message : 'Failed to check agent quota';
const msg = err instanceof Error ? err.message : 'Failed to check scout quota';
return { data: null, error: msg };
}
}),
/** Manually trigger a local agent run via the BE two-phase runner. */
/** Manually trigger a local scout run via the BE two-phase runner. */
runNow: publicProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input }) => {
try {
const agent = getLocalAgent(input.id);
if (!agent) return { data: null, error: 'Agent not found' };
const activeAgents = getLocalAgents().length;
const scout = getLocalScout(input.id);
if (!scout) return { data: null, error: 'Scout not found' };
const activeScouts = getLocalScouts().length;
console.log(
`[agents.runNow] Triggering agent "${agent.name}" (id=${agent.id}) with lastRunAt=${agent.lastRunAt} (${agent.lastRunAt ? new Date(agent.lastRunAt).toISOString() : 'null'})`,
`[scout.runNow] Triggering scout "${scout.name}" (id=${scout.id}) with lastRunAt=${scout.lastRunAt} (${scout.lastRunAt ? new Date(scout.lastRunAt).toISOString() : 'null'})`,
);
const result = await getBackendClient().proxyPost<{ id: string }>(
'/api/v1/agents/trigger',
'/api/v1/scouts/trigger',
{
directory: agent.directory,
directory: scout.directory,
deviceId: getDeviceId(),
agentId: agent.id,
whatToExtract: agent.dataTypes,
batchInterval: agent.scheduleCron,
agentConfig: agent.agentConfig ?? undefined,
activeAgents,
lastRunAt: agent.lastRunAt ?? undefined,
agentId: scout.id,
whatToExtract: scout.dataTypes,
batchInterval: scout.scheduleCron,
agentConfig: scout.agentConfig ?? undefined,
activeAgents: activeScouts,
lastRunAt: scout.lastRunAt ?? undefined,
},
);
// Create the run row so it appears in history even with zero mutations
if (result?.id) {
try {
await getDb().insert(agentRuns).values({
await getDb().insert(scoutRuns).values({
id: result.id,
agentId: agent.id,
scoutId: scout.id,
status: 'running',
startedAt: Date.now(),
}).onConflictDoNothing();
@@ -1393,12 +1393,12 @@ const agentRouter = router({
}
return { data: result, error: null };
} catch (err) {
const msg = err instanceof Error ? err.message : 'Failed to trigger agent run';
const msg = err instanceof Error ? err.message : 'Failed to trigger scout run';
return { data: null, error: msg };
}
}),
journey: agentJourneyRouter,
journey: scoutJourneyRouter,
});
// ---------------------------------------------------------------------------
@@ -1849,7 +1849,7 @@ export const appRouter = router({
taskAttachments: taskAttachmentsRouter,
ai: aiRouter,
auth: authRouter,
agent: agentRouter,
scout: scoutRouter,
memory: memoryRouter,
projectFolders: projectFoldersRouter,
aiChat: aiChatRouter,

View File

@@ -1,21 +1,21 @@
/**
* Agent scheduler checks locally-stored agent configs on a periodic
* Scout scheduler checks locally-stored scout configs on a periodic
* interval and triggers BE-orchestrated runs when they are due.
*
* Follows the same pattern as the daily brief scheduler in orchestrator.ts:
* a single `setInterval` tick that checks all enabled agents.
* a single `setInterval` tick that checks all enabled scouts.
*/
import { getLocalAgents, saveLocalAgent, getDeviceId } from '../store';
import { getLocalScouts, saveLocalScout, getDeviceId } from '../store';
import { getBackendClient } from '../api/backend-client';
import { getDb } from '../db';
import { agentRuns } from '../db/schema';
import { scoutRuns } from '../db/schema';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/** How often the scheduler checks for due agents (ms). */
/** How often the scheduler checks for due scouts (ms). */
const TICK_INTERVAL_MS = 60_000; // 60 seconds
/**
@@ -40,18 +40,18 @@ let schedulerTimer: ReturnType<typeof setInterval> | null = null;
// Public API
// ---------------------------------------------------------------------------
export function startAgentScheduler(): void {
export function startScoutScheduler(): void {
if (schedulerTimer) return;
schedulerTimer = setInterval(() => {
void tickAgentScheduler();
void tickScoutScheduler();
}, TICK_INTERVAL_MS);
// Run once immediately on start
void tickAgentScheduler();
void tickScoutScheduler();
}
export function stopAgentScheduler(): void {
export function stopScoutScheduler(): void {
if (schedulerTimer) {
clearInterval(schedulerTimer);
schedulerTimer = null;
@@ -62,46 +62,46 @@ export function stopAgentScheduler(): void {
// Tick
// ---------------------------------------------------------------------------
async function tickAgentScheduler(): Promise<void> {
const agents = getLocalAgents();
async function tickScoutScheduler(): Promise<void> {
const scouts = getLocalScouts();
const now = Date.now();
for (const agent of agents) {
if (!agent.enabled) continue;
for (const scout of scouts) {
if (!scout.enabled) continue;
// Manual-only agents don't auto-trigger
const intervalMs = CRON_INTERVAL_MS[agent.scheduleCron];
// Manual-only scouts don't auto-trigger
const intervalMs = CRON_INTERVAL_MS[scout.scheduleCron];
if (!intervalMs) continue;
// Check if enough time has passed since lastRunAt
if (agent.lastRunAt && now - agent.lastRunAt < intervalMs) continue;
if (scout.lastRunAt && now - scout.lastRunAt < intervalMs) continue;
try {
const activeAgents = agents.length;
const activeScouts = scouts.length;
console.log(
`[AgentScheduler] Triggering agent "${agent.name}" (id=${agent.id}) with lastRunAt=${agent.lastRunAt} (${agent.lastRunAt ? new Date(agent.lastRunAt).toISOString() : 'null'})`,
`[ScoutScheduler] Triggering scout "${scout.name}" (id=${scout.id}) with lastRunAt=${scout.lastRunAt} (${scout.lastRunAt ? new Date(scout.lastRunAt).toISOString() : 'null'})`,
);
const response = await getBackendClient().proxyPost<{ id: string }>(
'/api/v1/agents/trigger',
'/api/v1/scouts/trigger',
{
directory: agent.directory,
directory: scout.directory,
deviceId: getDeviceId(),
agentId: agent.id,
whatToExtract: agent.dataTypes,
batchInterval: agent.scheduleCron,
agentConfig: agent.agentConfig ?? undefined,
activeAgents,
lastRunAt: agent.lastRunAt ?? undefined,
agentId: scout.id,
whatToExtract: scout.dataTypes,
batchInterval: scout.scheduleCron,
agentConfig: scout.agentConfig ?? undefined,
activeAgents: activeScouts,
lastRunAt: scout.lastRunAt ?? undefined,
},
);
// Create the run row immediately so it appears in history even if
// the agent finds nothing to create/update.
// the scout finds nothing to create/update.
if (response?.id) {
try {
await getDb().insert(agentRuns).values({
await getDb().insert(scoutRuns).values({
id: response.id,
agentId: agent.id,
scoutId: scout.id,
status: 'running',
startedAt: now,
}).onConflictDoNothing();
@@ -109,11 +109,11 @@ async function tickAgentScheduler(): Promise<void> {
}
// Mark the run time so we don't re-trigger until the next interval
saveLocalAgent({ ...agent, lastRunAt: now });
console.log(`[AgentScheduler] Triggered agent "${agent.name}" (id=${agent.id}).`);
saveLocalScout({ ...scout, lastRunAt: now });
console.log(`[ScoutScheduler] Triggered scout "${scout.name}" (id=${scout.id}).`);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.warn(`[AgentScheduler] Failed to trigger agent "${agent.name}": ${msg}`);
console.warn(`[ScoutScheduler] Failed to trigger scout "${scout.name}": ${msg}`);
}
}
}

View File

@@ -1,10 +1,10 @@
import Store from 'electron-store';
// ---------------------------------------------------------------------------
// Local agent config — stored entirely on the FE, never on the backend.
// Local scout config — stored entirely on the FE, never on the backend.
// ---------------------------------------------------------------------------
export interface LocalAgentLocalConfig {
export interface LocalScoutConfig {
id: string;
name: string;
directory: string;
@@ -43,8 +43,8 @@ interface AppSettings {
deviceId: string;
/** Cached daily brief — regenerated once per day or when relevant data changes. */
dailyBriefCache: { content: string; date: string } | null;
/** Locally-managed agent configurations. */
localAgents: LocalAgentLocalConfig[];
/** Locally-managed scout configurations. */
localScouts: LocalScoutConfig[];
/** OS-detected display format preferences. */
formatPrefs: FormatPrefs | null;
/** UI language code (e.g. 'en', 'it', 'es', 'fr', 'de'). */
@@ -66,7 +66,7 @@ export function getStore(): Store<AppSettings> {
backendUrl: 'http://localhost:8000',
deviceId: '',
dailyBriefCache: null,
localAgents: [],
localScouts: [],
formatPrefs: null,
uiLanguage: 'en',
timelineZoom: 'day',
@@ -91,31 +91,31 @@ export function getDeviceId(): string {
}
// ---------------------------------------------------------------------------
// Local agent helpers
// Local scout helpers
// ---------------------------------------------------------------------------
export function getLocalAgents(): LocalAgentLocalConfig[] {
return getStore().get('localAgents');
export function getLocalScouts(): LocalScoutConfig[] {
return getStore().get('localScouts');
}
export function getLocalAgent(id: string): LocalAgentLocalConfig | undefined {
return getLocalAgents().find((a) => a.id === id);
export function getLocalScout(id: string): LocalScoutConfig | undefined {
return getLocalScouts().find((s) => s.id === id);
}
export function saveLocalAgent(agent: LocalAgentLocalConfig): void {
const agents = getLocalAgents();
const idx = agents.findIndex((a) => a.id === agent.id);
export function saveLocalScout(scout: LocalScoutConfig): void {
const scouts = getLocalScouts();
const idx = scouts.findIndex((s) => s.id === scout.id);
if (idx >= 0) {
agents[idx] = agent;
scouts[idx] = scout;
} else {
agents.push(agent);
scouts.push(scout);
}
getStore().set('localAgents', agents);
getStore().set('localScouts', scouts);
}
export function deleteLocalAgent(id: string): void {
const agents = getLocalAgents().filter((a) => a.id !== id);
getStore().set('localAgents', agents);
export function deleteLocalScout(id: string): void {
const scouts = getLocalScouts().filter((s) => s.id !== id);
getStore().set('localScouts', scouts);
}
// ---------------------------------------------------------------------------

View File

@@ -1,20 +1,27 @@
import { useState } from 'react';
import {
CheckCircle2,
XCircle,
AlertCircle,
Loader2,
ChevronDown,
ChevronRight,
Clock,
FileCheck,
FilePlus,
} from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import { trpc } from '@/lib/trpc';
import { useFormatPrefs, formatTs, formatDuration } from '@/lib/date';
import type { AgentRunLog } from '../../../shared/api-types';
// ---------------------------------------------------------------------------
// Types inferred from router return
// ---------------------------------------------------------------------------
type ScoutRunSummary = {
id: string;
scoutId: string;
status: 'running' | 'completed' | 'failed' | 'partial';
startedAt: number;
completedAt: number | null | undefined;
actionCounts: { created: number; updated: number; deleted: number };
};
// ---------------------------------------------------------------------------
// Helpers
@@ -22,16 +29,16 @@ import type { AgentRunLog } from '../../../shared/api-types';
function statusBadge(status: string) {
switch (status) {
case 'success':
case 'completed':
return (
<Badge variant="secondary" className="gap-1 text-emerald-600 dark:text-emerald-400 shrink-0">
<CheckCircle2 className="size-3" /> Success
<CheckCircle2 className="size-3" /> Done
</Badge>
);
case 'error':
case 'failed':
return (
<Badge variant="destructive" className="gap-1 shrink-0">
<XCircle className="size-3" /> Error
<XCircle className="size-3" /> Failed
</Badge>
);
case 'running':
@@ -55,11 +62,10 @@ function statusBadge(status: string) {
// Per-run row
// ---------------------------------------------------------------------------
function RunRow({ run }: { run: AgentRunLog }) {
function RunRow({ run }: { run: ScoutRunSummary }) {
const prefs = useFormatPrefs();
const [errorsOpen, setErrorsOpen] = useState(false);
const hasErrors = (run.errors ?? []).length > 0;
const duration = formatDuration(run.startedAt, run.completedAt);
const totalActions = run.actionCounts.created + run.actionCounts.updated + run.actionCounts.deleted;
return (
<div className="rounded-lg border bg-muted/20 overflow-hidden">
@@ -75,44 +81,20 @@ function RunRow({ run }: { run: AgentRunLog }) {
</span>
)}
<span className="flex items-center gap-1 text-muted-foreground shrink-0">
<FileCheck className="size-3" />
{run.itemsProcessed} processed
<span className="text-muted-foreground shrink-0">
{totalActions} action{totalActions !== 1 ? 's' : ''}
</span>
<span className="flex items-center gap-1 text-muted-foreground shrink-0">
<FilePlus className="size-3" />
{run.itemsCreated} created
</span>
{hasErrors && (
<button
onClick={() => setErrorsOpen(v => !v)}
className="ml-auto flex items-center gap-1 text-destructive hover:text-destructive/80 transition-colors"
>
{errorsOpen ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
{run.errors.length} {run.errors.length === 1 ? 'error' : 'errors'}
</button>
)}
</div>
{hasErrors && errorsOpen && (
<div className="border-t px-3 py-2 flex flex-col gap-1">
{run.errors.map((err, i) => (
<p key={i} className="text-xs text-destructive font-mono break-all">{err}</p>
))}
</div>
)}
</div>
);
}
// ---------------------------------------------------------------------------
// AgentRunLog
// ScoutRunLog
// ---------------------------------------------------------------------------
export function AgentRunLog({ agentId, expanded }: { agentId: string; expanded: boolean }) {
const runsQuery = trpc.agent.runs.useQuery(
export function ScoutRunLog({ agentId, expanded }: { agentId: string; expanded: boolean }) {
const runsQuery = trpc.scout.runs.useQuery(
{ agentId, limit: 10 },
{ enabled: expanded },
);
@@ -139,7 +121,7 @@ export function AgentRunLog({ agentId, expanded }: { agentId: string; expanded:
{!runsQuery.isPending && (runsQuery.data ?? []).length > 0 && (
<div className="flex flex-col gap-2">
{(runsQuery.data as AgentRunLog[]).map(run => (
{(runsQuery.data as ScoutRunSummary[]).map(run => (
<RunRow key={run.id} run={run} />
))}
</div>

View File

@@ -1,165 +0,0 @@
import { useState } from 'react';
import { Bot, Plus } from 'lucide-react';
import { trpc } from '@/lib/trpc';
import { useNotify } from '@/hooks/useNotify';
import { Button } from '@/components/ui/button';
import type { CloudAgentConfig } from '../../../../shared/api-types';
import type { LocalAgentConfig } from './types';
import { AgentRow } from './AgentRow';
import { InlineAgentCreationStepper } from './InlineAgentCreationStepper';
import { JourneyDialog } from './JourneyDialog';
import { useTranslation } from 'react-i18next';
export function AgentsSection() {
const { t } = useTranslation();
const utils = trpc.useUtils();
const localAgentsQuery = trpc.agent.local.list.useQuery();
const cloudAgentsQuery = trpc.agent.cloud.list.useQuery();
const deleteLocalMutation = trpc.agent.local.delete.useMutation();
const deleteCloudMutation = trpc.agent.cloud.delete.useMutation();
const updateLocalMutation = trpc.agent.local.update.useMutation();
const updateCloudMutation = trpc.agent.cloud.update.useMutation();
const runNowMutation = trpc.agent.runNow.useMutation();
const { notify, notifyError, notifyPromise } = useNotify();
const [expandedAgent, setExpandedAgent] = useState<string | null>(null);
const [showTemplatePicker, setShowTemplatePicker] = useState(false);
const [journeyAgent, setJourneyAgent] = useState<{ id: string; type: 'local' | 'cloud'; name: string; currentConfig: Record<string, unknown> | null; dataTypes: string[]; directory?: string } | null>(null);
const catalogQuery = trpc.agent.catalog.useQuery(undefined, {
enabled: showTemplatePicker,
});
const localAgents: LocalAgentConfig[] = localAgentsQuery.data ?? [];
const cloudAgents: CloudAgentConfig[] = cloudAgentsQuery.data ?? [];
const allAgents = [
...localAgents.map(a => ({ ...a, agentType: 'local' as const })),
...cloudAgents.map(a => ({ ...a, agentType: 'cloud' as const })),
];
const hasAgents = allAgents.length > 0;
function handleDelete(id: string, type: 'local' | 'cloud') {
const mutation = type === 'local' ? deleteLocalMutation : deleteCloudMutation;
mutation.mutate({ id }, {
onSuccess: () => {
notify('warning', 'toast.agent.deleted');
void utils.agent.local.list.invalidate();
void utils.agent.cloud.list.invalidate();
},
onError: (err) => notifyError('toast.agent.deleteError', err),
});
}
function handleToggleEnabled(id: string, type: 'local' | 'cloud', enabled: boolean) {
if (type === 'local') {
updateLocalMutation.mutate({ id, enabled }, {
onSuccess: () => void utils.agent.local.list.invalidate(),
onError: (err) => notifyError('toast.agent.updateError', err),
});
} else {
updateCloudMutation.mutate({ id, enabled }, {
onSuccess: () => void utils.agent.cloud.list.invalidate(),
onError: (err) => notifyError('toast.agent.updateError', err),
});
}
}
function handleRunNow(id: string) {
const promise = runNowMutation.mutateAsync({ id });
notifyPromise(promise, { loading: 'toast.agent.runStarted', success: 'toast.agent.runStarted', error: 'toast.agent.runError' });
}
return (
<div className="flex flex-col gap-8">
{/* Empty first-run state */}
{!hasAgents && !showTemplatePicker && (
<div className="py-4 text-center">
<div className="size-11 rounded-2xl bg-primary/10 flex items-center justify-center mx-auto mb-4">
<Bot className="size-5 text-primary" />
</div>
<h2 className="text-base font-semibold">{t('agents.noAgentsYet')}</h2>
<p className="text-sm text-muted-foreground max-w-md mx-auto mt-1.5">
{t('agents.noAgentsDescription')}
</p>
<Button size="sm" className="mt-5" onClick={() => setShowTemplatePicker(true)}>
<Plus className="size-3.5 mr-1.5" />
{t('agents.createFirstAgent')}
</Button>
</div>
)}
{/* Existing configured agents */}
{hasAgents && !showTemplatePicker && (
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between gap-3">
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">{t('agents.yourAgents')}</h2>
<Button size="sm" variant="outline" onClick={() => setShowTemplatePicker(prev => !prev)}>
<Plus className="size-3.5 mr-1.5" />
{t('agents.createAgent')}
</Button>
</div>
<div className="grid gap-4 md:grid-cols-2">
{allAgents.map((agent) => (
<AgentRow
key={agent.id}
agent={agent}
expanded={expandedAgent === agent.id}
onToggleExpand={() => setExpandedAgent(prev => prev === agent.id ? null : agent.id)}
onToggleEnabled={(enabled) => handleToggleEnabled(agent.id, agent.agentType, enabled)}
onDelete={() => handleDelete(agent.id, agent.agentType)}
onRunNow={() => handleRunNow(agent.id)}
onOpenJourney={() => setJourneyAgent({
id: agent.id,
type: agent.agentType,
name: agent.name,
currentConfig: agent.agentType === 'local' ? (agent as LocalAgentConfig).agentConfig ?? null : null,
dataTypes: agent.dataTypes,
directory: agent.agentType === 'local' ? (agent as LocalAgentConfig).directory : undefined,
})}
/>
))}
</div>
</div>
)}
{/* Backend templates picker */}
{showTemplatePicker && (
<InlineAgentCreationStepper
catalog={catalogQuery.data ?? []}
isLoadingCatalog={catalogQuery.isPending}
onCancel={() => setShowTemplatePicker(false)}
onCreated={() => {
setShowTemplatePicker(false);
void utils.agent.local.list.invalidate();
void utils.agent.cloud.list.invalidate();
}}
/>
)}
{/* Chatbot Journey dialog */}
{journeyAgent && (
<JourneyDialog
agentType={journeyAgent.type}
agentName={journeyAgent.name}
currentConfig={journeyAgent.currentConfig}
dataTypes={journeyAgent.dataTypes}
directory={journeyAgent.directory}
onClose={() => setJourneyAgent(null)}
onSaved={(agentConfig) => {
const local = localAgents.find(a => a.id === journeyAgent.id);
if (local) {
updateLocalMutation.mutate({ id: journeyAgent.id, agentConfig }, {
onSuccess: () => {
void utils.agent.local.list.invalidate();
setJourneyAgent(null);
},
});
} else {
setJourneyAgent(null);
}
}}
/>
)}
</div>
);
}

View File

@@ -12,21 +12,21 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import type { CloudAgentConfig } from '../../../../shared/api-types';
import type { CloudScoutConfig } from '../../../shared/api-types';
import { DATA_TYPES, SCHEDULE_OPTIONS } from './types';
export function CloudAgentConfigPanel({
agent,
export function CloudScoutConfigPanel({
scout,
onOpenJourney,
}: {
agent: CloudAgentConfig & { agentType: 'cloud' };
scout: CloudScoutConfig & { scoutType: 'cloud' };
onOpenJourney: () => void;
}) {
const utils = trpc.useUtils();
const updateMutation = trpc.agent.cloud.update.useMutation();
const updateMutation = trpc.scout.cloud.update.useMutation();
const [dataTypes, setDataTypes] = useState<string[]>(agent.dataTypes ?? []);
const [schedule, setSchedule] = useState(agent.scheduleCron ?? '0 * * * *');
const [dataTypes, setDataTypes] = useState<string[]>(scout.dataTypes ?? []);
const [schedule, setSchedule] = useState(scout.scheduleCron ?? '0 * * * *');
const { notify, notifyError } = useNotify();
function toggleDataType(type: string) {
@@ -37,13 +37,13 @@ export function CloudAgentConfigPanel({
function handleSave() {
updateMutation.mutate(
{ id: agent.id, dataTypes, scheduleCron: schedule },
{ id: scout.id, dataTypes, scheduleCron: schedule },
{
onSuccess: () => {
notify('success', 'toast.agent.updated');
void utils.agent.cloud.list.invalidate();
notify('success', 'toast.scout.updated');
void utils.scout.cloud.list.invalidate();
},
onError: (err) => notifyError('toast.agent.updateError', err),
onError: (err) => notifyError('toast.scout.updateError', err),
},
);
}
@@ -52,7 +52,7 @@ export function CloudAgentConfigPanel({
<div className="flex flex-col gap-4">
{/* Provider info */}
<div className="flex items-center gap-2">
<Badge variant="outline" className="capitalize">{agent.provider}</Badge>
<Badge variant="outline" className="capitalize">{scout.provider}</Badge>
<span className="text-xs text-muted-foreground">Connected service</span>
</div>

View File

@@ -17,12 +17,12 @@ import {
Dialog,
DialogContent,
} from '@/components/ui/dialog';
import type { AgentCatalogItem } from '../../../../shared/api-types';
import type { AgentCatalogItem } from '../../../shared/api-types';
import { DATA_TYPE_CONFIG, SCHEDULE_OPTIONS } from './types';
import { TemplateSelectCard } from './TemplateSelectCard';
import { PromptBuilderChat } from './PromptBuilderChat';
export function InlineAgentCreationStepper({
export function InlineScoutCreationStepper({
catalog,
isLoadingCatalog,
onCancel,
@@ -33,8 +33,8 @@ export function InlineAgentCreationStepper({
onCancel: () => void;
onCreated: () => void;
}) {
const createLocalMutation = trpc.agent.local.create.useMutation();
const createCloudMutation = trpc.agent.cloud.create.useMutation();
const createLocalMutation = trpc.scout.local.create.useMutation();
const createCloudMutation = trpc.scout.cloud.create.useMutation();
const { notify, notifyError } = useNotify();
const [step, setStep] = useState<1 | 2 | 3>(1);
@@ -45,7 +45,7 @@ export function InlineAgentCreationStepper({
const [dataTypes, setDataTypes] = useState<string[]>([]);
const [schedule, setSchedule] = useState('0 * * * *');
const [promptTemplate, setPromptTemplate] = useState('');
const [agentConfig, setAgentConfig] = useState<Record<string, unknown> | null>(null);
const [scoutConfig, setScoutConfig] = useState<Record<string, unknown> | null>(null);
const [error, setError] = useState('');
const isSubmitting = createLocalMutation.isPending || createCloudMutation.isPending;
@@ -57,7 +57,7 @@ export function InlineAgentCreationStepper({
setDataTypes((item.supportedDataTypes ?? []).slice(0, 2));
setSchedule('0 * * * *');
setPromptTemplate('');
setAgentConfig(null);
setScoutConfig(null);
setError('');
setStep(2);
}
@@ -66,7 +66,7 @@ export function InlineAgentCreationStepper({
try {
const result = await window.electronDialog.showOpenDialog({
properties: ['openDirectory'],
title: 'Select directory for agent to watch',
title: 'Select directory for scout to watch',
});
if (!result.canceled && result.filePaths.length > 0) {
setDirectory(result.filePaths[0]!);
@@ -85,7 +85,7 @@ export function InlineAgentCreationStepper({
function nextFromConfig() {
if (!selectedTemplate) return;
if (!name.trim()) {
setError('Agent name is required.');
setError('Scout name is required.');
return;
}
if (selectedTemplate.type === 'local_directory' && !directory) {
@@ -112,15 +112,15 @@ export function InlineAgentCreationStepper({
directory,
dataTypes,
scheduleCron: schedule,
agentConfig: agentConfig ?? null,
agentConfig: scoutConfig ?? null,
},
{
onSuccess: () => {
notify('success', 'toast.agent.created');
notify('success', 'toast.scout.created');
onCreated();
},
onError: (err) => {
notifyError('toast.agent.createError', err);
notifyError('toast.scout.createError', err);
setError(err.message);
},
},
@@ -139,11 +139,11 @@ export function InlineAgentCreationStepper({
},
{
onSuccess: () => {
notify('success', 'toast.agent.created');
notify('success', 'toast.scout.created');
onCreated();
},
onError: (err) => {
notifyError('toast.agent.createError', err);
notifyError('toast.scout.createError', err);
setError(err.message);
},
},
@@ -161,7 +161,7 @@ export function InlineAgentCreationStepper({
Choose your<br />
<span className="text-muted-foreground/50">starting template.</span>
</h2>
<p className="text-sm text-muted-foreground mt-3 leading-relaxed">Pick a starting point you can customize everything before the agent goes live.</p>
<p className="text-sm text-muted-foreground mt-3 leading-relaxed">Pick a starting point you can customize everything before the scout goes live.</p>
</div>
{isLoadingCatalog && (
@@ -200,7 +200,7 @@ export function InlineAgentCreationStepper({
value={name}
onChange={(e) => setName(e.target.value)}
className="text-muted-foreground/50 bg-transparent outline-none border-none w-full placeholder:text-muted-foreground/30 caret-primary"
placeholder="agent name."
placeholder="scout name."
spellCheck={false}
/>
</h2>
@@ -234,7 +234,7 @@ export function InlineAgentCreationStepper({
{/* Cloud: sign-in notice */}
{selectedTemplate.type !== 'local_directory' && (
<div className="rounded-xl border border-dashed px-4 py-3 text-sm text-muted-foreground">
After creating this agent, you'll be asked to sign in to <span className="font-medium text-foreground capitalize">{selectedTemplate.provider}</span> and grant read access.
After creating this scout, you'll be asked to sign in to <span className="font-medium text-foreground capitalize">{selectedTemplate.provider}</span> and grant read access.
</div>
)}
@@ -284,13 +284,13 @@ export function InlineAgentCreationStepper({
return (
<div>
<Button
variant={promptTemplate || agentConfig ? 'outline' : 'default'}
variant={promptTemplate || scoutConfig ? 'outline' : 'default'}
size="sm"
disabled={!unlocked}
onClick={() => setPromptDialogOpen(true)}
>
<Sparkles className="size-3.5 mr-1.5" />
{promptTemplate || agentConfig ? 'Edit extraction prompt' : 'Build extraction prompt'}
{promptTemplate || scoutConfig ? 'Edit extraction prompt' : 'Build extraction prompt'}
</Button>
<Dialog open={promptDialogOpen} onOpenChange={setPromptDialogOpen}>
@@ -302,7 +302,7 @@ export function InlineAgentCreationStepper({
dataTypes={dataTypes}
directory={selectedTemplate.type === 'local_directory' ? directory : undefined}
onPromptUpdate={(p) => setPromptTemplate(p)}
onConfigUpdate={(c) => setAgentConfig(c)}
onConfigUpdate={(c) => setScoutConfig(c)}
/>
</div>
<div className="flex justify-end gap-2 px-5 py-4 border-t shrink-0">
@@ -327,9 +327,9 @@ export function InlineAgentCreationStepper({
<p className="text-xs font-semibold uppercase tracking-widest text-muted-foreground/60 mb-2">Step 3 of 3</p>
<h2 className="text-3xl font-semibold tracking-tight leading-tight">
Review and<br />
<span className="text-muted-foreground/50">create your agent.</span>
<span className="text-muted-foreground/50">create your scout.</span>
</h2>
<p className="text-sm text-muted-foreground mt-3 leading-relaxed">Everything looks good? Hit create and your agent will start running on schedule.</p>
<p className="text-sm text-muted-foreground mt-3 leading-relaxed">Everything looks good? Hit create and your scout will start running on schedule.</p>
</div>
<Card className="rounded-xl gap-0 py-0 shadow-none border-border/70">
<CardContent className="p-5 flex flex-col gap-4">
@@ -345,7 +345,7 @@ export function InlineAgentCreationStepper({
{selectedTemplate.type === 'local_directory' && directory && (
<p><span className="text-muted-foreground">Directory:</span> {directory}</p>
)}
{(selectedTemplate.type === 'local_directory' ? agentConfig : promptTemplate) && (
{(selectedTemplate.type === 'local_directory' ? scoutConfig : promptTemplate) && (
<p><span className="text-muted-foreground">Extraction config:</span> Added</p>
)}
</div>
@@ -380,7 +380,7 @@ export function InlineAgentCreationStepper({
{step === 3 && (
<Button size="sm" onClick={handleCreate} disabled={isSubmitting}>
Create agent now
Create scout now
</Button>
)}
</div>

View File

@@ -70,8 +70,8 @@ export function JourneyDialog({
onClose: () => void;
onSaved: (agentConfig: Record<string, unknown>) => void;
}) {
const startMutation = trpc.agent.journey.start.useMutation();
const messageMutation = trpc.agent.journey.message.useMutation();
const startMutation = trpc.scout.journey.start.useMutation();
const messageMutation = trpc.scout.journey.message.useMutation();
const [sessionId, setSessionId] = useState<string | null>(null);
const [messages, setMessages] = useState<JourneyMessage[]>([]);

View File

@@ -11,29 +11,29 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import type { LocalAgentConfig } from './types';
import type { LocalScoutConfig } from './types';
import { DATA_TYPES, SCHEDULE_OPTIONS } from './types';
export function LocalAgentConfigPanel({
agent,
export function LocalScoutConfigPanel({
scout,
onOpenJourney,
}: {
agent: LocalAgentConfig & { agentType: 'local' };
scout: LocalScoutConfig & { scoutType: 'local' };
onOpenJourney: () => void;
}) {
const utils = trpc.useUtils();
const updateMutation = trpc.agent.local.update.useMutation();
const updateMutation = trpc.scout.local.update.useMutation();
const [directory, setDirectory] = useState(agent.directory ?? '');
const [dataTypes, setDataTypes] = useState<string[]>(agent.dataTypes ?? []);
const [schedule, setSchedule] = useState(agent.scheduleCron ?? '0 * * * *');
const [directory, setDirectory] = useState(scout.directory ?? '');
const [dataTypes, setDataTypes] = useState<string[]>(scout.dataTypes ?? []);
const [schedule, setSchedule] = useState(scout.scheduleCron ?? '0 * * * *');
const { notify, notifyError } = useNotify();
async function pickDirectory() {
try {
const result = await window.electronDialog.showOpenDialog({
properties: ['openDirectory'],
title: 'Select directory for agent to watch',
title: 'Select directory for scout to watch',
});
if (!result.canceled && result.filePaths.length > 0) {
setDirectory(result.filePaths[0]!);
@@ -51,13 +51,13 @@ export function LocalAgentConfigPanel({
function handleSave() {
updateMutation.mutate(
{ id: agent.id, directory, dataTypes, scheduleCron: schedule },
{ id: scout.id, directory, dataTypes, scheduleCron: schedule },
{
onSuccess: () => {
notify('success', 'toast.agent.updated');
void utils.agent.local.list.invalidate();
notify('success', 'toast.scout.updated');
void utils.scout.local.list.invalidate();
},
onError: (err) => notifyError('toast.agent.updateError', err),
onError: (err) => notifyError('toast.scout.updateError', err),
},
);
}

View File

@@ -20,8 +20,8 @@ export function PromptBuilderChat({
onPromptUpdate?: (prompt: string) => void;
onConfigUpdate?: (config: Record<string, unknown>) => void;
}) {
const startMutation = trpc.agent.journey.start.useMutation();
const messageMutation = trpc.agent.journey.message.useMutation();
const startMutation = trpc.scout.journey.start.useMutation();
const messageMutation = trpc.scout.journey.message.useMutation();
const [started, setStarted] = useState(false);
const [sessionId, setSessionId] = useState<string | null>(null);

View File

@@ -3,15 +3,15 @@ import { Play, Trash2, ChevronDown, ChevronUp, History } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Switch } from '@/components/ui/switch';
import type { CloudAgentConfig } from '../../../../shared/api-types';
import type { LocalAgentConfig } from './types';
import type { CloudScoutConfig } from '../../../shared/api-types';
import type { LocalScoutConfig } from './types';
import { SCHEDULE_OPTIONS, formatTs } from './types';
import { LocalAgentConfigPanel } from './LocalAgentConfigPanel';
import { CloudAgentConfigPanel } from './CloudAgentConfigPanel';
import { AgentRunHistorySheet } from './AgentRunHistorySheet';
import { LocalScoutConfigPanel } from './LocalScoutConfigPanel';
import { CloudScoutConfigPanel } from './CloudScoutConfigPanel';
import { ScoutRunHistorySheet } from './ScoutRunHistorySheet';
export function AgentRow({
agent,
export function ScoutRow({
scout,
expanded,
onToggleExpand,
onToggleEnabled,
@@ -19,7 +19,7 @@ export function AgentRow({
onRunNow,
onOpenJourney,
}: {
agent: (LocalAgentConfig | CloudAgentConfig) & { agentType: 'local' | 'cloud' };
scout: (LocalScoutConfig | CloudScoutConfig) & { scoutType: 'local' | 'cloud' };
expanded: boolean;
onToggleExpand: () => void;
onToggleEnabled: (enabled: boolean) => void;
@@ -28,9 +28,9 @@ export function AgentRow({
onOpenJourney: () => void;
}) {
const [historyOpen, setHistoryOpen] = useState(false);
const scheduleLabel = SCHEDULE_OPTIONS.find(s => s.value === agent.scheduleCron)?.label ?? agent.scheduleCron;
const lastRunLabel = agent.lastRunAt ? formatTs(agent.lastRunAt) : 'Never';
const kindLabel = agent.agentType === 'local' ? 'Local' : `Cloud · ${(agent as CloudAgentConfig).provider}`;
const scheduleLabel = SCHEDULE_OPTIONS.find(s => s.value === scout.scheduleCron)?.label ?? scout.scheduleCron;
const lastRunLabel = scout.lastRunAt ? formatTs(scout.lastRunAt) : 'Never';
const kindLabel = scout.scoutType === 'local' ? 'Local' : `Cloud · ${(scout as CloudScoutConfig).provider}`;
return (
<Card className="rounded-xl py-0 gap-0 overflow-hidden h-fit border-border/70 shadow-none">
@@ -38,11 +38,11 @@ export function AgentRow({
<div className="px-4 py-4 space-y-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-sm font-semibold truncate">{agent.name}</p>
<p className="text-sm font-semibold truncate">{scout.name}</p>
<p className="text-xs text-muted-foreground mt-1">{kindLabel}</p>
</div>
<Switch
checked={agent.enabled}
checked={scout.enabled}
onCheckedChange={onToggleEnabled}
size="sm"
/>
@@ -54,7 +54,7 @@ export function AgentRow({
<span className="text-muted-foreground">Last run</span>
<span className="text-foreground truncate">{lastRunLabel}</span>
<span className="text-muted-foreground">Status</span>
<span className="text-foreground">{agent.enabled ? 'Enabled' : 'Disabled'}</span>
<span className="text-foreground">{scout.enabled ? 'Enabled' : 'Disabled'}</span>
</div>
<div className="flex items-center justify-between gap-2">
@@ -69,7 +69,7 @@ export function AgentRow({
</Button>
</div>
<div className="flex items-center gap-1">
<Button size="sm" variant="ghost" onClick={onDelete} title="Delete agent" className="h-8 w-8 p-0">
<Button size="sm" variant="ghost" onClick={onDelete} title="Delete scout" className="h-8 w-8 p-0">
<Trash2 className="size-3.5 text-muted-foreground" />
</Button>
<Button size="sm" variant="ghost" onClick={onToggleExpand} title={expanded ? 'Collapse' : 'Configure'} className="h-8 w-8 p-0">
@@ -82,17 +82,17 @@ export function AgentRow({
{/* Expanded config */}
{expanded && (
<div className="border-t px-4 py-4 bg-muted/20">
{agent.agentType === 'local' ? (
<LocalAgentConfigPanel agent={agent as LocalAgentConfig & { agentType: 'local' }} onOpenJourney={onOpenJourney} />
{scout.scoutType === 'local' ? (
<LocalScoutConfigPanel scout={scout as LocalScoutConfig & { scoutType: 'local' }} onOpenJourney={onOpenJourney} />
) : (
<CloudAgentConfigPanel agent={agent as CloudAgentConfig & { agentType: 'cloud' }} onOpenJourney={onOpenJourney} />
<CloudScoutConfigPanel scout={scout as CloudScoutConfig & { scoutType: 'cloud' }} onOpenJourney={onOpenJourney} />
)}
</div>
)}
<AgentRunHistorySheet
agentId={agent.id}
agentName={agent.name}
<ScoutRunHistorySheet
scoutId={scout.id}
scoutName={scout.name}
open={historyOpen}
onOpenChange={setHistoryOpen}
/>

View File

@@ -17,7 +17,7 @@ import { useFormatPrefs, formatTs, formatDuration } from '@/lib/date';
type RunSummary = {
id: string;
agentId: string;
scoutId: string;
status: 'running' | 'completed' | 'failed' | 'partial';
startedAt: number;
completedAt: number | null | undefined;
@@ -88,7 +88,7 @@ const VERB_ICON: Record<string, React.ReactNode> = {
// ---------------------------------------------------------------------------
function RunActionList({ runId }: { runId: string }) {
const query = trpc.agent.runActions.useQuery({ runId });
const query = trpc.scout.runActions.useQuery({ runId });
if (query.isPending) {
return (
@@ -166,19 +166,19 @@ function RunRow({ run }: { run: RunSummary }) {
// Sheet
// ---------------------------------------------------------------------------
export function AgentRunHistorySheet({
agentId,
agentName,
export function ScoutRunHistorySheet({
scoutId,
scoutName,
open,
onOpenChange,
}: {
agentId: string;
agentName: string;
scoutId: string;
scoutName: string;
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
const runsQuery = trpc.agent.runs.useQuery(
{ agentId, limit: 30 },
const runsQuery = trpc.scout.runs.useQuery(
{ agentId: scoutId, limit: 30 },
{ enabled: open },
);
@@ -188,7 +188,7 @@ export function AgentRunHistorySheet({
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="w-full sm:max-w-md flex flex-col gap-0 p-0">
<SheetHeader className="px-5 pt-5 pb-4">
<SheetTitle className="text-base font-semibold">{agentName}</SheetTitle>
<SheetTitle className="text-base font-semibold">{scoutName}</SheetTitle>
<p className="text-xs text-muted-foreground -mt-1">Run history</p>
</SheetHeader>
@@ -208,7 +208,7 @@ export function AgentRunHistorySheet({
</EmptyMedia>
<EmptyTitle className="text-sm">No runs yet</EmptyTitle>
<EmptyDescription className="text-xs">
Runs will appear here after the agent executes.
Runs will appear here after the scout executes.
</EmptyDescription>
</EmptyHeader>
</Empty>

View File

@@ -0,0 +1,165 @@
import { useState } from 'react';
import { Bot, Plus } from 'lucide-react';
import { trpc } from '@/lib/trpc';
import { useNotify } from '@/hooks/useNotify';
import { Button } from '@/components/ui/button';
import type { CloudScoutConfig } from '../../../shared/api-types';
import type { LocalScoutConfig } from './types';
import { ScoutRow } from './ScoutRow';
import { InlineScoutCreationStepper } from './InlineScoutCreationStepper';
import { JourneyDialog } from './JourneyDialog';
import { useTranslation } from 'react-i18next';
export function ScoutsSection() {
const { t } = useTranslation();
const utils = trpc.useUtils();
const localScoutsQuery = trpc.scout.local.list.useQuery();
const cloudScoutsQuery = trpc.scout.cloud.list.useQuery();
const deleteLocalMutation = trpc.scout.local.delete.useMutation();
const deleteCloudMutation = trpc.scout.cloud.delete.useMutation();
const updateLocalMutation = trpc.scout.local.update.useMutation();
const updateCloudMutation = trpc.scout.cloud.update.useMutation();
const runNowMutation = trpc.scout.runNow.useMutation();
const { notify, notifyError, notifyPromise } = useNotify();
const [expandedScout, setExpandedScout] = useState<string | null>(null);
const [showTemplatePicker, setShowTemplatePicker] = useState(false);
const [journeyScout, setJourneyScout] = useState<{ id: string; type: 'local' | 'cloud'; name: string; currentConfig: Record<string, unknown> | null; dataTypes: string[]; directory?: string } | null>(null);
const catalogQuery = trpc.scout.catalog.useQuery(undefined, {
enabled: showTemplatePicker,
});
const localScouts: LocalScoutConfig[] = localScoutsQuery.data ?? [];
const cloudScouts: CloudScoutConfig[] = cloudScoutsQuery.data ?? [];
const allScouts = [
...localScouts.map(a => ({ ...a, scoutType: 'local' as const })),
...cloudScouts.map(a => ({ ...a, scoutType: 'cloud' as const })),
];
const hasScouts = allScouts.length > 0;
function handleDelete(id: string, type: 'local' | 'cloud') {
const mutation = type === 'local' ? deleteLocalMutation : deleteCloudMutation;
mutation.mutate({ id }, {
onSuccess: () => {
notify('warning', 'toast.scout.deleted');
void utils.scout.local.list.invalidate();
void utils.scout.cloud.list.invalidate();
},
onError: (err) => notifyError('toast.scout.deleteError', err),
});
}
function handleToggleEnabled(id: string, type: 'local' | 'cloud', enabled: boolean) {
if (type === 'local') {
updateLocalMutation.mutate({ id, enabled }, {
onSuccess: () => void utils.scout.local.list.invalidate(),
onError: (err) => notifyError('toast.scout.updateError', err),
});
} else {
updateCloudMutation.mutate({ id, enabled }, {
onSuccess: () => void utils.scout.cloud.list.invalidate(),
onError: (err) => notifyError('toast.scout.updateError', err),
});
}
}
function handleRunNow(id: string) {
const promise = runNowMutation.mutateAsync({ id });
notifyPromise(promise, { loading: 'toast.scout.runStarted', success: 'toast.scout.runStarted', error: 'toast.scout.runError' });
}
return (
<div className="flex flex-col gap-8">
{/* Empty first-run state */}
{!hasScouts && !showTemplatePicker && (
<div className="py-4 text-center">
<div className="size-11 rounded-2xl bg-primary/10 flex items-center justify-center mx-auto mb-4">
<Bot className="size-5 text-primary" />
</div>
<h2 className="text-base font-semibold">{t('scouts.noScoutsYet')}</h2>
<p className="text-sm text-muted-foreground max-w-md mx-auto mt-1.5">
{t('scouts.noScoutsDescription')}
</p>
<Button size="sm" className="mt-5" onClick={() => setShowTemplatePicker(true)}>
<Plus className="size-3.5 mr-1.5" />
{t('scouts.createFirstScout')}
</Button>
</div>
)}
{/* Existing configured scouts */}
{hasScouts && !showTemplatePicker && (
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between gap-3">
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">{t('scouts.yourScouts')}</h2>
<Button size="sm" variant="outline" onClick={() => setShowTemplatePicker(prev => !prev)}>
<Plus className="size-3.5 mr-1.5" />
{t('scouts.createScout')}
</Button>
</div>
<div className="grid gap-4 md:grid-cols-2">
{allScouts.map((scout) => (
<ScoutRow
key={scout.id}
scout={scout}
expanded={expandedScout === scout.id}
onToggleExpand={() => setExpandedScout(prev => prev === scout.id ? null : scout.id)}
onToggleEnabled={(enabled) => handleToggleEnabled(scout.id, scout.scoutType, enabled)}
onDelete={() => handleDelete(scout.id, scout.scoutType)}
onRunNow={() => handleRunNow(scout.id)}
onOpenJourney={() => setJourneyScout({
id: scout.id,
type: scout.scoutType,
name: scout.name,
currentConfig: scout.scoutType === 'local' ? (scout as LocalScoutConfig).agentConfig ?? null : null,
dataTypes: scout.dataTypes,
directory: scout.scoutType === 'local' ? (scout as LocalScoutConfig).directory : undefined,
})}
/>
))}
</div>
</div>
)}
{/* Backend templates picker */}
{showTemplatePicker && (
<InlineScoutCreationStepper
catalog={catalogQuery.data ?? []}
isLoadingCatalog={catalogQuery.isPending}
onCancel={() => setShowTemplatePicker(false)}
onCreated={() => {
setShowTemplatePicker(false);
void utils.scout.local.list.invalidate();
void utils.scout.cloud.list.invalidate();
}}
/>
)}
{/* Chatbot Journey dialog */}
{journeyScout && (
<JourneyDialog
agentType={journeyScout.type}
agentName={journeyScout.name}
currentConfig={journeyScout.currentConfig}
dataTypes={journeyScout.dataTypes}
directory={journeyScout.directory}
onClose={() => setJourneyScout(null)}
onSaved={(agentConfig) => {
const local = localScouts.find(a => a.id === journeyScout.id);
if (local) {
updateLocalMutation.mutate({ id: journeyScout.id, agentConfig }, {
onSuccess: () => {
void utils.scout.local.list.invalidate();
setJourneyScout(null);
},
});
} else {
setJourneyScout(null);
}
}}
/>
)}
</div>
);
}

View File

@@ -1,7 +1,7 @@
import { ListTodo, FileText, CalendarDays, Layers, type LucideIcon } from 'lucide-react';
import { User, Brain, Shield, CreditCard, Palette, Bot } from 'lucide-react';
export type SectionId = 'profile' | 'account' | 'billing' | 'appearance' | 'agents' | 'memory';
export type SectionId = 'profile' | 'account' | 'billing' | 'appearance' | 'scouts' | 'memory';
export const SECTIONS: { id: SectionId; labelKey: string; icon: LucideIcon }[] = [
{ id: 'profile', labelKey: 'settings.profile', icon: User },
@@ -9,7 +9,7 @@ export const SECTIONS: { id: SectionId; labelKey: string; icon: LucideIcon }[] =
{ id: 'account', labelKey: 'settings.account', icon: Shield },
{ id: 'billing', labelKey: 'settings.billing', icon: CreditCard },
{ id: 'appearance', labelKey: 'settings.appearance', icon: Palette },
{ id: 'agents', labelKey: 'settings.agents', icon: Bot },
{ id: 'scouts', labelKey: 'settings.scouts', icon: Bot },
];
export const SCHEDULE_OPTIONS = [
@@ -30,7 +30,7 @@ export const DATA_TYPE_CONFIG = [
] as const;
/** Mirrors LocalAgentLocalConfig from electron-store (tRPC infers it). */
export interface LocalAgentConfig {
export interface LocalScoutConfig {
id: string;
name: string;
directory: string;

View File

@@ -99,9 +99,9 @@
"account": "Konto",
"accountSubtitle": "Sicherheit und Zugang.",
"accountDescription": "Verwalten Sie Sicherheit, Verbindungen und Kontoeinstellungen.",
"agents": "Agenten",
"agentsSubtitle": "arbeiten für Sie.",
"agentsDescription": "Unter-Agenten, die für Sie arbeiten — Daten aus lokalen Dateien oder Cloud-Diensten sammeln, wiederkehrende Aktionen auslösen und Ihren Arbeitsbereich synchron halten.",
"scouts": "Scouts",
"scoutsSubtitle": "die für dich arbeiten.",
"scoutsDescription": "Scouts überwachen deine Datenquellen — lokale Dateien, Postfächer, Cloud-Dienste — und heben hervor, was im Task Brief zählt.",
"aiPreferences": "KI-Einstellungen",
"aiPreferencesSubtitle": "auf Sie zugeschnitten.",
"aiPreferencesDescription": "Personalisieren Sie, wie die KI auf Sie reagiert.",
@@ -418,15 +418,15 @@
"webOnlyTooltip": "Ordnerverknüpfung ist in der Desktop-App verfügbar"
}
},
"agents": {
"title": "Agenten",
"subtitle": "arbeiten für Sie.",
"description": "Unteragenten, die in Ihrem Auftrag arbeiten — Daten aus lokalen Dateien oder Cloud-Diensten sammeln, wiederkehrende Aktionen auslösen und Ihren Arbeitsbereich synchron halten.",
"noAgentsYet": "Noch keine Agenten",
"noAgentsDescription": "Erstellen Sie Ihren ersten Agenten aus einer Vorlage. Sie können wählen, welche Daten extrahiert werden, einen Zeitplan festlegen und Anweisungen bearbeiten.",
"createFirstAgent": "Ersten Agenten erstellen",
"yourAgents": "Ihre Agenten",
"createAgent": "Agent erstellen"
"scouts": {
"title": "Scouts",
"subtitle": "die für dich arbeiten.",
"description": "Scouts überwachen deine Datenquellen — lokale Dateien, Postfächer, Cloud-Dienste — und heben hervor, was im Task Brief zählt.",
"noScoutsYet": "Noch keine Scouts",
"noScoutsDescription": "Erstelle deinen ersten Scout aus einer Vorlage. Wähle, welche Daten extrahiert werden, lege einen Zeitplan fest und bearbeite die Anweisungen vor dem Speichern.",
"createFirstScout": "Ersten Scout erstellen",
"yourScouts": "Deine Scouts",
"createScout": "Scout erstellen"
},
"toast": {
"profile": {
@@ -513,15 +513,15 @@
"createError": "Datei konnte nicht angehängt werden.",
"tooLarge": "{{filename}} ist zu groß (Limit 50 MB)."
},
"agent": {
"created": "Agent erstellt",
"createError": "Agent konnte nicht erstellt werden",
"updated": "Agent-Konfiguration gespeichert",
"scout": {
"created": "Scout erstellt",
"createError": "Scout konnte nicht erstellt werden",
"updated": "Scout-Konfiguration gespeichert",
"updateError": "Konfiguration konnte nicht gespeichert werden",
"deleted": "Agent gelöscht",
"deleteError": "Agent konnte nicht gelöscht werden",
"runStarted": "Agent-Ausführung gestartet",
"runError": "Agent konnte nicht gestartet werden"
"deleted": "Scout gelöscht",
"deleteError": "Scout konnte nicht gelöscht werden",
"runStarted": "Scout-Ausführung gestartet",
"runError": "Scout konnte nicht gestartet werden"
}
},
"date": {

View File

@@ -99,9 +99,9 @@
"account": "Account",
"accountSubtitle": "security & access.",
"accountDescription": "Manage your security, connections, and account settings.",
"agents": "Agents",
"agentsSubtitle": "working for you.",
"agentsDescription": "Sub-agents that work on your behalf — collecting data from local files or cloud services, triggering recurring actions, and keeping your workspace in sync.",
"scouts": "Scouts",
"scoutsSubtitle": "working for you.",
"scoutsDescription": "Scouts watch your data sources — local files, mailboxes, cloud services — and surface what matters in your task brief.",
"aiPreferences": "AI Preferences",
"aiPreferencesSubtitle": "tailored to you.",
"aiPreferencesDescription": "Personalize how the AI responds to you.",
@@ -418,15 +418,15 @@
"webOnlyTooltip": "Folder linking available in desktop app"
}
},
"agents": {
"title": "Agents",
"scouts": {
"title": "Scouts",
"subtitle": "working for you.",
"description": "Sub-agents that work on your behalf — collecting data from local files or cloud services, triggering recurring actions, and keeping your workspace in sync.",
"noAgentsYet": "No agents yet",
"noAgentsDescription": "Create your first agent from a template. You can choose what data to extract, set a schedule, and edit instructions before saving.",
"createFirstAgent": "Create first agent",
"yourAgents": "Your Agents",
"createAgent": "Create agent"
"description": "Scouts watch your data sources — local files, mailboxes, cloud services — and surface what matters in your task brief.",
"noScoutsYet": "No scouts yet",
"noScoutsDescription": "Create your first scout from a template. Choose what data to extract, set a schedule, and edit instructions before saving.",
"createFirstScout": "Create first scout",
"yourScouts": "Your Scouts",
"createScout": "Create scout"
},
"toast": {
"profile": {
@@ -513,15 +513,15 @@
"createError": "Could not attach file.",
"tooLarge": "{{filename}} is too large (limit 50 MB)."
},
"agent": {
"created": "Agent created",
"createError": "Failed to create agent",
"updated": "Agent configuration saved",
"updateError": "Failed to save agent configuration",
"deleted": "Agent deleted",
"deleteError": "Failed to delete agent",
"runStarted": "Agent run started",
"runError": "Failed to start agent"
"scout": {
"created": "Scout created",
"createError": "Failed to create scout",
"updated": "Scout configuration saved",
"updateError": "Failed to save scout configuration",
"deleted": "Scout deleted",
"deleteError": "Failed to delete scout",
"runStarted": "Scout run started",
"runError": "Failed to start scout"
}
},
"date": {

View File

@@ -99,9 +99,9 @@
"account": "Cuenta",
"accountSubtitle": "seguridad y acceso.",
"accountDescription": "Gestiona tu seguridad, conexiones y configuración de cuenta.",
"agents": "Agentes",
"agentsSubtitle": "trabajando para ti.",
"agentsDescription": "Sub-agentes que trabajan en tu nombre — recopilando datos de archivos locales o servicios en la nube, activando acciones recurrentes y manteniendo tu espacio de trabajo sincronizado.",
"scouts": "Scouts",
"scoutsSubtitle": "trabajando para ti.",
"scoutsDescription": "Los scouts vigilan tus fuentes de datos archivos locales, buzones, servicios en la nube — y destacan lo que importa en tu brief de tareas.",
"aiPreferences": "Preferencias de IA",
"aiPreferencesSubtitle": "adaptado a ti.",
"aiPreferencesDescription": "Personaliza cómo la IA responde a tus solicitudes.",
@@ -418,15 +418,15 @@
"webOnlyTooltip": "La vinculación de carpetas está disponible en la app de escritorio"
}
},
"agents": {
"title": "Agentes",
"scouts": {
"title": "Scouts",
"subtitle": "trabajando para ti.",
"description": "Sub-agentes que trabajan en tu nombre — recopilando datos de archivos locales o servicios en la nube, activando acciones recurrentes y manteniendo tu espacio de trabajo sincronizado.",
"noAgentsYet": "No hay agentes",
"noAgentsDescription": "Crea tu primer agente desde una plantilla. Puedes elegir qué datos extraer, establecer un horario y editar las instrucciones antes de guardar.",
"createFirstAgent": "Crear primer agente",
"yourAgents": "Tus agentes",
"createAgent": "Crear agente"
"description": "Los scouts vigilan tus fuentes de datos archivos locales, buzones, servicios en la nube — y destacan lo que importa en tu brief de tareas.",
"noScoutsYet": "No hay scouts",
"noScoutsDescription": "Crea tu primer scout desde una plantilla. Elige qué datos extraer, establece un horario y edita las instrucciones antes de guardar.",
"createFirstScout": "Crear primer scout",
"yourScouts": "Tus scouts",
"createScout": "Crear scout"
},
"toast": {
"profile": {
@@ -513,15 +513,15 @@
"createError": "No se pudo adjuntar el archivo.",
"tooLarge": "{{filename}} es demasiado grande (límite 50 MB)."
},
"agent": {
"created": "Agente creado",
"createError": "Error al crear el agente",
"updated": "Configuración del agente guardada",
"scout": {
"created": "Scout creado",
"createError": "Error al crear el scout",
"updated": "Configuración del scout guardada",
"updateError": "Error al guardar la configuración",
"deleted": "Agente eliminado",
"deleteError": "Error al eliminar el agente",
"runStarted": "Ejecución del agente iniciada",
"runError": "Error al iniciar el agente"
"deleted": "Scout eliminado",
"deleteError": "Error al eliminar el scout",
"runStarted": "Ejecución del scout iniciada",
"runError": "Error al iniciar el scout"
}
},
"date": {

View File

@@ -99,9 +99,9 @@
"account": "Compte",
"accountSubtitle": "sécurité et accès.",
"accountDescription": "Gérez votre sécurité, connexions et paramètres de compte.",
"agents": "Agents",
"agentsSubtitle": "à votre service.",
"agentsDescription": "Sous-agents qui travaillent pour vous — collectant des données depuis des fichiers locaux ou services cloud, déclenchant des actions récurrentes et maintenant votre espace de travail synchronisé.",
"scouts": "Scouts",
"scoutsSubtitle": "qui travaillent pour vous.",
"scoutsDescription": "Les scouts surveillent vos sources de données fichiers locaux, boîtes mail, services cloud — et font remonter ce qui compte dans votre brief de tâches.",
"aiPreferences": "Préférences IA",
"aiPreferencesSubtitle": "adapté à vous.",
"aiPreferencesDescription": "Personnalisez la façon dont l'IA vous répond.",
@@ -418,15 +418,15 @@
"webOnlyTooltip": "La liaison de dossiers est disponible dans l'application bureau"
}
},
"agents": {
"title": "Agents",
"subtitle": "à votre service.",
"description": "Sous-agents qui travaillent pour vous — collectant des données depuis des fichiers locaux ou des services cloud, déclenchant des actions récurrentes et gardant votre espace de travail synchronisé.",
"noAgentsYet": "Aucun agent",
"noAgentsDescription": "Créez votre premier agent à partir d'un modèle. Vous pouvez choisir les données à extraire, définir un planning et modifier les instructions avant d'enregistrer.",
"createFirstAgent": "Créer le premier agent",
"yourAgents": "Vos agents",
"createAgent": "Créer un agent"
"scouts": {
"title": "Scouts",
"subtitle": "qui travaillent pour vous.",
"description": "Les scouts surveillent vos sources de données fichiers locaux, boîtes mail, services cloud — et font remonter ce qui compte dans votre brief de tâches.",
"noScoutsYet": "Aucun scout",
"noScoutsDescription": "Créez votre premier scout à partir d'un modèle. Choisissez les données à extraire, définissez un planning et modifiez les instructions avant d'enregistrer.",
"createFirstScout": "Créer le premier scout",
"yourScouts": "Vos scouts",
"createScout": "Créer un scout"
},
"toast": {
"profile": {
@@ -513,15 +513,15 @@
"createError": "Impossible de joindre le fichier.",
"tooLarge": "{{filename}} est trop volumineux (limite 50 Mo)."
},
"agent": {
"created": "Agent créé",
"createError": "Impossible de créer l'agent",
"updated": "Configuration de l'agent enregistrée",
"scout": {
"created": "Scout créé",
"createError": "Impossible de créer le scout",
"updated": "Configuration du scout enregistrée",
"updateError": "Impossible d'enregistrer la configuration",
"deleted": "Agent supprimé",
"deleteError": "Impossible de supprimer l'agent",
"runStarted": "Exécution de l'agent lancée",
"runError": "Impossible de lancer l'agent"
"deleted": "Scout supprimé",
"deleteError": "Impossible de supprimer le scout",
"runStarted": "Exécution du scout lancée",
"runError": "Impossible de lancer le scout"
}
},
"date": {

View File

@@ -99,9 +99,9 @@
"account": "Account",
"accountSubtitle": "sicurezza e accesso.",
"accountDescription": "Gestisci sicurezza, connessioni e impostazioni dell'account.",
"agents": "Agenti",
"agentsSubtitle": "al tuo servizio.",
"agentsDescription": "Sotto-agenti che lavorano per te — raccogliendo dati da file locali o servizi cloud, attivando azioni ricorrenti e mantenendo il tuo workspace sincronizzato.",
"scouts": "Scout",
"scoutsSubtitle": "che lavorano per te.",
"scoutsDescription": "Gli scout monitorano le tue fonti dati file locali, caselle email, servizi cloud — e mettono in evidenza ciò che conta nel tuo task brief.",
"aiPreferences": "Preferenze AI",
"aiPreferencesSubtitle": "su misura per te.",
"aiPreferencesDescription": "Personalizza come l'AI risponde alle tue richieste.",
@@ -418,15 +418,15 @@
"webOnlyTooltip": "Il collegamento cartelle è disponibile nell'app desktop"
}
},
"agents": {
"title": "Agenti",
"subtitle": "al tuo servizio.",
"description": "Sotto-agenti che lavorano per te — raccogliendo dati da file locali o servizi cloud, attivando azioni ricorrenti e mantenendo il tuo workspace sincronizzato.",
"noAgentsYet": "Nessun agente",
"noAgentsDescription": "Crea il tuo primo agente da un template. Puoi scegliere quali dati estrarre, impostare una pianificazione e modificare le istruzioni prima di salvare.",
"createFirstAgent": "Crea primo agente",
"yourAgents": "I tuoi agenti",
"createAgent": "Crea agente"
"scouts": {
"title": "Scout",
"subtitle": "che lavorano per te.",
"description": "Gli scout monitorano le tue fonti dati file locali, caselle email, servizi cloud — e mettono in evidenza ciò che conta nel tuo task brief.",
"noScoutsYet": "Nessuno scout",
"noScoutsDescription": "Crea il tuo primo scout da un template. Scegli quali dati estrarre, imposta una pianificazione e modifica le istruzioni prima di salvare.",
"createFirstScout": "Crea primo scout",
"yourScouts": "I tuoi scout",
"createScout": "Crea scout"
},
"toast": {
"profile": {
@@ -513,15 +513,15 @@
"createError": "Impossibile allegare il file.",
"tooLarge": "{{filename}} è troppo grande (limite 50 MB)."
},
"agent": {
"created": "Agente creato",
"createError": "Impossibile creare l'agente",
"updated": "Configurazione agente salvata",
"scout": {
"created": "Scout creato",
"createError": "Impossibile creare lo scout",
"updated": "Configurazione scout salvata",
"updateError": "Impossibile salvare la configurazione",
"deleted": "Agente eliminato",
"deleteError": "Impossibile eliminare l'agente",
"runStarted": "Esecuzione agente avviata",
"runError": "Impossibile avviare l'agente"
"deleted": "Scout eliminato",
"deleteError": "Impossibile eliminare lo scout",
"runStarted": "Esecuzione scout avviata",
"runError": "Impossibile avviare lo scout"
}
},
"date": {

View File

@@ -7,7 +7,7 @@ import { SidebarTrigger } from '@/components/ui/sidebar';
import { Separator } from '@/components/ui/separator';
import { ScrollArea } from '@/components/ui/scroll-area';
import { AccountSection } from '@/components/settings/AccountSection';
import { AgentsSection } from '@/components/settings/AgentsSection';
import { ScoutsSection } from '@/components/settings/ScoutsSection';
import { AppearanceSection } from '@/components/settings/AppearanceSection';
import { BillingSection } from '@/components/settings/BillingSection';
import { MemorySection } from '@/components/settings/MemorySection';
@@ -69,8 +69,8 @@ function SettingsPage() {
: t(`settings.${section}Subtitle`)}
</span>
</h1>
{section === 'agents' && (
<p className="text-sm text-muted-foreground mt-3 leading-relaxed">{t('settings.agentsDescription')}</p>
{section === 'scouts' && (
<p className="text-sm text-muted-foreground mt-3 leading-relaxed">{t('settings.scoutsDescription')}</p>
)}
</div>
{section === 'profile' && <ProfileSection />}
@@ -78,7 +78,7 @@ function SettingsPage() {
{section === 'account' && <AccountSection />}
{section === 'billing' && <BillingSection />}
{section === 'appearance' && <AppearanceSection />}
{section === 'agents' && <AgentsSection />}
{section === 'scouts' && <ScoutsSection />}
</div>
</ScrollArea>
</div>

View File

@@ -77,12 +77,12 @@ export type WsToolResult = z.infer<typeof WsToolResultSchema>;
/**
* First frame sent by Electron on the persistent device WS connection.
* Identifies the device and the agent configs it owns.
* Identifies the device and the scout configs it owns.
*/
export const WsDeviceHelloSchema = z.object({
type: z.literal('device_hello'),
deviceId: z.string(),
agentIds: z.array(z.string()),
scoutIds: z.array(z.string()),
});
export type WsDeviceHello = z.infer<typeof WsDeviceHelloSchema>;
@@ -322,25 +322,25 @@ export const AgentCatalogItemSchema = z.object({
});
export type AgentCatalogItem = z.infer<typeof AgentCatalogItemSchema>;
/** A configured local directory agent stored on the backend. */
export const LocalAgentConfigSchema = z.object({
/** A configured local directory scout stored on the backend. */
export const LocalScoutConfigSchema = z.object({
id: z.string(),
userId: z.string(),
deviceId: z.string(),
name: z.string(),
directoryPaths: z.array(z.string()),
dataTypes: z.array(z.string()),
agentConfig: z.record(z.string(), z.unknown()).nullable(),
scoutConfig: z.record(z.string(), z.unknown()).nullable(),
scheduleCron: z.string(),
enabled: z.boolean(),
lastRunAt: z.number().int().nullable().optional(),
createdAt: z.number().int(),
updatedAt: z.number().int(),
});
export type LocalAgentConfig = z.infer<typeof LocalAgentConfigSchema>;
export type LocalScoutConfig = z.infer<typeof LocalScoutConfigSchema>;
/** A configured cloud connector agent stored on the backend. */
export const CloudAgentConfigSchema = z.object({
/** A configured cloud connector scout stored on the backend. */
export const CloudScoutConfigSchema = z.object({
id: z.string(),
userId: z.string(),
provider: z.enum(['gmail', 'teams', 'outlook']),
@@ -354,7 +354,7 @@ export const CloudAgentConfigSchema = z.object({
createdAt: z.number().int(),
updatedAt: z.number().int(),
});
export type CloudAgentConfig = z.infer<typeof CloudAgentConfigSchema>;
export type CloudScoutConfig = z.infer<typeof CloudScoutConfigSchema>;
/** A single agent run log entry returned by GET /api/v1/agents/runs. */
export const AgentRunLogSchema = z.object({