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.
@@ -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
  /**
@@ -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
  }
@@ -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() {