agent-office 0.0.0 → 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.
Files changed (33) hide show
  1. package/dist/cli.js +102 -3
  2. package/dist/commands/serve.js +9 -2
  3. package/dist/commands/worker.d.ts +13 -0
  4. package/dist/commands/worker.js +120 -5
  5. package/dist/db/index.d.ts +32 -0
  6. package/dist/db/migrate.js +66 -0
  7. package/dist/manage/app.js +55 -35
  8. package/dist/manage/components/CronList.d.ts +9 -0
  9. package/dist/manage/components/CronList.js +310 -0
  10. package/dist/manage/components/ItemSelector.d.ts +7 -0
  11. package/dist/manage/components/ItemSelector.js +20 -0
  12. package/dist/manage/components/MenuSelect.d.ts +13 -0
  13. package/dist/manage/components/MenuSelect.js +22 -0
  14. package/dist/manage/components/MyMail.d.ts +9 -0
  15. package/dist/manage/components/MyMail.js +143 -0
  16. package/dist/manage/components/Profile.d.ts +8 -0
  17. package/dist/manage/components/Profile.js +60 -0
  18. package/dist/manage/components/ReadMail.d.ts +8 -0
  19. package/dist/manage/components/ReadMail.js +110 -0
  20. package/dist/manage/components/SendMessage.d.ts +9 -0
  21. package/dist/manage/components/SendMessage.js +79 -0
  22. package/dist/manage/components/SessionList.js +392 -31
  23. package/dist/manage/components/SessionSidebar.d.ts +6 -0
  24. package/dist/manage/components/SessionSidebar.js +18 -0
  25. package/dist/manage/hooks/useApi.d.ts +74 -1
  26. package/dist/manage/hooks/useApi.js +69 -3
  27. package/dist/server/cron.d.ts +24 -0
  28. package/dist/server/cron.js +121 -0
  29. package/dist/server/index.d.ts +2 -1
  30. package/dist/server/index.js +3 -3
  31. package/dist/server/routes.d.ts +3 -2
  32. package/dist/server/routes.js +976 -23
  33. package/package.json +3 -2
