agent-office 0.0.1 → 0.0.2
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/dist/cli.js +84 -3
- package/dist/commands/serve.js +7 -1
- package/dist/commands/worker.d.ts +11 -0
- package/dist/commands/worker.js +47 -0
- package/dist/db/index.d.ts +19 -0
- package/dist/db/migrate.js +36 -0
- package/dist/manage/app.js +42 -43
- package/dist/manage/components/CronList.d.ts +9 -0
- package/dist/manage/components/CronList.js +310 -0
- package/dist/manage/components/ItemSelector.d.ts +7 -0
- package/dist/manage/components/ItemSelector.js +20 -0
- package/dist/manage/components/MenuSelect.d.ts +13 -0
- package/dist/manage/components/MenuSelect.js +22 -0
- package/dist/manage/components/MyMail.d.ts +2 -1
- package/dist/manage/components/MyMail.js +107 -34
- package/dist/manage/components/ReadMail.js +3 -3
- package/dist/manage/components/SendMessage.d.ts +2 -1
- package/dist/manage/components/SendMessage.js +9 -6
- package/dist/manage/components/SessionList.js +392 -31
- package/dist/manage/hooks/useApi.d.ts +43 -1
- package/dist/manage/hooks/useApi.js +34 -2
- package/dist/server/cron.d.ts +24 -0
- package/dist/server/cron.js +121 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -2
- package/dist/server/routes.d.ts +2 -1
- package/dist/server/routes.js +719 -38
- package/package.json +2 -1
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
|
-
import {
|
|
4
|
+
import { Spinner, TextInput } from "@inkjs/ui";
|
|
5
|
+
import { ItemSelector } from "./ItemSelector.js";
|
|
5
6
|
import { useApi, useAsyncState } from "../hooks/useApi.js";
|
|
6
|
-
export function SendMessage({ serverUrl, password, onBack, contentHeight }) {
|
|
7
|
+
export function SendMessage({ serverUrl, password, onBack, contentHeight, initialRecipient }) {
|
|
7
8
|
const { listSessions, sendMailMessage, getConfig } = useApi(serverUrl, password);
|
|
8
9
|
const { run: runList } = useAsyncState();
|
|
9
10
|
const [sessions, setSessions] = useState([]);
|
|
10
|
-
const [sessionsLoading, setSessionsLoading] = useState(
|
|
11
|
-
const [stage, setStage] = useState("select-recipients");
|
|
12
|
-
const [recipients, setRecipients] = useState([]);
|
|
11
|
+
const [sessionsLoading, setSessionsLoading] = useState(!initialRecipient);
|
|
12
|
+
const [stage, setStage] = useState(initialRecipient ? "enter-body" : "select-recipients");
|
|
13
|
+
const [recipients, setRecipients] = useState(initialRecipient ? [initialRecipient] : []);
|
|
13
14
|
const [messageBody, setMessageBody] = useState("");
|
|
14
15
|
const [error, setError] = useState(null);
|
|
15
16
|
const [submitted, setSubmitted] = useState(false);
|
|
@@ -20,6 +21,8 @@ export function SendMessage({ serverUrl, password, onBack, contentHeight }) {
|
|
|
20
21
|
});
|
|
21
22
|
}, [getConfig]);
|
|
22
23
|
useEffect(() => {
|
|
24
|
+
if (initialRecipient)
|
|
25
|
+
return; // no need to load sessions list when replying
|
|
23
26
|
runList(listSessions).then((rows) => {
|
|
24
27
|
setSessions(rows ?? []);
|
|
25
28
|
setSessionsLoading(false);
|
|
@@ -58,7 +61,7 @@ export function SendMessage({ serverUrl, password, onBack, contentHeight }) {
|
|
|
58
61
|
if (sessions.length === 0) {
|
|
59
62
|
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "No sessions yet. Create one first." }) }));
|
|
60
63
|
}
|
|
61
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Send Message" }), _jsx(Text, { dimColor: true, children: "Select a recipient agent:" }), _jsx(
|
|
64
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Send Message" }), _jsx(Text, { dimColor: true, children: "Select a recipient agent:" }), _jsx(ItemSelector, { items: sessions.map((s) => s.name), onSelect: handleRecipientSelect, onCancel: onBack })] }));
|
|
62
65
|
}
|
|
63
66
|
if (stage === "enter-body" && !submitted) {
|
|
64
67
|
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Send Message" }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "From: " }), _jsx(Text, { color: "cyan", children: senderName })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "To: " }), _jsx(Text, { color: "cyan", children: recipients[0] })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Body: " }), _jsx(TextInput, { placeholder: "Type your message...", onSubmit: handleBodySubmit })] })] }));
|
|
@@ -1,52 +1,413 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState, useCallback } from "react";
|
|
3
3
|
import { Box, Text, useInput } from "ink";
|
|
4
|
-
import { Spinner } from "@inkjs/ui";
|
|
4
|
+
import { TextInput, Spinner, ConfirmInput } from "@inkjs/ui";
|
|
5
5
|
import { useApi, useAsyncState } from "../hooks/useApi.js";
|
|
6
6
|
const MASKED_CODE = "••••••••-••••-••••-••••-••••••••••••";
|
|
7
|
+
function TailView({ serverUrl, password, sessionName, contentHeight, onClose }) {
|
|
8
|
+
const { getMessages } = useApi(serverUrl, password);
|
|
9
|
+
const [messages, setMessages] = useState([]);
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
const [error, setError] = useState(null);
|
|
12
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
getMessages(sessionName, 50)
|
|
15
|
+
.then((msgs) => { setMessages(msgs); setLoading(false); })
|
|
16
|
+
.catch((err) => { setError(err instanceof Error ? err.message : String(err)); setLoading(false); });
|
|
17
|
+
}, [sessionName]);
|
|
18
|
+
useInput((_input, key) => {
|
|
19
|
+
if (key.escape) {
|
|
20
|
+
onClose();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (key.upArrow)
|
|
24
|
+
setScrollOffset((o) => Math.max(0, o - 1));
|
|
25
|
+
if (key.downArrow)
|
|
26
|
+
setScrollOffset((o) => o + 1);
|
|
27
|
+
});
|
|
28
|
+
if (loading) {
|
|
29
|
+
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: `Fetching messages for "${sessionName}"...` }) }));
|
|
30
|
+
}
|
|
31
|
+
if (error) {
|
|
32
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { bold: true, children: ["Messages \u2014 ", _jsx(Text, { color: "cyan", children: sessionName })] }), _jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsx(Text, { dimColor: true, children: "Esc to go back" })] }));
|
|
33
|
+
}
|
|
34
|
+
const messageLines = [];
|
|
35
|
+
for (const msg of messages) {
|
|
36
|
+
for (const part of msg.parts) {
|
|
37
|
+
const lines = part.text.split("\n");
|
|
38
|
+
for (let i = 0; i < lines.length; i++) {
|
|
39
|
+
messageLines.push({ role: msg.role, text: i === 0 ? lines[i] : ` ${lines[i]}` });
|
|
40
|
+
}
|
|
41
|
+
messageLines.push({ role: msg.role, text: "" });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const viewHeight = contentHeight - 3;
|
|
45
|
+
const maxOffset = Math.max(0, messageLines.length - viewHeight);
|
|
46
|
+
const clampedOffset = Math.min(scrollOffset, maxOffset);
|
|
47
|
+
const visible = messageLines.slice(clampedOffset, clampedOffset + viewHeight);
|
|
48
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Messages" }), _jsx(Text, { color: "cyan", children: sessionName }), _jsxs(Text, { dimColor: true, children: ["(", messages.length, " messages)"] })] }), _jsx(Box, { flexDirection: "column", height: viewHeight, overflow: "hidden", children: visible.length === 0 ? (_jsx(Text, { dimColor: true, children: "No messages in this session yet." })) : (visible.map((line, i) => (_jsx(Box, { children: line.role === "user" ? (_jsx(Text, { color: "green", children: line.text })) : (_jsx(Text, { children: line.text })) }, i)))) }), messageLines.length > viewHeight && (_jsxs(Text, { dimColor: true, children: ["[", clampedOffset + 1, "\u2013", Math.min(clampedOffset + viewHeight, messageLines.length), "/", messageLines.length, " lines] \u2191\u2193 scroll \u00B7 Esc back"] }))] }));
|
|
49
|
+
}
|
|
50
|
+
function InjectView({ serverUrl, password, sessionName, contentHeight, onClose }) {
|
|
51
|
+
const { injectText } = useApi(serverUrl, password);
|
|
52
|
+
const [stage, setStage] = useState("input");
|
|
53
|
+
const [error, setError] = useState(null);
|
|
54
|
+
const [submitted, setSubmitted] = useState(false);
|
|
55
|
+
useInput((_input, key) => {
|
|
56
|
+
if (key.escape && stage !== "submitting") {
|
|
57
|
+
onClose();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if ((stage === "done" || stage === "error") && key.escape) {
|
|
61
|
+
onClose();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
// Auto-close after success
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (stage === "done") {
|
|
68
|
+
const t = setTimeout(onClose, 1500);
|
|
69
|
+
return () => clearTimeout(t);
|
|
70
|
+
}
|
|
71
|
+
}, [stage, onClose]);
|
|
72
|
+
const handleSubmit = async (text) => {
|
|
73
|
+
const trimmed = text.trim();
|
|
74
|
+
if (!trimmed)
|
|
75
|
+
return;
|
|
76
|
+
setSubmitted(true);
|
|
77
|
+
setStage("submitting");
|
|
78
|
+
try {
|
|
79
|
+
await injectText(sessionName, trimmed);
|
|
80
|
+
setStage("done");
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
84
|
+
setStage("error");
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Inject Text" }), _jsx(Text, { color: "cyan", children: sessionName })] }), stage === "input" && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Text: " }), !submitted && (_jsx(TextInput, { placeholder: "Type your message...", onSubmit: (v) => void handleSubmit(v) }))] })), stage === "submitting" && _jsx(Spinner, { label: `Injecting into "${sessionName}"...` }), stage === "done" && (_jsx(Text, { color: "green", children: "Injected. Returning..." })), stage === "error" && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsx(Text, { dimColor: true, children: "Esc to go back" })] }))] }));
|
|
88
|
+
}
|
|
89
|
+
function CoworkerMailView({ serverUrl, password, sessionName, contentHeight, onClose }) {
|
|
90
|
+
const { getMailMessages } = useApi(serverUrl, password);
|
|
91
|
+
const [tab, setTab] = useState("received");
|
|
92
|
+
const [messages, setMessages] = useState([]);
|
|
93
|
+
const [loading, setLoading] = useState(true);
|
|
94
|
+
const [error, setError] = useState(null);
|
|
95
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
96
|
+
const loadTab = useCallback(async (t) => {
|
|
97
|
+
setLoading(true);
|
|
98
|
+
setScrollOffset(0);
|
|
99
|
+
try {
|
|
100
|
+
const msgs = await getMailMessages(sessionName, { sent: t === "sent" });
|
|
101
|
+
setMessages(msgs);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
setLoading(false);
|
|
108
|
+
}
|
|
109
|
+
}, [sessionName]);
|
|
110
|
+
useEffect(() => { void loadTab("received"); }, [loadTab]);
|
|
111
|
+
useInput((input, key) => {
|
|
112
|
+
if (key.escape) {
|
|
113
|
+
onClose();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!loading) {
|
|
117
|
+
if (key.upArrow)
|
|
118
|
+
setScrollOffset((o) => Math.max(0, o - 1));
|
|
119
|
+
if (key.downArrow)
|
|
120
|
+
setScrollOffset((o) => o + 1);
|
|
121
|
+
if (input === "r" && tab !== "received") {
|
|
122
|
+
setTab("received");
|
|
123
|
+
void loadTab("received");
|
|
124
|
+
}
|
|
125
|
+
if (input === "s" && tab !== "sent") {
|
|
126
|
+
setTab("sent");
|
|
127
|
+
void loadTab("sent");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
const renderMessages = () => {
|
|
132
|
+
if (loading)
|
|
133
|
+
return _jsx(Spinner, { label: "Loading..." });
|
|
134
|
+
if (error)
|
|
135
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
136
|
+
if (messages.length === 0)
|
|
137
|
+
return _jsxs(Text, { dimColor: true, children: ["No ", tab, " messages."] });
|
|
138
|
+
const lines = [];
|
|
139
|
+
const maxNameLen = Math.max(...messages.map((m) => m.from_name.length));
|
|
140
|
+
for (const msg of messages) {
|
|
141
|
+
const timestamp = new Date(msg.created_at).toLocaleString();
|
|
142
|
+
lines.push({ text: "─", color: "gray" });
|
|
143
|
+
lines.push({ text: `${msg.from_name.padEnd(maxNameLen)} → ${msg.to_name}`, color: "cyan" });
|
|
144
|
+
lines.push({ text: timestamp, color: "gray" });
|
|
145
|
+
if (!msg.read)
|
|
146
|
+
lines.push({ text: " [unread]", color: "yellow" });
|
|
147
|
+
lines.push({ text: "" });
|
|
148
|
+
for (const line of msg.body.split("\n")) {
|
|
149
|
+
lines.push({ text: ` ${line}` });
|
|
150
|
+
}
|
|
151
|
+
lines.push({ text: "" });
|
|
152
|
+
}
|
|
153
|
+
const viewHeight = contentHeight - 7;
|
|
154
|
+
const maxOffset = Math.max(0, lines.length - viewHeight);
|
|
155
|
+
const clamped = Math.min(scrollOffset, maxOffset);
|
|
156
|
+
const visible = lines.slice(clamped, clamped + viewHeight);
|
|
157
|
+
return (_jsx(Box, { flexDirection: "column", height: viewHeight, overflow: "hidden", children: visible.map((line, i) => (_jsx(Text, { color: line.color, children: line.text }, i))) }));
|
|
158
|
+
};
|
|
159
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Coworker Mail" }), _jsx(Text, { color: "cyan", children: sessionName }), _jsxs(Text, { dimColor: true, children: ["(", messages.length, " ", tab, ")"] })] }), _jsxs(Box, { gap: 2, marginBottom: 1, children: [tab === "received"
|
|
160
|
+
? _jsx(Text, { bold: true, color: "green", children: "[Received]" })
|
|
161
|
+
: _jsx(Text, { dimColor: true, children: "Received (r)" }), tab === "sent"
|
|
162
|
+
? _jsx(Text, { bold: true, color: "yellow", children: "[Sent]" })
|
|
163
|
+
: _jsx(Text, { dimColor: true, children: "Sent (s)" })] }), renderMessages()] }));
|
|
164
|
+
}
|
|
165
|
+
// ─── Main component ──────────────────────────────────────────────────────────
|
|
7
166
|
export function SessionList({ serverUrl, password, contentHeight }) {
|
|
8
|
-
const { listSessions } = useApi(serverUrl, password);
|
|
9
|
-
const { data: sessions, loading, error, run } = useAsyncState();
|
|
167
|
+
const { listSessions, createSession, deleteSession, regenerateCode, getModes } = useApi(serverUrl, password);
|
|
168
|
+
const { data: sessions, loading, error: loadError, run } = useAsyncState();
|
|
10
169
|
const [cursor, setCursor] = useState(0);
|
|
11
170
|
const [revealedRows, setRevealedRows] = useState(new Set());
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
171
|
+
const [mode, setMode] = useState("browse");
|
|
172
|
+
const [subView, setSubView] = useState(null);
|
|
173
|
+
const [actionError, setActionError] = useState(null);
|
|
174
|
+
const [actionMsg, setActionMsg] = useState(null);
|
|
175
|
+
const [availableModes, setAvailableModes] = useState([]);
|
|
176
|
+
const [pendingMode, setPendingMode] = useState(null);
|
|
177
|
+
const [modeCursor, setModeCursor] = useState(0);
|
|
178
|
+
const reload = () => void run(listSessions);
|
|
179
|
+
useEffect(() => { reload(); }, []);
|
|
15
180
|
const rows = sessions ?? [];
|
|
181
|
+
// Clamp cursor when list shrinks
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (rows.length > 0)
|
|
184
|
+
setCursor((c) => Math.min(c, rows.length - 1));
|
|
185
|
+
}, [rows.length]);
|
|
16
186
|
useInput((input, key) => {
|
|
17
|
-
|
|
187
|
+
// Sub-views own Esc themselves; pass nothing else through
|
|
188
|
+
if (subView !== null)
|
|
18
189
|
return;
|
|
19
|
-
if (
|
|
20
|
-
|
|
190
|
+
if (loading)
|
|
191
|
+
return;
|
|
192
|
+
if (mode === "browse") {
|
|
193
|
+
if (key.upArrow)
|
|
194
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
195
|
+
if (key.downArrow)
|
|
196
|
+
setCursor((c) => Math.min(rows.length - 1, c + 1));
|
|
197
|
+
if (input === "r" && rows.length > 0) {
|
|
198
|
+
const id = rows[cursor]?.id;
|
|
199
|
+
if (id == null)
|
|
200
|
+
return;
|
|
201
|
+
setRevealedRows((prev) => {
|
|
202
|
+
const next = new Set(prev);
|
|
203
|
+
next.has(id) ? next.delete(id) : next.add(id);
|
|
204
|
+
return next;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (input === "c") {
|
|
208
|
+
setActionError(null);
|
|
209
|
+
setActionMsg(null);
|
|
210
|
+
setPendingMode(null);
|
|
211
|
+
setModeCursor(0);
|
|
212
|
+
setMode("creating-loading");
|
|
213
|
+
getModes().then((modes) => {
|
|
214
|
+
const safeMode = Array.isArray(modes) ? modes : [];
|
|
215
|
+
setAvailableModes(safeMode);
|
|
216
|
+
setMode(safeMode.length > 0 ? "creating-pick-mode" : "creating-name");
|
|
217
|
+
}).catch(() => {
|
|
218
|
+
setAvailableModes([]);
|
|
219
|
+
setMode("creating-name");
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (input === "d" && rows.length > 0) {
|
|
223
|
+
setActionError(null);
|
|
224
|
+
setActionMsg(null);
|
|
225
|
+
setMode("confirm-delete");
|
|
226
|
+
}
|
|
227
|
+
if (input === "g" && rows.length > 0) {
|
|
228
|
+
setActionError(null);
|
|
229
|
+
setActionMsg(null);
|
|
230
|
+
setMode("confirm-regen");
|
|
231
|
+
}
|
|
232
|
+
if (rows.length > 0) {
|
|
233
|
+
if (input === "t")
|
|
234
|
+
setSubView("tail");
|
|
235
|
+
if (input === "i")
|
|
236
|
+
setSubView("inject");
|
|
237
|
+
if (input === "m")
|
|
238
|
+
setSubView("coworker-mail");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (mode === "creating-pick-mode") {
|
|
242
|
+
// +1 for the "no mode" option at index 0
|
|
243
|
+
const total = availableModes.length + 1;
|
|
244
|
+
if (key.upArrow)
|
|
245
|
+
setModeCursor((c) => (c - 1 + total) % total);
|
|
246
|
+
if (key.downArrow)
|
|
247
|
+
setModeCursor((c) => (c + 1) % total);
|
|
248
|
+
if (key.return) {
|
|
249
|
+
const selected = modeCursor === 0 ? null : (availableModes[modeCursor - 1]?.name ?? null);
|
|
250
|
+
setPendingMode(selected);
|
|
251
|
+
setMode("creating-name");
|
|
252
|
+
}
|
|
253
|
+
if (key.escape) {
|
|
254
|
+
setMode("browse");
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (mode === "creating-name" && key.escape) {
|
|
259
|
+
setMode("browse");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// Dismiss feedback states with any key
|
|
263
|
+
if (mode === "create-done" || mode === "create-error" || mode === "delete-done" || mode === "delete-error") {
|
|
264
|
+
setMode("browse");
|
|
265
|
+
setActionError(null);
|
|
266
|
+
setActionMsg(null);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
const handleCreate = async (name) => {
|
|
270
|
+
const trimmed = name.trim();
|
|
271
|
+
if (!trimmed)
|
|
272
|
+
return;
|
|
273
|
+
setMode("creating-busy");
|
|
274
|
+
try {
|
|
275
|
+
await createSession(trimmed, pendingMode ?? undefined);
|
|
276
|
+
const modeNote = pendingMode ? ` [${pendingMode}]` : "";
|
|
277
|
+
setActionMsg(`Coworker "${trimmed}"${modeNote} created.`);
|
|
278
|
+
setMode("create-done");
|
|
279
|
+
reload();
|
|
21
280
|
}
|
|
22
|
-
|
|
23
|
-
|
|
281
|
+
catch (err) {
|
|
282
|
+
setActionError(err instanceof Error ? err.message : String(err));
|
|
283
|
+
setMode("create-error");
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const handleConfirmDelete = async (confirmed) => {
|
|
287
|
+
if (!confirmed) {
|
|
288
|
+
setMode("browse");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const target = rows[cursor];
|
|
292
|
+
if (!target) {
|
|
293
|
+
setMode("browse");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
setMode("deleting");
|
|
297
|
+
try {
|
|
298
|
+
await deleteSession(target.name);
|
|
299
|
+
setActionMsg(`Coworker "${target.name}" deleted.`);
|
|
300
|
+
setMode("delete-done");
|
|
301
|
+
reload();
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
setActionError(err instanceof Error ? err.message : String(err));
|
|
305
|
+
setMode("delete-error");
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
const handleConfirmRegen = async (confirmed) => {
|
|
309
|
+
if (!confirmed) {
|
|
310
|
+
setMode("browse");
|
|
311
|
+
return;
|
|
24
312
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
313
|
+
const target = rows[cursor];
|
|
314
|
+
if (!target) {
|
|
315
|
+
setMode("browse");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
setMode("regenerating");
|
|
319
|
+
try {
|
|
320
|
+
await regenerateCode(target.name);
|
|
321
|
+
setActionMsg(`Agent code regenerated for "${target.name}".`);
|
|
29
322
|
setRevealedRows((prev) => {
|
|
30
323
|
const next = new Set(prev);
|
|
31
|
-
if (
|
|
32
|
-
next.
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
next.add(id);
|
|
36
|
-
}
|
|
324
|
+
if (target.id != null)
|
|
325
|
+
next.add(target.id);
|
|
37
326
|
return next;
|
|
38
327
|
});
|
|
328
|
+
setMode("create-done");
|
|
329
|
+
reload();
|
|
39
330
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
331
|
+
catch (err) {
|
|
332
|
+
setActionError(err instanceof Error ? err.message : String(err));
|
|
333
|
+
setMode("create-error");
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
// ── Full-screen create flow ───────────────────────────────────────────────
|
|
337
|
+
if (mode === "creating-loading") {
|
|
338
|
+
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Loading agent configs..." }) }));
|
|
43
339
|
}
|
|
44
|
-
if (
|
|
45
|
-
|
|
340
|
+
if (mode === "creating-pick-mode") {
|
|
341
|
+
const modeOptions = [
|
|
342
|
+
{ label: "No mode (default)", description: "" },
|
|
343
|
+
...availableModes.map((m) => ({ label: m.name, description: m.description })),
|
|
344
|
+
];
|
|
345
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Create Coworker" }), _jsx(Text, { dimColor: true, children: "Select a mode" })] }), _jsx(Box, { flexDirection: "column", children: modeOptions.map((opt, i) => {
|
|
346
|
+
const sel = i === modeCursor;
|
|
347
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: sel ? "cyan" : undefined, bold: sel, children: sel ? "▶" : " " }), _jsx(Text, { color: sel ? "cyan" : undefined, bold: sel, children: opt.label }), opt.description ? _jsxs(Text, { dimColor: true, children: ["\u2014 ", opt.description] }) : null] }, i));
|
|
348
|
+
}) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate \u00B7 Enter select \u00B7 Esc cancel" }) })] }));
|
|
349
|
+
}
|
|
350
|
+
if (mode === "creating-name") {
|
|
351
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Create Coworker" }), pendingMode
|
|
352
|
+
? _jsxs(Text, { color: "yellow", children: ["[", pendingMode, "]"] })
|
|
353
|
+
: _jsx(Text, { dimColor: true, children: "[no mode]" })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { placeholder: "e.g. alice", onSubmit: (v) => void handleCreate(v) })] }), _jsx(Text, { dimColor: true, children: "Enter to create \u00B7 Esc cancel" })] }));
|
|
354
|
+
}
|
|
355
|
+
if (mode === "creating-busy") {
|
|
356
|
+
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Creating coworker..." }) }));
|
|
357
|
+
}
|
|
358
|
+
if (mode === "create-done") {
|
|
359
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, children: [_jsx(Text, { color: "green", children: actionMsg }), _jsx(Text, { dimColor: true, children: "Press any key to continue" })] }));
|
|
360
|
+
}
|
|
361
|
+
if (mode === "create-error") {
|
|
362
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, paddingX: 2, children: [_jsxs(Text, { color: "red", children: ["Error: ", actionError] }), _jsx(Text, { dimColor: true, children: "Press any key to continue" })] }));
|
|
363
|
+
}
|
|
364
|
+
// ── Sub-view rendering ────────────────────────────────────────────────────
|
|
365
|
+
const activeSession = rows[cursor];
|
|
366
|
+
if (subView !== null && activeSession) {
|
|
367
|
+
const closeSubView = () => setSubView(null);
|
|
368
|
+
if (subView === "tail") {
|
|
369
|
+
return (_jsx(TailView, { serverUrl: serverUrl, password: password, sessionName: activeSession.name, contentHeight: contentHeight, onClose: closeSubView }));
|
|
370
|
+
}
|
|
371
|
+
if (subView === "inject") {
|
|
372
|
+
return (_jsx(InjectView, { serverUrl: serverUrl, password: password, sessionName: activeSession.name, contentHeight: contentHeight, onClose: closeSubView }));
|
|
373
|
+
}
|
|
374
|
+
if (subView === "coworker-mail") {
|
|
375
|
+
return (_jsx(CoworkerMailView, { serverUrl: serverUrl, password: password, sessionName: activeSession.name, contentHeight: contentHeight, onClose: closeSubView }));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// ── Initial load ──────────────────────────────────────────────────────────
|
|
379
|
+
if (loading && rows.length === 0) {
|
|
380
|
+
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Loading coworkers..." }) }));
|
|
381
|
+
}
|
|
382
|
+
if (loadError) {
|
|
383
|
+
return (_jsxs(Box, { height: contentHeight, flexDirection: "column", gap: 1, children: [_jsx(Text, { color: "red", bold: true, children: "Error" }), _jsx(Text, { children: loadError })] }));
|
|
46
384
|
}
|
|
47
|
-
|
|
385
|
+
// ── Inline action panel ───────────────────────────────────────────────────
|
|
386
|
+
const renderActionPanel = () => {
|
|
387
|
+
const target = rows[cursor];
|
|
388
|
+
if (mode === "confirm-delete" && target)
|
|
389
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Delete Coworker" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Delete ", _jsx(Text, { color: "yellow", bold: true, children: target.name }), "?", " ", _jsx(Text, { dimColor: true, children: "This also removes the OpenCode session." })] }) }), _jsx(Box, { marginTop: 1, children: _jsx(ConfirmInput, { defaultChoice: "cancel", onConfirm: () => void handleConfirmDelete(true), onCancel: () => void handleConfirmDelete(false) }) })] }));
|
|
390
|
+
if (mode === "deleting")
|
|
391
|
+
return (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1, marginBottom: 1, children: _jsx(Spinner, { label: `Deleting "${rows[cursor]?.name}"...` }) }));
|
|
392
|
+
if (mode === "delete-done")
|
|
393
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, marginBottom: 1, children: [_jsx(Text, { color: "green", children: actionMsg }), _jsx(Text, { dimColor: true, children: "Press any key to continue" })] }));
|
|
394
|
+
if (mode === "delete-error")
|
|
395
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1, marginBottom: 1, children: [_jsxs(Text, { color: "red", children: ["Error: ", actionError] }), _jsx(Text, { dimColor: true, children: "Press any key to continue" })] }));
|
|
396
|
+
if (mode === "confirm-regen" && target)
|
|
397
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: "Regenerate Agent Code" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Regenerate code for ", _jsx(Text, { color: "cyan", bold: true, children: target.name }), "?", " ", _jsx(Text, { dimColor: true, children: "The old code will stop working immediately." })] }) }), _jsx(Box, { marginTop: 1, children: _jsx(ConfirmInput, { defaultChoice: "cancel", onConfirm: () => void handleConfirmRegen(true), onCancel: () => void handleConfirmRegen(false) }) })] }));
|
|
398
|
+
if (mode === "regenerating")
|
|
399
|
+
return (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginBottom: 1, children: _jsx(Spinner, { label: "Generating new agent code..." }) }));
|
|
400
|
+
return null;
|
|
401
|
+
};
|
|
402
|
+
// ── Coworker table ────────────────────────────────────────────────────────
|
|
403
|
+
const actionPanel = renderActionPanel();
|
|
404
|
+
const panelHeight = actionPanel ? 5 : 0;
|
|
405
|
+
const tableHeight = contentHeight - panelHeight - 3;
|
|
406
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Coworkers" }), _jsxs(Text, { dimColor: true, children: ["(", rows.length, ")"] }), loading && _jsx(Spinner, {})] }), actionPanel, rows.length === 0 ? (_jsx(Box, { height: tableHeight, alignItems: "center", justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "No coworkers yet. Press c to create one." }) })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: " NAME".padEnd(18) }), _jsx(Text, { bold: true, color: "cyan", children: "MODE".padEnd(12) }), _jsx(Text, { bold: true, color: "cyan", children: "OPENCODE SESSION ID".padEnd(36) }), _jsx(Text, { bold: true, color: "cyan", children: "AGENT CODE" })] }), rows.map((s, i) => {
|
|
48
407
|
const selected = i === cursor;
|
|
49
408
|
const revealed = revealedRows.has(s.id);
|
|
50
|
-
return (_jsxs(Box, { gap: 2, children: [_jsxs(Box, { width: 18, children: [_jsx(Text, { color: selected ? "cyan" : undefined, children: selected ? "▶ " : " " }), _jsx(Text, { color: selected ? "cyan" : "green", bold: selected, children: s.name })] }), _jsx(Text, { dimColor: !selected, children: s.
|
|
51
|
-
|
|
409
|
+
return (_jsxs(Box, { gap: 2, children: [_jsxs(Box, { width: 18, children: [_jsx(Text, { color: selected ? "cyan" : undefined, children: selected ? "▶ " : " " }), _jsx(Text, { color: selected ? "cyan" : "green", bold: selected, children: s.name })] }), _jsx(Text, { color: selected ? "magenta" : undefined, dimColor: !selected && !s.mode, children: (s.mode ?? "—").padEnd(12) }), _jsx(Text, { dimColor: !selected, children: s.session_id.padEnd(36) }), revealed
|
|
410
|
+
? _jsx(Text, { color: "yellow", children: s.agent_code })
|
|
411
|
+
: _jsx(Text, { dimColor: true, children: MASKED_CODE })] }, s.id));
|
|
412
|
+
})] }))] }));
|
|
52
413
|
}
|
|
@@ -3,8 +3,14 @@ export interface Session {
|
|
|
3
3
|
name: string;
|
|
4
4
|
session_id: string;
|
|
5
5
|
agent_code: string;
|
|
6
|
+
mode: string | null;
|
|
6
7
|
created_at: string;
|
|
7
8
|
}
|
|
9
|
+
export interface AppMode {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
model: string;
|
|
13
|
+
}
|
|
8
14
|
export interface MessagePart {
|
|
9
15
|
type: "text";
|
|
10
16
|
text: string;
|
|
@@ -25,9 +31,28 @@ export interface MailMessage {
|
|
|
25
31
|
export interface Config {
|
|
26
32
|
[key: string]: string;
|
|
27
33
|
}
|
|
34
|
+
export interface CronJob {
|
|
35
|
+
id: number;
|
|
36
|
+
name: string;
|
|
37
|
+
session_name: string;
|
|
38
|
+
schedule: string;
|
|
39
|
+
timezone: string | null;
|
|
40
|
+
message: string;
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
created_at: string;
|
|
43
|
+
last_run: string | null;
|
|
44
|
+
next_run: string | null;
|
|
45
|
+
}
|
|
46
|
+
export interface CronHistoryEntry {
|
|
47
|
+
id: number;
|
|
48
|
+
cron_job_id: number;
|
|
49
|
+
executed_at: string;
|
|
50
|
+
success: boolean;
|
|
51
|
+
error_message: string | null;
|
|
52
|
+
}
|
|
28
53
|
export declare function useApi(serverUrl: string, password: string): {
|
|
29
54
|
listSessions: () => Promise<Session[]>;
|
|
30
|
-
createSession: (name: string) => Promise<Session>;
|
|
55
|
+
createSession: (name: string, mode?: string) => Promise<Session>;
|
|
31
56
|
deleteSession: (name: string) => Promise<void>;
|
|
32
57
|
checkHealth: () => Promise<boolean>;
|
|
33
58
|
getMessages: (name: string, limit?: number) => Promise<SessionMessage[]>;
|
|
@@ -55,6 +80,23 @@ export declare function useApi(serverUrl: string, password: string): {
|
|
|
55
80
|
}>;
|
|
56
81
|
}>;
|
|
57
82
|
markMessageRead: (id: number) => Promise<MailMessage>;
|
|
83
|
+
getModes: () => Promise<AppMode[]>;
|
|
84
|
+
listCrons: (sessionName?: string) => Promise<CronJob[]>;
|
|
85
|
+
createCron: (data: {
|
|
86
|
+
name: string;
|
|
87
|
+
session_name: string;
|
|
88
|
+
schedule: string;
|
|
89
|
+
message: string;
|
|
90
|
+
timezone?: string;
|
|
91
|
+
}) => Promise<CronJob>;
|
|
92
|
+
deleteCron: (id: number) => Promise<{
|
|
93
|
+
deleted: boolean;
|
|
94
|
+
id: number;
|
|
95
|
+
name: string;
|
|
96
|
+
}>;
|
|
97
|
+
enableCron: (id: number) => Promise<CronJob>;
|
|
98
|
+
disableCron: (id: number) => Promise<CronJob>;
|
|
99
|
+
getCronHistory: (id: number, limit?: number) => Promise<CronHistoryEntry[]>;
|
|
58
100
|
};
|
|
59
101
|
export declare function useAsyncState<T>(): {
|
|
60
102
|
run: (fn: () => Promise<T>) => Promise<T | null>;
|
|
@@ -28,12 +28,15 @@ export function useApi(serverUrl, password) {
|
|
|
28
28
|
const listSessions = useCallback(async () => {
|
|
29
29
|
return apiFetch(`${base}/sessions`, password);
|
|
30
30
|
}, [base, password]);
|
|
31
|
-
const createSession = useCallback(async (name) => {
|
|
31
|
+
const createSession = useCallback(async (name, mode) => {
|
|
32
32
|
return apiFetch(`${base}/sessions`, password, {
|
|
33
33
|
method: "POST",
|
|
34
|
-
body: JSON.stringify({ name }),
|
|
34
|
+
body: JSON.stringify({ name, ...(mode ? { mode } : {}) }),
|
|
35
35
|
});
|
|
36
36
|
}, [base, password]);
|
|
37
|
+
const getModes = useCallback(async () => {
|
|
38
|
+
return apiFetch(`${base}/modes`, password);
|
|
39
|
+
}, [base, password]);
|
|
37
40
|
const deleteSession = useCallback(async (name) => {
|
|
38
41
|
await apiFetch(`${base}/sessions/${encodeURIComponent(name)}`, password, {
|
|
39
42
|
method: "DELETE",
|
|
@@ -78,6 +81,28 @@ export function useApi(serverUrl, password) {
|
|
|
78
81
|
const markMessageRead = useCallback(async (id) => {
|
|
79
82
|
return apiFetch(`${base}/messages/${id}/read`, password, { method: "POST" });
|
|
80
83
|
}, [base, password]);
|
|
84
|
+
const listCrons = useCallback(async (sessionName) => {
|
|
85
|
+
const params = sessionName ? `?session_name=${encodeURIComponent(sessionName)}` : "";
|
|
86
|
+
return apiFetch(`${base}/crons${params}`, password);
|
|
87
|
+
}, [base, password]);
|
|
88
|
+
const createCron = useCallback(async (data) => {
|
|
89
|
+
return apiFetch(`${base}/crons`, password, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
body: JSON.stringify(data),
|
|
92
|
+
});
|
|
93
|
+
}, [base, password]);
|
|
94
|
+
const deleteCron = useCallback(async (id) => {
|
|
95
|
+
return apiFetch(`${base}/crons/${id}`, password, { method: "DELETE" });
|
|
96
|
+
}, [base, password]);
|
|
97
|
+
const enableCron = useCallback(async (id) => {
|
|
98
|
+
return apiFetch(`${base}/crons/${id}/enable`, password, { method: "POST" });
|
|
99
|
+
}, [base, password]);
|
|
100
|
+
const disableCron = useCallback(async (id) => {
|
|
101
|
+
return apiFetch(`${base}/crons/${id}/disable`, password, { method: "POST" });
|
|
102
|
+
}, [base, password]);
|
|
103
|
+
const getCronHistory = useCallback(async (id, limit = 10) => {
|
|
104
|
+
return apiFetch(`${base}/crons/${id}/history?limit=${limit}`, password);
|
|
105
|
+
}, [base, password]);
|
|
81
106
|
return {
|
|
82
107
|
listSessions,
|
|
83
108
|
createSession,
|
|
@@ -91,6 +116,13 @@ export function useApi(serverUrl, password) {
|
|
|
91
116
|
getMailMessages,
|
|
92
117
|
sendMailMessage,
|
|
93
118
|
markMessageRead,
|
|
119
|
+
getModes,
|
|
120
|
+
listCrons,
|
|
121
|
+
createCron,
|
|
122
|
+
deleteCron,
|
|
123
|
+
enableCron,
|
|
124
|
+
disableCron,
|
|
125
|
+
getCronHistory,
|
|
94
126
|
};
|
|
95
127
|
}
|
|
96
128
|
export function useAsyncState() {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Sql } from "../db/index.js";
|
|
2
|
+
import type { OpencodeClient } from "../lib/opencode.js";
|
|
3
|
+
import type { CronJobRow } from "../db/index.js";
|
|
4
|
+
interface CronSchedulerOptions {
|
|
5
|
+
onJobExecuted?: (jobId: number, success: boolean, error?: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare class CronScheduler {
|
|
8
|
+
private options;
|
|
9
|
+
private activeJobs;
|
|
10
|
+
private sql;
|
|
11
|
+
private opencode;
|
|
12
|
+
private started;
|
|
13
|
+
constructor(options?: CronSchedulerOptions);
|
|
14
|
+
start(sql: Sql, opencode: OpencodeClient): Promise<void>;
|
|
15
|
+
stop(): void;
|
|
16
|
+
private addJob;
|
|
17
|
+
private executeJob;
|
|
18
|
+
addCronJob(job: CronJobRow): void;
|
|
19
|
+
removeCronJob(jobId: number): void;
|
|
20
|
+
enableCronJob(job: CronJobRow): void;
|
|
21
|
+
disableCronJob(jobId: number): void;
|
|
22
|
+
isTracking(jobId: number): boolean;
|
|
23
|
+
}
|
|
24
|
+
export {};
|