Compare commits
5 Commits
c1b1b289c1
...
5cd895f04e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cd895f04e | ||
|
|
49b1d60fca | ||
|
|
b258ec3de5 | ||
|
|
f0a18d7011 | ||
|
|
9b66dc3329 |
@@ -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).
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
44
src/main/db/migrations/0007_scouts_rename.sql
Normal file
44
src/main/db/migrations/0007_scouts_rename.sql
Normal 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`;
|
||||
1058
src/main/db/migrations/meta/0007_snapshot.json
Normal file
1058
src/main/db/migrations/meta/0007_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,13 @@
|
||||
"when": 1778777130582,
|
||||
"tag": "0006_misty_cammi",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "6",
|
||||
"when": 1747353600000,
|
||||
"tag": "0007_scouts_rename",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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[]>([]);
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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>
|
||||
165
src/renderer/components/settings/ScoutsSection.tsx
Normal file
165
src/renderer/components/settings/ScoutsSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user