@@ -0,0 +1,310 @@
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
+ const NEXT_RUN_PADDING = 22;
7
+ export function CronList({ serverUrl, password, onBack, contentHeight, sessionName: initialSessionName }) {
8
+ const { listCrons, createCron, deleteCron, enableCron, disableCron, getCronHistory, listSessions } = useApi(serverUrl, password);
9
+ const { data: crons, loading, error: loadError, run } = useAsyncState();
10
+ const { data: sessions, run: runSessions } = useAsyncState();
11
+ const [cursor, setCursor] = useState(0);
12
+ const [mode, setMode] = useState("list");
13
+ const [actionMsg, setActionMsg] = useState(null);
14
+ const [actionError, setActionError] = useState(null);
15
+ const [selectedSession, setSelectedSession] = useState(initialSessionName ?? null);
16
+ const [createData, setCreateData] = useState({
17
+ name: "",
18
+ schedule: "",
19
+ message: "",
20
+ });
21
+ const [selectedSessionCursor, setSelectedSessionCursor] = useState(0);
22
+ const [history, setHistory] = useState([]);
23
+ const [historyLoading, setHistoryLoading] = useState(false);
24
+ const filteredCrons = crons?.filter((c) => (selectedSession ? c.session_name === selectedSession : true)) ?? [];
25
+ const reload = () => {
26
+ void run(listCrons);
27
+ void runSessions(() => listSessions());
28
+ };
29
+ useEffect(() => {
30
+ reload();
31
+ }, []);
32
+ useEffect(() => {
33
+ if (filteredCrons.length > 0) {
34
+ setCursor((c) => Math.min(c, filteredCrons.length - 1));
35
+ }
36
+ }, [filteredCrons.length]);
37
+ useInput((input, key) => {
38
+ if (key.escape && mode === "list") {
39
+ onBack();
40
+ return;
41
+ }
42
+ if (key.escape && (mode === "confirm-delete" || mode === "confirm-enable" || mode === "confirm-disable" || mode === "history")) {
43
+ setMode("list");
44
+ return;
45
+ }
46
+ if (key.escape && (mode === "create-name" || mode === "create-schedule" || mode === "create-message" || mode === "create-timezone")) {
47
+ setMode("list");
48
+ setCreateData({ name: "", schedule: "", message: "" });
49
+ return;
50
+ }
51
+ if (loading)
52
+ return;
53
+ if (mode === "list") {
54
+ if (key.upArrow)
55
+ setCursor((c) => Math.max(0, c - 1));
56
+ if (key.downArrow)
57
+ setCursor((c) => Math.min(filteredCrons.length - 1, c + 1));
58
+ if (input === "c") {
59
+ setActionError(null);
60
+ setActionMsg(null);
61
+ setCreateData({ name: "", schedule: "", message: "", timezone: "" });
62
+ setSelectedSessionCursor(0);
63
+ setMode("create-select-session");
64
+ }
65
+ if (input === "f" && filteredCrons.length > 0) {
66
+ setSelectedSession((prev) => (prev ? null : filteredCrons[0]?.session_name ?? null));
67
+ }
68
+ if (filteredCrons.length > 0) {
69
+ const selected = filteredCrons[cursor];
70
+ if (input === "d") {
71
+ setActionError(null);
72
+ setActionMsg(null);
73
+ setMode("confirm-delete");
74
+ }
75
+ if (input === "e") {
76
+ setActionError(null);
77
+ setActionMsg(null);
78
+ setMode(selected?.enabled ? "confirm-disable" : "confirm-enable");
79
+ }
80
+ if (input === "h") {
81
+ setActionError(null);
82
+ loadHistory(selected.id);
83
+ }
84
+ }
85
+ }
86
+ if (mode === "confirm-delete" || mode === "confirm-enable" || mode === "confirm-disable") {
87
+ if (key.return || input === "y") {
88
+ if (mode === "confirm-delete") {
89
+ handleDelete();
90
+ }
91
+ else if (mode === "confirm-enable") {
92
+ handleEnable();
93
+ }
94
+ else if (mode === "confirm-disable") {
95
+ handleDisable();
96
+ }
97
+ }
98
+ if (input === "n" || key.escape) {
99
+ setMode("list");
100
+ }
101
+ return;
102
+ }
103
+ if (mode === "create-select-session") {
104
+ if (key.upArrow)
105
+ setSelectedSessionCursor((c) => Math.max(0, c - 1));
106
+ if (key.downArrow)
107
+ setSelectedSessionCursor((c) => Math.min((sessions ?? []).length - 1, c + 1));
108
+ if (key.return) {
109
+ const selected = sessions?.[selectedSessionCursor];
110
+ if (selected) {
111
+ setSelectedSession(selected.name);
112
+ setMode("create-name");
113
+ }
114
+ }
115
+ if (key.escape) {
116
+ setCreateData({ name: "", schedule: "", message: "" });
117
+ setSelectedSession(null);
118
+ setMode("list");
119
+ }
120
+ }
121
+ });
122
+ const loadHistory = useCallback(async (cronId) => {
123
+ setHistoryLoading(true);
124
+ try {
125
+ const entries = await getCronHistory(cronId, 10);
126
+ setHistory(entries);
127
+ setMode("history");
128
+ }
129
+ catch (err) {
130
+ setActionError(err instanceof Error ? err.message : String(err));
131
+ }
132
+ finally {
133
+ setHistoryLoading(false);
134
+ }
135
+ }, [getCronHistory]);
136
+ const handleDelete = async () => {
137
+ const selected = filteredCrons[cursor];
138
+ if (!selected)
139
+ return;
140
+ setMode("deleting");
141
+ try {
142
+ await deleteCron(selected.id);
143
+ setActionMsg(`Cron job "${selected.name}" deleted.`);
144
+ setMode("list");
145
+ reload();
146
+ }
147
+ catch (err) {
148
+ setActionError(err instanceof Error ? err.message : String(err));
149
+ setMode("list");
150
+ }
151
+ };
152
+ const handleEnable = async () => {
153
+ const selected = filteredCrons[cursor];
154
+ if (!selected)
155
+ return;
156
+ setMode("toggling");
157
+ try {
158
+ await enableCron(selected.id);
159
+ setActionMsg(`Cron job "${selected.name}" enabled.`);
160
+ setMode("list");
161
+ reload();
162
+ }
163
+ catch (err) {
164
+ setActionError(err instanceof Error ? err.message : String(err));
165
+ setMode("list");
166
+ }
167
+ };
168
+ const handleDisable = async () => {
169
+ const selected = filteredCrons[cursor];
170
+ if (!selected)
171
+ return;
172
+ setMode("toggling");
173
+ try {
174
+ await disableCron(selected.id);
175
+ setActionMsg(`Cron job "${selected.name}" disabled.`);
176
+ setMode("list");
177
+ reload();
178
+ }
179
+ catch (err) {
180
+ setActionError(err instanceof Error ? err.message : String(err));
181
+ setMode("list");
182
+ }
183
+ };
184
+ const handleCreate = async () => {
185
+ if (!createData.name.trim() || !createData.schedule.trim() || !createData.message.trim())
186
+ return;
187
+ if (!selectedSession)
188
+ return;
189
+ setMode("creating");
190
+ try {
191
+ await createCron({
192
+ name: createData.name.trim(),
193
+ session_name: selectedSession,
194
+ schedule: createData.schedule.trim(),
195
+ message: createData.message.trim(),
196
+ timezone: createData.timezone,
197
+ });
198
+ setActionMsg(`Cron job "${createData.name.trim()}" created.`);
199
+ setMode("list");
200
+ setCreateData({ name: "", schedule: "", message: "", timezone: "" });
201
+ reload();
202
+ }
203
+ catch (err) {
204
+ setActionError(err instanceof Error ? err.message : String(err));
205
+ setMode("list");
206
+ setCreateData({ name: "", schedule: "", message: "", timezone: "" });
207
+ }
208
+ };
209
+ const formatNextRun = (nextRun) => {
210
+ if (!nextRun)
211
+ return "—";
212
+ try {
213
+ const date = new Date(nextRun);
214
+ return date.toLocaleString();
215
+ }
216
+ catch {
217
+ return nextRun;
218
+ }
219
+ };
220
+ const formatLastRun = (lastRun) => {
221
+ if (!lastRun)
222
+ return "never";
223
+ try {
224
+ const date = new Date(lastRun);
225
+ return date.toLocaleString();
226
+ }
227
+ catch {
228
+ return lastRun;
229
+ }
230
+ };
231
+ if (loading && filteredCrons.length === 0) {
232
+ return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Loading cron jobs..." }) }));
233
+ }
234
+ if (loadError) {
235
+ return (_jsxs(Box, { height: contentHeight, flexDirection: "column", gap: 1, children: [_jsx(Text, { color: "red", bold: true, children: "Error" }), _jsx(Text, { children: loadError })] }));
236
+ }
237
+ const renderActionPanel = () => {
238
+ if (mode === "deleting") {
239
+ return (_jsx(Box, { borderStyle: "round", borderColor: "red", paddingX: 1, marginBottom: 1, children: _jsx(Spinner, { label: `Deleting "${filteredCrons[cursor]?.name}"...` }) }));
240
+ }
241
+ if (mode === "toggling") {
242
+ return (_jsx(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginBottom: 1, children: _jsx(Spinner, { label: "Updating cron job..." }) }));
243
+ }
244
+ if (mode === "confirm-delete" && filteredCrons[cursor]) {
245
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Delete Cron Job" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Delete ", _jsx(Text, { color: "yellow", bold: true, children: filteredCrons[cursor].name }), "? This cannot be undone."] }) }), _jsx(Box, { marginTop: 1, children: _jsx(ConfirmInput, { defaultChoice: "cancel", onConfirm: () => void handleDelete(), onCancel: () => setMode("list") }) })] }));
246
+ }
247
+ if (mode === "confirm-enable" && filteredCrons[cursor]) {
248
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "green", children: "Enable Cron Job" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Enable ", _jsx(Text, { color: "cyan", bold: true, children: filteredCrons[cursor].name }), "?"] }) }), _jsx(Box, { marginTop: 1, children: _jsx(ConfirmInput, { defaultChoice: "cancel", onConfirm: () => void handleEnable(), onCancel: () => setMode("list") }) })] }));
249
+ }
250
+ if (mode === "confirm-disable" && filteredCrons[cursor]) {
251
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: "Disable Cron Job" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Disable ", _jsx(Text, { color: "cyan", bold: true, children: filteredCrons[cursor].name }), "?"] }) }), _jsx(Box, { marginTop: 1, children: _jsx(ConfirmInput, { defaultChoice: "cancel", onConfirm: () => void handleDisable(), onCancel: () => setMode("list") }) })] }));
252
+ }
253
+ return null;
254
+ };
255
+ const renderCreateSelectSession = () => {
256
+ if (mode !== "create-select-session")
257
+ return null;
258
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { gap: 2, children: _jsx(Text, { bold: true, children: "Select Coworker" }) }), (sessions ?? []).map((s, i) => {
259
+ const sel = i === selectedSessionCursor;
260
+ return (_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: sel ? "cyan" : undefined, bold: sel, children: sel ? "▶ " : " " }), _jsx(Text, { color: sel ? "cyan" : undefined, bold: sel, children: s.name })] }, s.id));
261
+ }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate \u00B7 Enter select \u00B7 Esc cancel" }) })] }));
262
+ };
263
+ const renderCreateFields = () => {
264
+ if (mode === "create-name") {
265
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Create Cron Job" }), _jsx(Text, { dimColor: true, children: selectedSession })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { placeholder: "e.g. daily-standup", onSubmit: (v) => {
266
+ setCreateData({ ...createData, name: v });
267
+ setMode("create-schedule");
268
+ } }, "create-name")] })] }));
269
+ }
270
+ if (mode === "create-schedule") {
271
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Create Cron Job" }), _jsx(Text, { dimColor: true, children: createData.name ?? "" })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Schedule (cron expr): " }), _jsx(TextInput, { placeholder: "e.g. 0 9 * * *", onSubmit: (v) => {
272
+ setCreateData({ ...createData, schedule: v });
273
+ setMode("create-message");
274
+ } }, "create-schedule")] }), _jsx(Text, { dimColor: true, children: "Example: \"0 9 * * *\" = daily at 9am, \"*/30 * * * *\" = every 30 minutes" })] }));
275
+ }
276
+ if (mode === "create-message") {
277
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Create Cron Job" }), _jsx(Text, { dimColor: true, children: createData.schedule ?? "" })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Message: " }), _jsx(TextInput, { placeholder: "Message to inject", onSubmit: (v) => {
278
+ setCreateData({ ...createData, message: v });
279
+ setMode("create-timezone");
280
+ } }, "create-message")] })] }));
281
+ }
282
+ if (mode === "create-timezone") {
283
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Create Cron Job" }), _jsxs(Text, { dimColor: true, children: [createData.message.slice(0, 40), "..."] })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Timezone (optional, ENTER to skip): " }), _jsx(TextInput, { placeholder: "e.g. America/New_York", onSubmit: (v) => {
284
+ setCreateData({ ...createData, timezone: v || "" });
285
+ handleCreate();
286
+ } }, "create-timezone")] }), _jsx(Text, { dimColor: true, children: "Leave empty for system timezone" })] }));
287
+ }
288
+ if (mode === "creating") {
289
+ return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Creating cron job..." }) }));
290
+ }
291
+ return null;
292
+ };
293
+ const renderHistory = () => {
294
+ if (mode !== "history")
295
+ return null;
296
+ if (historyLoading) {
297
+ return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Loading history..." }) }));
298
+ }
299
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "History" }), _jsx(Text, { dimColor: true, children: "Press any key to go back" })] }), history.length === 0 ? (_jsx(Text, { dimColor: true, children: "No execution history yet." })) : (_jsx(Box, { flexDirection: "column", children: history.map((entry) => (_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: entry.success ? "green" : "red", children: entry.success ? "✓" : "✗" }), _jsx(Text, { children: new Date(entry.executed_at).toLocaleString() }), !entry.success && entry.error_message && (_jsx(Text, { color: "red", children: entry.error_message }))] }, entry.id))) }))] }));
300
+ };
301
+ const actionPanel = renderActionPanel();
302
+ const panelHeight = actionPanel ? 5 : 0;
303
+ const tableHeight = contentHeight - panelHeight - 5;
304
+ return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [renderCreateSelectSession(), renderCreateFields(), renderHistory(), (mode === "list" || mode === "confirm-delete" || mode === "confirm-enable" || mode === "confirm-disable" || mode === "deleting" || mode === "toggling") && (_jsxs(_Fragment, { children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Cron Jobs" }), selectedSession ? (_jsxs(Text, { color: "cyan", dimColor: true, children: ["[", filteredCrons.length, " jobs for ", selectedSession, "]"] })) : (_jsxs(Text, { dimColor: true, children: ["[", filteredCrons.length, " total]"] })), loading && _jsx(Spinner, {})] }), actionPanel, filteredCrons.length === 0 ? (_jsx(Box, { height: tableHeight, alignItems: "center", justifyContent: "center", children: _jsx(Text, { dimColor: true, children: selectedSession
305
+ ? `No cron jobs for ${selectedSession}. Press c to create one.`
306
+ : "No cron jobs 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(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: "NEXT RUN".padEnd(NEXT_RUN_PADDING) }), _jsx(Text, { bold: true, color: "cyan", children: "STATUS" })] }), filteredCrons.map((job, idx) => {
307
+ const selected = idx === cursor;
308
+ 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: job.name })] }), _jsx(Box, { width: 15, children: _jsx(Text, { color: selected ? "magenta" : undefined, dimColor: !selected, children: job.session_name.padEnd(15) }) }), _jsx(Box, { width: 20, children: _jsx(Text, { dimColor: !selected, children: job.schedule.padEnd(20) }) }), _jsx(Box, { width: NEXT_RUN_PADDING, children: _jsx(Text, { color: job.enabled ? (selected ? "cyan" : "green") : "gray", dimColor: !selected, children: job.enabled ? formatNextRun(job.next_run).padEnd(NEXT_RUN_PADDING) : "DISABLED".padEnd(NEXT_RUN_PADDING) }) }), _jsx(Text, { color: job.enabled ? "green" : "gray", dimColor: !selected, children: job.enabled ? "enabled" : "disabled" })] }, job.id));
309
+ })] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["c create \u00B7 d delete \u00B7 e enable/disable \u00B7 h history \u00B7 f", " ", selectedSession ? "show all" : "filter by coworker", " \u00B7 Esc back"] }) })] }))] }));
310
+ }
@@ -0,0 +1,7 @@
1
+ interface ItemSelectorProps {
2
+ items: string[];
3
+ onSelect: (value: string) => void;
4
+ onCancel?: () => void;
5
+ }
6
+ export declare function ItemSelector({ items, onSelect, onCancel }: ItemSelectorProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ export function ItemSelector({ items, onSelect, onCancel }) {
5
+ const [cursor, setCursor] = useState(0);
6
+ useInput((_input, key) => {
7
+ if (key.upArrow)
8
+ setCursor((c) => Math.max(0, c - 1));
9
+ if (key.downArrow)
10
+ setCursor((c) => Math.min(items.length - 1, c + 1));
11
+ if (key.return && items[cursor] != null)
12
+ onSelect(items[cursor]);
13
+ if (key.escape)
14
+ onCancel?.();
15
+ });
16
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [items.map((item, i) => {
17
+ const sel = i === cursor;
18
+ 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: item })] }, item));
19
+ }), _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate \u00B7 Enter select \u00B7 Esc cancel" })] }));
20
+ }
@@ -0,0 +1,13 @@
1
+ export interface MenuOption {
2
+ label: string;
3
+ value: string;
4
+ hint?: string;
5
+ }
6
+ interface MenuSelectProps {
7
+ options: MenuOption[];
8
+ onChange: (value: string) => void;
9
+ /** Optional badge text shown next to a specific option value */
10
+ badges?: Record<string, string>;
11
+ }
12
+ export declare function MenuSelect({ options, onChange, badges }: MenuSelectProps): import("react/jsx-runtime").JSX.Element;
13
+ export {};
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ export function MenuSelect({ options, onChange, badges = {} }) {
5
+ const [selectedIndex, setSelectedIndex] = useState(0);
6
+ useInput((input, key) => {
7
+ if (key.upArrow) {
8
+ setSelectedIndex((i) => (i - 1 + options.length) % options.length);
9
+ }
10
+ if (key.downArrow) {
11
+ setSelectedIndex((i) => (i + 1) % options.length);
12
+ }
13
+ if (key.return) {
14
+ onChange(options[selectedIndex].value);
15
+ }
16
+ });
17
+ return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: options.map((opt, i) => {
18
+ const isSelected = i === selectedIndex;
19
+ const badge = badges[opt.value];
20
+ return (_jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { color: isSelected ? "cyan" : undefined, bold: isSelected, children: isSelected ? "▶" : " " }), _jsx(Text, { color: isSelected ? "cyan" : undefined, bold: isSelected, children: opt.label }), badge ? (_jsx(Text, { color: "yellow", bold: true, children: badge })) : null] }, opt.value));
21
+ }) }));
22
+ }
@@ -0,0 +1,9 @@
1
+ interface MyMailProps {
2
+ serverUrl: string;
3
+ password: string;
4
+ onBack: () => void;
5
+ contentHeight: number;
6
+ onReply: (toName: string) => void;
7
+ }
8
+ export declare function MyMail({ serverUrl, password, onBack, contentHeight, onReply }: MyMailProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
@@ -0,0 +1,143 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState, useRef } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { Spinner } from "@inkjs/ui";
5
+ import { useApi } from "../hooks/useApi.js";
6
+ export function MyMail({ serverUrl, password, onBack, contentHeight, onReply }) {
7
+ const { getConfig, getMailMessages, markMessageRead } = useApi(serverUrl, password);
8
+ const [humanName, setHumanName] = useState("Human");
9
+ const [viewTab, setViewTab] = useState("received");
10
+ const [messages, setMessages] = useState([]);
11
+ const [error, setError] = useState(null);
12
+ const [selectedMsgIndex, setSelectedMsgIndex] = useState(0);
13
+ const [marking, setMarking] = useState(false);
14
+ const humanNameRef = useRef("Human");
15
+ useEffect(() => {
16
+ getConfig().then((config) => {
17
+ const name = config.human_name ?? "Human";
18
+ setHumanName(name);
19
+ humanNameRef.current = name;
20
+ loadMessages(name, "received");
21
+ }).catch((err) => {
22
+ setError(err instanceof Error ? err.message : String(err));
23
+ });
24
+ }, []);
25
+ const loadMessages = async (name, tab) => {
26
+ try {
27
+ const msgs = await getMailMessages(name, { sent: tab === "sent" });
28
+ setMessages(msgs);
29
+ setSelectedMsgIndex(0);
30
+ }
31
+ catch (err) {
32
+ setError(err instanceof Error ? err.message : String(err));
33
+ }
34
+ };
35
+ const handleTabSwitch = (newTab) => {
36
+ setViewTab(newTab);
37
+ loadMessages(humanNameRef.current, newTab);
38
+ };
39
+ const handleMarkRead = async () => {
40
+ const msg = messages[selectedMsgIndex];
41
+ if (!msg || msg.read || viewTab !== "received")
42
+ return;
43
+ setMarking(true);
44
+ try {
45
+ await markMessageRead(msg.id);
46
+ setMessages((prev) => prev.map((m) => (m.id === msg.id ? { ...m, read: true } : m)));
47
+ }
48
+ catch (err) {
49
+ setError(err instanceof Error ? err.message : String(err));
50
+ }
51
+ finally {
52
+ setMarking(false);
53
+ }
54
+ };
55
+ const handleMarkAllRead = async () => {
56
+ if (viewTab !== "received")
57
+ return;
58
+ const unread = messages.filter((m) => !m.read);
59
+ if (unread.length === 0)
60
+ return;
61
+ setMarking(true);
62
+ try {
63
+ await Promise.all(unread.map((m) => markMessageRead(m.id)));
64
+ setMessages((prev) => prev.map((m) => ({ ...m, read: true })));
65
+ }
66
+ catch (err) {
67
+ setError(err instanceof Error ? err.message : String(err));
68
+ }
69
+ finally {
70
+ setMarking(false);
71
+ }
72
+ };
73
+ useInput((input, key) => {
74
+ if (marking)
75
+ return;
76
+ if (key.upArrow) {
77
+ setSelectedMsgIndex((i) => Math.max(0, i - 1));
78
+ }
79
+ if (key.downArrow) {
80
+ setSelectedMsgIndex((i) => Math.min(messages.length - 1, i + 1));
81
+ }
82
+ if (input === "r") {
83
+ if (viewTab === "received") {
84
+ const msg = messages[selectedMsgIndex];
85
+ if (msg)
86
+ onReply(msg.from_name);
87
+ }
88
+ else {
89
+ handleTabSwitch("received");
90
+ }
91
+ }
92
+ if (input === "m" && viewTab === "received") {
93
+ handleMarkRead();
94
+ }
95
+ if (input === "s" && viewTab !== "sent") {
96
+ handleTabSwitch("sent");
97
+ }
98
+ if (input === "a" && viewTab === "received") {
99
+ handleMarkAllRead();
100
+ }
101
+ });
102
+ const renderMessages = () => {
103
+ const viewHeight = contentHeight - 8;
104
+ if (messages.length === 0) {
105
+ return (_jsx(Box, { flexDirection: "column", height: viewHeight, children: _jsx(Text, { dimColor: true, children: "No messages." }) }));
106
+ }
107
+ const allLines = [];
108
+ const msgStartLine = [];
109
+ for (let mi = 0; mi < messages.length; mi++) {
110
+ const msg = messages[mi];
111
+ const isSelected = mi === selectedMsgIndex;
112
+ const timestamp = new Date(msg.created_at).toLocaleString();
113
+ const accentColor = isSelected ? "cyan" : "gray";
114
+ msgStartLine.push(allLines.length);
115
+ allLines.push({ text: isSelected ? "▶ ──────────────────────────" : " ──────────────────────────", color: accentColor, bold: isSelected, msgIndex: mi });
116
+ allLines.push({
117
+ text: ` ${msg.from_name} → ${msg.to_name}`,
118
+ color: isSelected ? "cyan" : undefined,
119
+ bold: isSelected,
120
+ msgIndex: mi,
121
+ });
122
+ allLines.push({ text: ` ${timestamp}`, color: "gray", msgIndex: mi });
123
+ if (!msg.read) {
124
+ allLines.push({ text: " [unread]", color: "yellow", bold: true, msgIndex: mi });
125
+ }
126
+ allLines.push({ text: "", msgIndex: mi });
127
+ for (const line of msg.body.split("\n")) {
128
+ allLines.push({ text: ` ${line}`, msgIndex: mi });
129
+ }
130
+ allLines.push({ text: "", msgIndex: mi });
131
+ }
132
+ // Auto-scroll: keep selected message's first line in view
133
+ const selStart = msgStartLine[selectedMsgIndex] ?? 0;
134
+ const maxOffset = Math.max(0, allLines.length - viewHeight);
135
+ // Prefer showing the selected message near the top, but clamp to bounds
136
+ const rawOffset = Math.max(0, selStart - 1);
137
+ const scrollOffset = Math.min(rawOffset, maxOffset);
138
+ const visible = allLines.slice(scrollOffset, scrollOffset + viewHeight);
139
+ return (_jsx(Box, { flexDirection: "column", height: viewHeight, overflow: "hidden", children: visible.map((line, i) => (_jsx(Text, { color: line.color, bold: line.bold, children: line.text }, i))) }));
140
+ };
141
+ const unreadCount = messages.filter((m) => !m.read).length;
142
+ return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "My Mail" }), _jsx(Text, { color: "cyan", children: humanName }), _jsxs(Text, { dimColor: true, children: ["(", messages.length, " ", viewTab] }), viewTab === "received" && unreadCount > 0 && (_jsxs(Text, { color: "yellow", bold: true, children: ["\u00B7 ", unreadCount, " unread"] })), _jsx(Text, { dimColor: true, children: ")" }), marking && _jsx(Spinner, { label: "saving..." })] }), _jsxs(Box, { gap: 2, marginBottom: 1, children: [viewTab === "received" ? (_jsx(Text, { bold: true, color: "green", children: "[Received]" })) : (_jsx(Text, { dimColor: true, children: "Received (press r)" })), viewTab === "sent" ? (_jsx(Text, { bold: true, color: "yellow", children: "[Sent]" })) : (_jsx(Text, { dimColor: true, children: "Sent (s)" }))] }), error ? (_jsx(Text, { color: "red", children: error })) : (renderMessages())] }));
143
+ }
@@ -0,0 +1,8 @@
1
+ interface ProfileProps {
2
+ serverUrl: string;
3
+ password: string;
4
+ onBack: () => void;
5
+ contentHeight: number;
6
+ }
7
+ export declare function Profile({ serverUrl, password, onBack, contentHeight }: ProfileProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,60 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Box, Text } from "ink";
4
+ import { Spinner, TextInput } from "@inkjs/ui";
5
+ import { useApi } from "../hooks/useApi.js";
6
+ export function Profile({ serverUrl, password, onBack, contentHeight }) {
7
+ const { getConfig, setConfig } = useApi(serverUrl, password);
8
+ const [name, setName] = useState("");
9
+ const [description, setDescription] = useState("");
10
+ const [stage, setStage] = useState("loading");
11
+ const [error, setError] = useState(null);
12
+ useEffect(() => {
13
+ getConfig()
14
+ .then((config) => {
15
+ setName(config.human_name ?? "Human");
16
+ setDescription(config.human_description ?? "");
17
+ setStage("editing-name");
18
+ })
19
+ .catch((err) => {
20
+ setError(err instanceof Error ? err.message : String(err));
21
+ setStage("error");
22
+ });
23
+ }, []);
24
+ useEffect(() => {
25
+ if (stage === "done" || stage === "error") {
26
+ const timer = setTimeout(onBack, 1500);
27
+ return () => clearTimeout(timer);
28
+ }
29
+ }, [stage, onBack]);
30
+ const handleNameSubmit = (newName) => {
31
+ setName(newName);
32
+ setStage("editing-description");
33
+ };
34
+ const handleDescriptionSubmit = async (newDesc) => {
35
+ setDescription(newDesc);
36
+ setStage("saving");
37
+ try {
38
+ await setConfig("human_name", name);
39
+ await setConfig("human_description", newDesc);
40
+ setStage("done");
41
+ }
42
+ catch (err) {
43
+ setError(err instanceof Error ? err.message : String(err));
44
+ setStage("error");
45
+ }
46
+ };
47
+ if (stage === "loading") {
48
+ return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Loading profile..." }) }));
49
+ }
50
+ if (stage === "error") {
51
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "My Profile" }), _jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsx(Text, { dimColor: true, children: "Returning to menu..." })] }));
52
+ }
53
+ if (stage === "saving") {
54
+ return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Saving profile..." }) }));
55
+ }
56
+ if (stage === "done") {
57
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "My Profile" }), _jsx(Text, { color: "green", children: "Profile saved successfully!" }), _jsx(Text, { dimColor: true, children: "Returning to menu..." })] }));
58
+ }
59
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "My Profile" }), _jsx(Text, { dimColor: true, children: "Configure your name and description (visible to agents)" }), _jsxs(Box, { flexDirection: "column", gap: 1, marginTop: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, children: "Name:" }), stage === "editing-name" ? (_jsx(TextInput, { placeholder: name || "Your name", onSubmit: handleNameSubmit })) : (_jsx(Text, { color: "cyan", children: name }))] }), stage === "editing-description" && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, children: "Description:" }), _jsx(TextInput, { placeholder: description || "Optional description", onSubmit: handleDescriptionSubmit })] }))] }), stage === "editing-name" && (_jsx(Text, { dimColor: true, children: "Enter to continue \u00B7 Esc to cancel" })), stage === "editing-description" && (_jsx(Text, { dimColor: true, children: "Enter to save \u00B7 Esc to cancel" }))] }));
60
+ }
@@ -0,0 +1,8 @@
1
+ interface ReadMailProps {
2
+ serverUrl: string;
3
+ password: string;
4
+ onBack: () => void;
5
+ contentHeight: number;
6
+ }
7
+ export declare function ReadMail({ serverUrl, password, onBack, contentHeight }: ReadMailProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};