agent-office 0.4.6 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -21
- package/dist/cli.js +100 -4
- package/dist/commands/communicator.js +582 -0
- package/dist/commands/task-board.d.ts +29 -0
- package/dist/commands/task-board.js +251 -0
- package/dist/commands/worker.d.ts +2 -1
- package/dist/commands/worker.js +7 -3
- package/dist/db/index.d.ts +23 -0
- package/dist/db/postgresql-storage.d.ts +17 -1
- package/dist/db/postgresql-storage.js +175 -0
- package/dist/db/sqlite-storage.d.ts +17 -1
- package/dist/db/sqlite-storage.js +241 -0
- package/dist/db/storage-base.d.ts +17 -1
- package/dist/db/storage.d.ts +17 -1
- package/dist/manage/app.js +6 -1
- package/dist/manage/components/CronRequests.d.ts +8 -0
- package/dist/manage/components/CronRequests.js +181 -0
- package/dist/manage/hooks/useApi.d.ts +22 -0
- package/dist/manage/hooks/useApi.js +18 -0
- package/dist/server/routes.js +184 -35
- package/package.json +1 -1
|
@@ -361,6 +361,208 @@ export class AgentOfficeSqliteStorage extends AgentOfficeStorageBase {
|
|
|
361
361
|
`);
|
|
362
362
|
stmt.run(cronJobId, executedAt.toISOString(), success ? 1 : 0, errorMessage ?? null);
|
|
363
363
|
}
|
|
364
|
+
// Cron Requests
|
|
365
|
+
async listCronRequests(filters) {
|
|
366
|
+
let query = `
|
|
367
|
+
SELECT id, name, session_name, schedule, timezone, message, status, requested_at, reviewed_at, reviewed_by, reviewer_notes
|
|
368
|
+
FROM cron_requests
|
|
369
|
+
WHERE 1=1
|
|
370
|
+
`;
|
|
371
|
+
const params = [];
|
|
372
|
+
if (filters?.status) {
|
|
373
|
+
query += ` AND status = ?`;
|
|
374
|
+
params.push(filters.status);
|
|
375
|
+
}
|
|
376
|
+
if (filters?.sessionName) {
|
|
377
|
+
query += ` AND session_name = ?`;
|
|
378
|
+
params.push(filters.sessionName);
|
|
379
|
+
}
|
|
380
|
+
query += ` ORDER BY requested_at DESC`;
|
|
381
|
+
const stmt = this.db.prepare(query);
|
|
382
|
+
const rows = stmt.all(...params);
|
|
383
|
+
return rows.map(row => ({
|
|
384
|
+
...row,
|
|
385
|
+
requested_at: new Date(row.requested_at),
|
|
386
|
+
reviewed_at: row.reviewed_at ? new Date(row.reviewed_at) : null,
|
|
387
|
+
}));
|
|
388
|
+
}
|
|
389
|
+
async getCronRequestById(id) {
|
|
390
|
+
const stmt = this.db.prepare(`
|
|
391
|
+
SELECT id, name, session_name, schedule, timezone, message, status, requested_at, reviewed_at, reviewed_by, reviewer_notes
|
|
392
|
+
FROM cron_requests WHERE id = ?
|
|
393
|
+
`);
|
|
394
|
+
const row = stmt.get(id);
|
|
395
|
+
if (!row)
|
|
396
|
+
return null;
|
|
397
|
+
return {
|
|
398
|
+
...row,
|
|
399
|
+
requested_at: new Date(row.requested_at),
|
|
400
|
+
reviewed_at: row.reviewed_at ? new Date(row.reviewed_at) : null,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
async createCronRequest(name, sessionName, schedule, timezone, message) {
|
|
404
|
+
const stmt = this.db.prepare(`
|
|
405
|
+
INSERT INTO cron_requests (name, session_name, schedule, timezone, message)
|
|
406
|
+
VALUES (?, ?, ?, ?, ?)
|
|
407
|
+
RETURNING id, name, session_name, schedule, timezone, message, status, requested_at, reviewed_at, reviewed_by, reviewer_notes
|
|
408
|
+
`);
|
|
409
|
+
const row = stmt.get(name, sessionName, schedule, timezone, message);
|
|
410
|
+
return {
|
|
411
|
+
...row,
|
|
412
|
+
requested_at: new Date(row.requested_at),
|
|
413
|
+
reviewed_at: row.reviewed_at ? new Date(row.reviewed_at) : null,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
async updateCronRequestStatus(id, status, reviewedBy, reviewerNotes) {
|
|
417
|
+
const stmt = this.db.prepare(`
|
|
418
|
+
UPDATE cron_requests
|
|
419
|
+
SET status = ?, reviewed_at = ?, reviewed_by = ?, reviewer_notes = ?
|
|
420
|
+
WHERE id = ?
|
|
421
|
+
RETURNING id, name, session_name, schedule, timezone, message, status, requested_at, reviewed_at, reviewed_by, reviewer_notes
|
|
422
|
+
`);
|
|
423
|
+
const row = stmt.get(status, new Date().toISOString(), reviewedBy, reviewerNotes ?? null, id);
|
|
424
|
+
if (!row)
|
|
425
|
+
return null;
|
|
426
|
+
return {
|
|
427
|
+
...row,
|
|
428
|
+
requested_at: new Date(row.requested_at),
|
|
429
|
+
reviewed_at: row.reviewed_at ? new Date(row.reviewed_at) : null,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
// Tasks
|
|
433
|
+
async listTasks() {
|
|
434
|
+
const stmt = this.db.prepare(`
|
|
435
|
+
SELECT id, title, description, assignee, column_name, dependencies, created_at, updated_at
|
|
436
|
+
FROM tasks
|
|
437
|
+
ORDER BY created_at DESC
|
|
438
|
+
`);
|
|
439
|
+
const rows = stmt.all();
|
|
440
|
+
return rows.map(row => ({
|
|
441
|
+
id: row.id,
|
|
442
|
+
title: row.title,
|
|
443
|
+
description: row.description,
|
|
444
|
+
assignee: row.assignee,
|
|
445
|
+
column: row.column_name,
|
|
446
|
+
dependencies: JSON.parse(row.dependencies),
|
|
447
|
+
created_at: new Date(row.created_at),
|
|
448
|
+
updated_at: new Date(row.updated_at),
|
|
449
|
+
}));
|
|
450
|
+
}
|
|
451
|
+
async getTaskById(id) {
|
|
452
|
+
const stmt = this.db.prepare(`
|
|
453
|
+
SELECT id, title, description, assignee, column_name, dependencies, created_at, updated_at
|
|
454
|
+
FROM tasks WHERE id = ?
|
|
455
|
+
`);
|
|
456
|
+
const row = stmt.get(id);
|
|
457
|
+
if (!row)
|
|
458
|
+
return null;
|
|
459
|
+
return {
|
|
460
|
+
id: row.id,
|
|
461
|
+
title: row.title,
|
|
462
|
+
description: row.description,
|
|
463
|
+
assignee: row.assignee,
|
|
464
|
+
column: row.column_name,
|
|
465
|
+
dependencies: JSON.parse(row.dependencies),
|
|
466
|
+
created_at: new Date(row.created_at),
|
|
467
|
+
updated_at: new Date(row.updated_at),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
async createTask(title, description, assignee, column, dependencies) {
|
|
471
|
+
const now = new Date().toISOString();
|
|
472
|
+
const stmt = this.db.prepare(`
|
|
473
|
+
INSERT INTO tasks (title, description, assignee, column_name, dependencies, created_at, updated_at)
|
|
474
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
475
|
+
RETURNING id, title, description, assignee, column_name, dependencies, created_at, updated_at
|
|
476
|
+
`);
|
|
477
|
+
const row = stmt.get(title, description, assignee, column, JSON.stringify(dependencies), now, now);
|
|
478
|
+
return {
|
|
479
|
+
id: row.id,
|
|
480
|
+
title: row.title,
|
|
481
|
+
description: row.description,
|
|
482
|
+
assignee: row.assignee,
|
|
483
|
+
column: row.column_name,
|
|
484
|
+
dependencies: JSON.parse(row.dependencies),
|
|
485
|
+
created_at: new Date(row.created_at),
|
|
486
|
+
updated_at: new Date(row.updated_at),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
async updateTask(id, updates) {
|
|
490
|
+
const setParts = [];
|
|
491
|
+
const values = [];
|
|
492
|
+
if (updates.title !== undefined) {
|
|
493
|
+
setParts.push('title = ?');
|
|
494
|
+
values.push(updates.title);
|
|
495
|
+
}
|
|
496
|
+
if (updates.description !== undefined) {
|
|
497
|
+
setParts.push('description = ?');
|
|
498
|
+
values.push(updates.description);
|
|
499
|
+
}
|
|
500
|
+
if (updates.assignee !== undefined) {
|
|
501
|
+
setParts.push('assignee = ?');
|
|
502
|
+
values.push(updates.assignee);
|
|
503
|
+
}
|
|
504
|
+
if (updates.column !== undefined) {
|
|
505
|
+
setParts.push('column_name = ?');
|
|
506
|
+
values.push(updates.column);
|
|
507
|
+
}
|
|
508
|
+
if (updates.dependencies !== undefined) {
|
|
509
|
+
setParts.push('dependencies = ?');
|
|
510
|
+
values.push(JSON.stringify(updates.dependencies));
|
|
511
|
+
}
|
|
512
|
+
if (setParts.length === 0)
|
|
513
|
+
return this.getTaskById(id);
|
|
514
|
+
setParts.push('updated_at = ?');
|
|
515
|
+
values.push(new Date().toISOString());
|
|
516
|
+
const sql = `UPDATE tasks SET ${setParts.join(', ')} WHERE id = ? RETURNING id, title, description, assignee, column_name, dependencies, created_at, updated_at`;
|
|
517
|
+
values.push(id);
|
|
518
|
+
const stmt = this.db.prepare(sql);
|
|
519
|
+
const row = stmt.get(...values);
|
|
520
|
+
if (!row)
|
|
521
|
+
return null;
|
|
522
|
+
return {
|
|
523
|
+
id: row.id,
|
|
524
|
+
title: row.title,
|
|
525
|
+
description: row.description,
|
|
526
|
+
assignee: row.assignee,
|
|
527
|
+
column: row.column_name,
|
|
528
|
+
dependencies: JSON.parse(row.dependencies),
|
|
529
|
+
created_at: new Date(row.created_at),
|
|
530
|
+
updated_at: new Date(row.updated_at),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
async deleteTask(id) {
|
|
534
|
+
const stmt = this.db.prepare(`DELETE FROM tasks WHERE id = ?`);
|
|
535
|
+
stmt.run(id);
|
|
536
|
+
}
|
|
537
|
+
async searchTasks(query, filters) {
|
|
538
|
+
let sql = `
|
|
539
|
+
SELECT id, title, description, assignee, column_name, dependencies, created_at, updated_at
|
|
540
|
+
FROM tasks
|
|
541
|
+
WHERE (title LIKE ? OR description LIKE ?)
|
|
542
|
+
`;
|
|
543
|
+
const params = [`${query}%`, `${query}%`];
|
|
544
|
+
if (filters?.assignee) {
|
|
545
|
+
sql += ` AND assignee = ?`;
|
|
546
|
+
params.push(filters.assignee);
|
|
547
|
+
}
|
|
548
|
+
if (filters?.column) {
|
|
549
|
+
sql += ` AND column_name = ?`;
|
|
550
|
+
params.push(filters.column);
|
|
551
|
+
}
|
|
552
|
+
sql += ` ORDER BY created_at DESC`;
|
|
553
|
+
const stmt = this.db.prepare(sql);
|
|
554
|
+
const rows = stmt.all(...params);
|
|
555
|
+
return rows.map(row => ({
|
|
556
|
+
id: row.id,
|
|
557
|
+
title: row.title,
|
|
558
|
+
description: row.description,
|
|
559
|
+
assignee: row.assignee,
|
|
560
|
+
column: row.column_name,
|
|
561
|
+
dependencies: JSON.parse(row.dependencies),
|
|
562
|
+
created_at: new Date(row.created_at),
|
|
563
|
+
updated_at: new Date(row.updated_at),
|
|
564
|
+
}));
|
|
565
|
+
}
|
|
364
566
|
// Migrations
|
|
365
567
|
async runMigrations() {
|
|
366
568
|
const MIGRATIONS = [
|
|
@@ -488,6 +690,45 @@ export class AgentOfficeSqliteStorage extends AgentOfficeStorageBase {
|
|
|
488
690
|
name: "add_notified_to_messages",
|
|
489
691
|
sql: `
|
|
490
692
|
ALTER TABLE messages ADD COLUMN notified INTEGER NOT NULL DEFAULT 0;
|
|
693
|
+
`,
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
version: 10,
|
|
697
|
+
name: "create_tasks_table",
|
|
698
|
+
sql: `
|
|
699
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
700
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
701
|
+
title TEXT NOT NULL,
|
|
702
|
+
description TEXT NOT NULL,
|
|
703
|
+
assignee TEXT,
|
|
704
|
+
column_name TEXT NOT NULL,
|
|
705
|
+
dependencies TEXT NOT NULL DEFAULT '[]',
|
|
706
|
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
707
|
+
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
708
|
+
);
|
|
709
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON tasks(assignee);
|
|
710
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_column ON tasks(column_name);
|
|
711
|
+
`,
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
version: 11,
|
|
715
|
+
name: "create_cron_requests_table",
|
|
716
|
+
sql: `
|
|
717
|
+
CREATE TABLE IF NOT EXISTS cron_requests (
|
|
718
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
719
|
+
name TEXT NOT NULL,
|
|
720
|
+
session_name TEXT NOT NULL REFERENCES sessions(name) ON DELETE CASCADE,
|
|
721
|
+
schedule TEXT NOT NULL,
|
|
722
|
+
timezone TEXT,
|
|
723
|
+
message TEXT NOT NULL,
|
|
724
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
725
|
+
requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
726
|
+
reviewed_at DATETIME,
|
|
727
|
+
reviewed_by TEXT,
|
|
728
|
+
reviewer_notes TEXT
|
|
729
|
+
);
|
|
730
|
+
CREATE INDEX IF NOT EXISTS idx_cron_requests_session_name ON cron_requests(session_name);
|
|
731
|
+
CREATE INDEX IF NOT EXISTS idx_cron_requests_status ON cron_requests(status);
|
|
491
732
|
`,
|
|
492
733
|
},
|
|
493
734
|
];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentOfficeStorage, SessionRow, ConfigRow, MessageRow, CronJobRow, CronHistoryRow } from "./index.js";
|
|
1
|
+
import type { AgentOfficeStorage, SessionRow, ConfigRow, MessageRow, CronJobRow, CronHistoryRow, CronRequestRow, TaskRow } from "./index.js";
|
|
2
2
|
import type { WatchListener, WatchState, SenderInfo } from "./storage.js";
|
|
3
3
|
export { WatchListener, WatchState, SenderInfo };
|
|
4
4
|
export declare abstract class AgentOfficeStorageBase implements AgentOfficeStorage {
|
|
@@ -44,6 +44,22 @@ export declare abstract class AgentOfficeStorageBase implements AgentOfficeStora
|
|
|
44
44
|
abstract cronJobExistsForSession(name: string, sessionName: string): Promise<boolean>;
|
|
45
45
|
abstract listCronHistory(cronJobId: number, limit: number): Promise<CronHistoryRow[]>;
|
|
46
46
|
abstract createCronHistory(cronJobId: number, executedAt: Date, success: boolean, errorMessage?: string): Promise<void>;
|
|
47
|
+
abstract listCronRequests(filters?: {
|
|
48
|
+
status?: string;
|
|
49
|
+
sessionName?: string;
|
|
50
|
+
}): Promise<CronRequestRow[]>;
|
|
51
|
+
abstract getCronRequestById(id: number): Promise<CronRequestRow | null>;
|
|
52
|
+
abstract createCronRequest(name: string, sessionName: string, schedule: string, timezone: string | null, message: string): Promise<CronRequestRow>;
|
|
53
|
+
abstract updateCronRequestStatus(id: number, status: "approved" | "rejected", reviewedBy: string, reviewerNotes?: string): Promise<CronRequestRow | null>;
|
|
54
|
+
abstract listTasks(): Promise<TaskRow[]>;
|
|
55
|
+
abstract getTaskById(id: number): Promise<TaskRow | null>;
|
|
56
|
+
abstract createTask(title: string, description: string, assignee: string | null, column: string, dependencies: number[]): Promise<TaskRow>;
|
|
57
|
+
abstract updateTask(id: number, updates: Partial<Pick<TaskRow, 'title' | 'description' | 'assignee' | 'column' | 'dependencies'>>): Promise<TaskRow | null>;
|
|
58
|
+
abstract deleteTask(id: number): Promise<void>;
|
|
59
|
+
abstract searchTasks(query: string, filters?: {
|
|
60
|
+
assignee?: string;
|
|
61
|
+
column?: string;
|
|
62
|
+
}): Promise<TaskRow[]>;
|
|
47
63
|
abstract runMigrations(): Promise<void>;
|
|
48
64
|
abstract createMessageImpl(from: string, to: string, body: string): Promise<MessageRow>;
|
|
49
65
|
/**
|
package/dist/db/storage.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SessionRow, ConfigRow, MessageRow, CronJobRow, CronHistoryRow } from "./index.js";
|
|
1
|
+
import type { SessionRow, ConfigRow, MessageRow, CronJobRow, CronHistoryRow, CronRequestRow } from "./index.js";
|
|
2
2
|
export interface SenderInfo {
|
|
3
3
|
lastSent: string;
|
|
4
4
|
}
|
|
@@ -54,5 +54,21 @@ export interface AgentOfficeStorage {
|
|
|
54
54
|
cronJobExistsForSession(name: string, sessionName: string): Promise<boolean>;
|
|
55
55
|
listCronHistory(cronJobId: number, limit: number): Promise<CronHistoryRow[]>;
|
|
56
56
|
createCronHistory(cronJobId: number, executedAt: Date, success: boolean, errorMessage?: string): Promise<void>;
|
|
57
|
+
listCronRequests(filters?: {
|
|
58
|
+
status?: string;
|
|
59
|
+
sessionName?: string;
|
|
60
|
+
}): Promise<CronRequestRow[]>;
|
|
61
|
+
getCronRequestById(id: number): Promise<CronRequestRow | null>;
|
|
62
|
+
createCronRequest(name: string, sessionName: string, schedule: string, timezone: string | null, message: string): Promise<CronRequestRow>;
|
|
63
|
+
updateCronRequestStatus(id: number, status: "approved" | "rejected", reviewedBy: string, reviewerNotes?: string): Promise<CronRequestRow | null>;
|
|
64
|
+
listTasks(): Promise<import("./index.js").TaskRow[]>;
|
|
65
|
+
getTaskById(id: number): Promise<import("./index.js").TaskRow | null>;
|
|
66
|
+
createTask(title: string, description: string, assignee: string | null, column: string, dependencies: number[]): Promise<import("./index.js").TaskRow>;
|
|
67
|
+
updateTask(id: number, updates: Partial<Pick<import("./index.js").TaskRow, 'title' | 'description' | 'assignee' | 'column' | 'dependencies'>>): Promise<import("./index.js").TaskRow | null>;
|
|
68
|
+
deleteTask(id: number): Promise<void>;
|
|
69
|
+
searchTasks(query: string, filters?: {
|
|
70
|
+
assignee?: string;
|
|
71
|
+
column?: string;
|
|
72
|
+
}): Promise<import("./index.js").TaskRow[]>;
|
|
57
73
|
runMigrations(): Promise<void>;
|
|
58
74
|
}
|
package/dist/manage/app.js
CHANGED
|
@@ -10,15 +10,17 @@ import { MyMail } from "./components/MyMail.js";
|
|
|
10
10
|
import { SessionSidebar } from "./components/SessionSidebar.js";
|
|
11
11
|
import { MenuSelect } from "./components/MenuSelect.js";
|
|
12
12
|
import { CronList } from "./components/CronList.js";
|
|
13
|
+
import { CronRequests } from "./components/CronRequests.js";
|
|
13
14
|
const MENU_OPTIONS = [
|
|
14
15
|
{ label: "Send message", value: "send-message" },
|
|
15
16
|
{ label: "My mail", value: "my-mail" },
|
|
16
17
|
{ label: "Coworkers", value: "list" },
|
|
17
18
|
{ label: "Cron jobs", value: "cron" },
|
|
19
|
+
{ label: "Cron requests", value: "cron-requests" },
|
|
18
20
|
{ label: "My profile", value: "profile" },
|
|
19
21
|
{ label: "Quit", value: "quit" },
|
|
20
22
|
];
|
|
21
|
-
const SUB_SCREENS = ["list", "send-message", "my-mail", "profile", "cron"];
|
|
23
|
+
const SUB_SCREENS = ["list", "send-message", "my-mail", "profile", "cron", "cron-requests"];
|
|
22
24
|
const FOOTER_HINTS = {
|
|
23
25
|
connecting: "",
|
|
24
26
|
"auth-error": "",
|
|
@@ -28,6 +30,7 @@ const FOOTER_HINTS = {
|
|
|
28
30
|
"my-mail": "↑↓ select message · r reply · m mark read · a mark all read · s sent tab · Esc back",
|
|
29
31
|
"profile": "Enter submit · Esc back to menu",
|
|
30
32
|
cron: "↑↓ navigate · c create · d delete · e enable/disable · h history · Esc back",
|
|
33
|
+
"cron-requests": "↑↓ navigate · a approve · r reject · v view · f filter · Esc back",
|
|
31
34
|
};
|
|
32
35
|
function Header({ serverUrl, unreadCount }) {
|
|
33
36
|
return (_jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: "cyan", children: "agent-office" }), _jsxs(Box, { gap: 2, children: [unreadCount > 0 && (_jsxs(Text, { color: "yellow", bold: true, children: ["\u2709 ", unreadCount, " unread"] })), _jsx(Text, { dimColor: true, children: serverUrl })] })] }));
|
|
@@ -117,6 +120,8 @@ export function App({ serverUrl, password }) {
|
|
|
117
120
|
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(MyMail, { serverUrl: serverUrl, password: password, onBack: goBack, contentHeight: contentHeight - 2, onReply: (name) => { setReplyTo(name); setScreen("send-message"); } }) }));
|
|
118
121
|
case "cron":
|
|
119
122
|
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(CronList, { serverUrl: serverUrl, password: password, onBack: goBack, contentHeight: contentHeight - 2 }) }));
|
|
123
|
+
case "cron-requests":
|
|
124
|
+
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(CronRequests, { serverUrl: serverUrl, password: password, onBack: goBack, contentHeight: contentHeight - 2 }) }));
|
|
120
125
|
}
|
|
121
126
|
};
|
|
122
127
|
return (_jsxs(Box, { flexDirection: "column", width: termWidth, children: [_jsx(Header, { serverUrl: serverUrl, unreadCount: unreadCount }), renderContent(), screen !== "connecting" && (_jsx(Footer, { hint: FOOTER_HINTS[screen] }))] }));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
interface CronRequestsProps {
|
|
2
|
+
serverUrl: string;
|
|
3
|
+
password: string;
|
|
4
|
+
onBack: () => void;
|
|
5
|
+
contentHeight: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function CronRequests({ serverUrl, password, onBack, contentHeight }: CronRequestsProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState, useCallback } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { TextInput, Spinner, ConfirmInput } from "@inkjs/ui";
|
|
5
|
+
import { useApi, useAsyncState } from "../hooks/useApi.js";
|
|
6
|
+
export function CronRequests({ serverUrl, password, onBack, contentHeight }) {
|
|
7
|
+
const { listCronRequests, approveCronRequest, rejectCronRequest } = useApi(serverUrl, password);
|
|
8
|
+
const { data: requests, loading, error: loadError, run } = useAsyncState();
|
|
9
|
+
const [cursor, setCursor] = useState(0);
|
|
10
|
+
const [mode, setMode] = useState("list");
|
|
11
|
+
const [actionMsg, setActionMsg] = useState(null);
|
|
12
|
+
const [actionError, setActionError] = useState(null);
|
|
13
|
+
const [rejectNotes, setRejectNotes] = useState("");
|
|
14
|
+
const [showAll, setShowAll] = useState(false);
|
|
15
|
+
const filteredRequests = (requests ?? []).filter((r) => showAll || r.status === "pending");
|
|
16
|
+
const reload = useCallback(() => {
|
|
17
|
+
void run(() => listCronRequests(showAll ? undefined : { status: "pending" }));
|
|
18
|
+
}, [showAll]);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
reload();
|
|
21
|
+
}, [showAll]);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (filteredRequests.length > 0) {
|
|
24
|
+
setCursor((c) => Math.min(c, filteredRequests.length - 1));
|
|
25
|
+
}
|
|
26
|
+
}, [filteredRequests.length]);
|
|
27
|
+
useInput((input, key) => {
|
|
28
|
+
if (key.escape && mode === "list") {
|
|
29
|
+
onBack();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (key.escape && (mode === "confirm-approve" || mode === "confirm-reject" || mode === "reject-notes" || mode === "view-message")) {
|
|
33
|
+
setMode("list");
|
|
34
|
+
setRejectNotes("");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (loading)
|
|
38
|
+
return;
|
|
39
|
+
if (mode === "list") {
|
|
40
|
+
if (key.upArrow)
|
|
41
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
42
|
+
if (key.downArrow)
|
|
43
|
+
setCursor((c) => Math.min(filteredRequests.length - 1, c + 1));
|
|
44
|
+
if (filteredRequests.length > 0) {
|
|
45
|
+
const selected = filteredRequests[cursor];
|
|
46
|
+
if (input === "a" && selected?.status === "pending") {
|
|
47
|
+
setActionError(null);
|
|
48
|
+
setActionMsg(null);
|
|
49
|
+
setMode("confirm-approve");
|
|
50
|
+
}
|
|
51
|
+
if (input === "r" && selected?.status === "pending") {
|
|
52
|
+
setActionError(null);
|
|
53
|
+
setActionMsg(null);
|
|
54
|
+
setRejectNotes("");
|
|
55
|
+
setMode("reject-notes");
|
|
56
|
+
}
|
|
57
|
+
if (input === "v") {
|
|
58
|
+
setMode("view-message");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (input === "f") {
|
|
62
|
+
setShowAll((prev) => !prev);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (mode === "confirm-approve") {
|
|
66
|
+
if (key.return || input === "y") {
|
|
67
|
+
handleApprove();
|
|
68
|
+
}
|
|
69
|
+
if (input === "n" || key.escape) {
|
|
70
|
+
setMode("list");
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (mode === "confirm-reject") {
|
|
75
|
+
if (key.return || input === "y") {
|
|
76
|
+
handleReject();
|
|
77
|
+
}
|
|
78
|
+
if (input === "n" || key.escape) {
|
|
79
|
+
setMode("list");
|
|
80
|
+
setRejectNotes("");
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
const handleApprove = async () => {
|
|
86
|
+
const selected = filteredRequests[cursor];
|
|
87
|
+
if (!selected)
|
|
88
|
+
return;
|
|
89
|
+
setMode("approving");
|
|
90
|
+
try {
|
|
91
|
+
await approveCronRequest(selected.id);
|
|
92
|
+
setActionMsg(`Cron request "${selected.name}" approved and scheduled.`);
|
|
93
|
+
setMode("list");
|
|
94
|
+
reload();
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
setActionError(err instanceof Error ? err.message : String(err));
|
|
98
|
+
setMode("list");
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const handleReject = async () => {
|
|
102
|
+
const selected = filteredRequests[cursor];
|
|
103
|
+
if (!selected)
|
|
104
|
+
return;
|
|
105
|
+
setMode("rejecting");
|
|
106
|
+
try {
|
|
107
|
+
await rejectCronRequest(selected.id, rejectNotes || undefined);
|
|
108
|
+
setActionMsg(`Cron request "${selected.name}" rejected.`);
|
|
109
|
+
setMode("list");
|
|
110
|
+
setRejectNotes("");
|
|
111
|
+
reload();
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
setActionError(err instanceof Error ? err.message : String(err));
|
|
115
|
+
setMode("list");
|
|
116
|
+
setRejectNotes("");
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const formatDate = (dateStr) => {
|
|
120
|
+
try {
|
|
121
|
+
return new Date(dateStr).toLocaleString();
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return dateStr;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const statusColor = (status) => {
|
|
128
|
+
switch (status) {
|
|
129
|
+
case "pending": return "yellow";
|
|
130
|
+
case "approved": return "green";
|
|
131
|
+
case "rejected": return "red";
|
|
132
|
+
default: return "white";
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
if (loading && filteredRequests.length === 0) {
|
|
136
|
+
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Loading cron requests..." }) }));
|
|
137
|
+
}
|
|
138
|
+
if (loadError) {
|
|
139
|
+
return (_jsxs(Box, { height: contentHeight, flexDirection: "column", gap: 1, children: [_jsx(Text, { color: "red", bold: true, children: "Error" }), _jsx(Text, { children: loadError })] }));
|
|
140
|
+
}
|
|
141
|
+
const renderActionPanel = () => {
|
|
142
|
+
if (mode === "approving") {
|
|
143
|
+
return (_jsx(Box, { borderStyle: "round", borderColor: "green", paddingX: 1, marginBottom: 1, children: _jsx(Spinner, { label: `Approving "${filteredRequests[cursor]?.name}"...` }) }));
|
|
144
|
+
}
|
|
145
|
+
if (mode === "rejecting") {
|
|
146
|
+
return (_jsx(Box, { borderStyle: "round", borderColor: "red", paddingX: 1, marginBottom: 1, children: _jsx(Spinner, { label: `Rejecting "${filteredRequests[cursor]?.name}"...` }) }));
|
|
147
|
+
}
|
|
148
|
+
if (mode === "confirm-approve" && filteredRequests[cursor]) {
|
|
149
|
+
const req = filteredRequests[cursor];
|
|
150
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "green", children: "Approve Cron Request" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["Approve ", _jsx(Text, { color: "cyan", bold: true, children: req.name }), " from ", _jsx(Text, { color: "magenta", children: req.session_name }), "?"] }), _jsxs(Text, { dimColor: true, children: ["Schedule: ", req.schedule] })] }), _jsx(Box, { marginTop: 1, children: _jsx(ConfirmInput, { defaultChoice: "cancel", onConfirm: () => void handleApprove(), onCancel: () => setMode("list") }) })] }));
|
|
151
|
+
}
|
|
152
|
+
if (mode === "confirm-reject" && filteredRequests[cursor]) {
|
|
153
|
+
const req = filteredRequests[cursor];
|
|
154
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Reject Cron Request" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["Reject ", _jsx(Text, { color: "cyan", bold: true, children: req.name }), " from ", _jsx(Text, { color: "magenta", children: req.session_name }), "?"] }), rejectNotes && _jsxs(Text, { dimColor: true, children: ["Notes: ", rejectNotes] })] }), _jsx(Box, { marginTop: 1, children: _jsx(ConfirmInput, { defaultChoice: "cancel", onConfirm: () => void handleReject(), onCancel: () => { setMode("list"); setRejectNotes(""); } }) })] }));
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
};
|
|
158
|
+
const renderRejectNotes = () => {
|
|
159
|
+
if (mode !== "reject-notes")
|
|
160
|
+
return null;
|
|
161
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, color: "red", children: "Reject Cron Request" }), _jsx(Text, { dimColor: true, children: filteredRequests[cursor]?.name })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Reason (optional, Enter to skip): " }), _jsx(TextInput, { placeholder: "e.g. Schedule too frequent", onSubmit: (v) => {
|
|
162
|
+
setRejectNotes(v);
|
|
163
|
+
setMode("confirm-reject");
|
|
164
|
+
} }, "reject-notes")] }), _jsx(Text, { dimColor: true, children: "Esc cancel" })] }));
|
|
165
|
+
};
|
|
166
|
+
const renderViewMessage = () => {
|
|
167
|
+
if (mode !== "view-message")
|
|
168
|
+
return null;
|
|
169
|
+
const selected = filteredRequests[cursor];
|
|
170
|
+
if (!selected)
|
|
171
|
+
return null;
|
|
172
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Request Details" }), _jsx(Text, { dimColor: true, children: selected.name })] }), _jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 0, flexDirection: "column", children: [_jsxs(Text, { children: ["From: ", _jsx(Text, { color: "magenta", bold: true, children: selected.session_name })] }), _jsxs(Text, { children: ["Schedule: ", _jsx(Text, { bold: true, children: selected.schedule })] }), selected.timezone && _jsxs(Text, { children: ["Timezone: ", selected.timezone] }), _jsxs(Text, { children: ["Status: ", _jsx(Text, { color: statusColor(selected.status), bold: true, children: selected.status })] }), _jsxs(Text, { children: ["Requested: ", formatDate(selected.requested_at)] }), selected.reviewed_at && _jsxs(Text, { children: ["Reviewed: ", formatDate(selected.reviewed_at), " by ", selected.reviewed_by] }), selected.reviewer_notes && _jsxs(Text, { children: ["Notes: ", selected.reviewer_notes] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, children: "Message:" }) }), _jsx(Text, { wrap: "wrap", children: selected.message })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Esc back" }) })] }));
|
|
173
|
+
};
|
|
174
|
+
const actionPanel = renderActionPanel();
|
|
175
|
+
const panelHeight = actionPanel ? 5 : 0;
|
|
176
|
+
const tableHeight = contentHeight - panelHeight - 5;
|
|
177
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [renderRejectNotes(), renderViewMessage(), (mode === "list" || mode === "confirm-approve" || mode === "confirm-reject" || mode === "approving" || mode === "rejecting") && (_jsxs(_Fragment, { children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Cron Requests" }), _jsxs(Text, { dimColor: true, children: ["[", filteredRequests.length, " ", showAll ? "total" : "pending", "]"] }), loading && _jsx(Spinner, {})] }), actionMsg && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "green", children: actionMsg }) })), actionError && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "red", children: actionError }) })), actionPanel, filteredRequests.length === 0 ? (_jsx(Box, { height: tableHeight, alignItems: "center", justifyContent: "center", children: _jsx(Text, { dimColor: true, children: showAll ? "No cron requests found." : "No pending cron requests. Press f to show all." }) })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: " NAME".padEnd(20) }), _jsx(Text, { bold: true, color: "cyan", children: "COWORKER".padEnd(15) }), _jsx(Text, { bold: true, color: "cyan", children: "SCHEDULE".padEnd(20) }), _jsx(Text, { bold: true, color: "cyan", children: "REQUESTED".padEnd(22) }), _jsx(Text, { bold: true, color: "cyan", children: "STATUS" })] }), filteredRequests.map((req, idx) => {
|
|
178
|
+
const selected = idx === cursor;
|
|
179
|
+
return (_jsxs(Box, { gap: 2, children: [_jsxs(Box, { width: 20, children: [_jsx(Text, { color: selected ? "cyan" : undefined, children: selected ? "▶ " : " " }), _jsx(Text, { color: selected ? "cyan" : "green", bold: selected, children: req.name })] }), _jsx(Box, { width: 15, children: _jsx(Text, { color: selected ? "magenta" : undefined, dimColor: !selected, children: req.session_name.padEnd(15) }) }), _jsx(Box, { width: 20, children: _jsx(Text, { dimColor: !selected, children: req.schedule.padEnd(20) }) }), _jsx(Box, { width: 22, children: _jsx(Text, { dimColor: !selected, children: formatDate(req.requested_at).padEnd(22) }) }), _jsx(Text, { color: statusColor(req.status), bold: selected, children: req.status })] }, req.id));
|
|
180
|
+
})] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["a approve \u00B7 r reject \u00B7 v view details \u00B7 f ", showAll ? "show pending only" : "show all", " \u00B7 Esc back"] }) })] }))] }));
|
|
181
|
+
}
|
|
@@ -55,6 +55,19 @@ export interface CronHistoryEntry {
|
|
|
55
55
|
success: boolean;
|
|
56
56
|
error_message: string | null;
|
|
57
57
|
}
|
|
58
|
+
export interface CronRequest {
|
|
59
|
+
id: number;
|
|
60
|
+
name: string;
|
|
61
|
+
session_name: string;
|
|
62
|
+
schedule: string;
|
|
63
|
+
timezone: string | null;
|
|
64
|
+
message: string;
|
|
65
|
+
status: "pending" | "approved" | "rejected";
|
|
66
|
+
requested_at: string;
|
|
67
|
+
reviewed_at: string | null;
|
|
68
|
+
reviewed_by: string | null;
|
|
69
|
+
reviewer_notes: string | null;
|
|
70
|
+
}
|
|
58
71
|
export declare function useApi(serverUrl: string, password: string): {
|
|
59
72
|
listSessions: () => Promise<Session[]>;
|
|
60
73
|
createSession: (name: string, agent: string) => Promise<Session>;
|
|
@@ -116,6 +129,15 @@ export declare function useApi(serverUrl: string, password: string): {
|
|
|
116
129
|
enableCron: (id: number) => Promise<CronJob>;
|
|
117
130
|
disableCron: (id: number) => Promise<CronJob>;
|
|
118
131
|
getCronHistory: (id: number, limit?: number) => Promise<CronHistoryEntry[]>;
|
|
132
|
+
listCronRequests: (filters?: {
|
|
133
|
+
status?: string;
|
|
134
|
+
sessionName?: string;
|
|
135
|
+
}) => Promise<CronRequest[]>;
|
|
136
|
+
approveCronRequest: (id: number, notes?: string) => Promise<{
|
|
137
|
+
request: CronRequest;
|
|
138
|
+
cron_job: CronJob;
|
|
139
|
+
}>;
|
|
140
|
+
rejectCronRequest: (id: number, notes?: string) => Promise<CronRequest>;
|
|
119
141
|
};
|
|
120
142
|
export declare function useAsyncState<T>(): {
|
|
121
143
|
run: (fn: () => Promise<T>) => Promise<T | null>;
|
|
@@ -115,6 +115,21 @@ export function useApi(serverUrl, password) {
|
|
|
115
115
|
const getCronHistory = useCallback(async (id, limit = 10) => {
|
|
116
116
|
return apiFetch(`${base}/crons/${id}/history?limit=${limit}`, password);
|
|
117
117
|
}, [base, password]);
|
|
118
|
+
const listCronRequests = useCallback(async (filters) => {
|
|
119
|
+
const params = new URLSearchParams();
|
|
120
|
+
if (filters?.status)
|
|
121
|
+
params.set("status", filters.status);
|
|
122
|
+
if (filters?.sessionName)
|
|
123
|
+
params.set("session_name", filters.sessionName);
|
|
124
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
125
|
+
return apiFetch(`${base}/cron-requests${query}`, password);
|
|
126
|
+
}, [base, password]);
|
|
127
|
+
const approveCronRequest = useCallback(async (id, notes) => {
|
|
128
|
+
return apiFetch(`${base}/cron-requests/${id}/approve`, password, { method: "POST", body: JSON.stringify({ notes }) });
|
|
129
|
+
}, [base, password]);
|
|
130
|
+
const rejectCronRequest = useCallback(async (id, notes) => {
|
|
131
|
+
return apiFetch(`${base}/cron-requests/${id}/reject`, password, { method: "POST", body: JSON.stringify({ notes }) });
|
|
132
|
+
}, [base, password]);
|
|
118
133
|
return {
|
|
119
134
|
listSessions,
|
|
120
135
|
createSession,
|
|
@@ -138,6 +153,9 @@ export function useApi(serverUrl, password) {
|
|
|
138
153
|
enableCron,
|
|
139
154
|
disableCron,
|
|
140
155
|
getCronHistory,
|
|
156
|
+
listCronRequests,
|
|
157
|
+
approveCronRequest,
|
|
158
|
+
rejectCronRequest,
|
|
141
159
|
};
|
|
142
160
|
}
|
|
143
161
|
export function useAsyncState() {
|