agent-office 0.0.1 → 0.0.3

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.
@@ -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
+ }
@@ -3,6 +3,7 @@ interface MyMailProps {
3
3
  password: string;
4
4
  onBack: () => void;
5
5
  contentHeight: number;
6
+ onReply: (toName: string) => void;
6
7
  }
7
- export declare function MyMail({ serverUrl, password, onBack, contentHeight }: MyMailProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function MyMail({ serverUrl, password, onBack, contentHeight, onReply }: MyMailProps): import("react/jsx-runtime").JSX.Element;
8
9
  export {};
@@ -1,28 +1,32 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from "react";
2
+ import { useEffect, useState, useRef } from "react";
3
3
  import { Box, Text, useInput } from "ink";
4
+ import { Spinner } from "@inkjs/ui";
4
5
  import { useApi } from "../hooks/useApi.js";
5
- export function MyMail({ serverUrl, password, onBack, contentHeight }) {
6
- const { getConfig, getMailMessages } = useApi(serverUrl, password);
6
+ export function MyMail({ serverUrl, password, onBack, contentHeight, onReply }) {
7
+ const { getConfig, getMailMessages, markMessageRead } = useApi(serverUrl, password);
7
8
  const [humanName, setHumanName] = useState("Human");
8
9
  const [viewTab, setViewTab] = useState("received");
9
10
  const [messages, setMessages] = useState([]);
10
11
  const [error, setError] = useState(null);
11
- const [scrollOffset, setScrollOffset] = useState(0);
12
+ const [selectedMsgIndex, setSelectedMsgIndex] = useState(0);
13
+ const [marking, setMarking] = useState(false);
14
+ const humanNameRef = useRef("Human");
12
15
  useEffect(() => {
13
16
  getConfig().then((config) => {
14
- setHumanName(config.human_name ?? "Human");
15
- loadMessages(config.human_name ?? "Human");
17
+ const name = config.human_name ?? "Human";
18
+ setHumanName(name);
19
+ humanNameRef.current = name;
20
+ loadMessages(name, "received");
16
21
  }).catch((err) => {
17
22
  setError(err instanceof Error ? err.message : String(err));
18
23
  });
19
24
  }, []);
20
- const loadMessages = async (name) => {
21
- const sent = viewTab === "sent";
25
+ const loadMessages = async (name, tab) => {
22
26
  try {
23
- const msgs = await getMailMessages(name, { sent });
27
+ const msgs = await getMailMessages(name, { sent: tab === "sent" });
24
28
  setMessages(msgs);
25
- setScrollOffset(0);
29
+ setSelectedMsgIndex(0);
26
30
  }
27
31
  catch (err) {
28
32
  setError(err instanceof Error ? err.message : String(err));
@@ -30,41 +34,110 @@ export function MyMail({ serverUrl, password, onBack, contentHeight }) {
30
34
  };
31
35
  const handleTabSwitch = (newTab) => {
32
36
  setViewTab(newTab);
33
- setScrollOffset(0);
34
- loadMessages(humanName);
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
+ }
35
72
  };
36
73
  useInput((input, key) => {
37
- if (key.upArrow)
38
- setScrollOffset((o) => Math.max(0, o - 1));
39
- if (key.downArrow)
40
- setScrollOffset((o) => o + 1);
41
- if (input === "r" && viewTab !== "received") {
42
- handleTabSwitch("received");
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();
43
94
  }
44
95
  if (input === "s" && viewTab !== "sent") {
45
96
  handleTabSwitch("sent");
46
97
  }
98
+ if (input === "a" && viewTab === "received") {
99
+ handleMarkAllRead();
100
+ }
47
101
  });
48
102
  const renderMessages = () => {
49
- const lines = [];
50
- const maxNameLen = Math.max(...messages.map((m) => m.from_name.length), humanName.length);
51
- for (const msg of messages) {
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;
52
112
  const timestamp = new Date(msg.created_at).toLocaleString();
53
- lines.push({ text: `─`, color: "gray" });
54
- lines.push({ text: `${msg.from_name.padEnd(maxNameLen)} → ${msg.to_name}`, color: "cyan" });
55
- lines.push({ text: `${timestamp}`, color: "gray" });
56
- lines.push({ text: msg.read ? "" : " [unread]" });
57
- lines.push({ text: "" });
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 });
58
127
  for (const line of msg.body.split("\n")) {
59
- lines.push({ text: ` ${line}` });
128
+ allLines.push({ text: ` ${line}`, msgIndex: mi });
60
129
  }
61
- lines.push({ text: "" });
130
+ allLines.push({ text: "", msgIndex: mi });
62
131
  }
63
- const viewHeight = contentHeight - 8;
64
- const maxOffset = Math.max(0, lines.length - viewHeight);
65
- const clampedOffset = Math.min(scrollOffset, maxOffset);
66
- const visible = lines.slice(clampedOffset, clampedOffset + viewHeight);
67
- return (_jsx(Box, { flexDirection: "column", height: viewHeight, overflow: "hidden", children: visible.length === 0 ? (_jsx(Text, { dimColor: true, children: "No messages." })) : (visible.map((line, i) => (_jsx(Text, { color: line.color, children: line.text }, i)))) }));
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))) }));
68
140
  };
69
- 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, ")"] })] }), _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 (press s)" }))] }), renderMessages()] }));
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())] }));
70
143
  }
@@ -74,13 +74,13 @@ export function ReadMail({ serverUrl, password, onBack, contentHeight }) {
74
74
  if (sessions.length === 0) {
75
75
  return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "No sessions yet. Create one first." }) }));
76
76
  }
77
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Read Mail" }), _jsx(Text, { dimColor: true, children: "Select a session:" }), _jsx(Select, { options: sessions.map((s) => ({ label: s.name, value: s.name })), onChange: loadMessages })] }));
77
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Coworker Mail" }), _jsx(Text, { dimColor: true, children: "Select a session:" }), _jsx(Select, { options: sessions.map((s) => ({ label: s.name, value: s.name })), onChange: loadMessages })] }));
78
78
  }
79
79
  if (stage === "loading") {
80
80
  return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: `Loading ${viewTab} messages...` }) }));
81
81
  }
82
82
  if (stage === "error") {
83
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Read Mail" }), _jsxs(Text, { color: "red", children: ["Error: ", error] })] }));
83
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Coworker Mail" }), _jsxs(Text, { color: "red", children: ["Error: ", error] })] }));
84
84
  }
85
85
  const renderMessages = () => {
86
86
  if (messages.length === 0) {
@@ -106,5 +106,5 @@ export function ReadMail({ serverUrl, password, onBack, contentHeight }) {
106
106
  const visible = lines.slice(clampedOffset, clampedOffset + viewHeight);
107
107
  return (_jsx(Box, { flexDirection: "column", height: viewHeight, overflow: "hidden", children: visible.map((line, i) => (_jsx(Text, { color: line.color, children: line.text }, i))) }));
108
108
  };
109
- return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Mail" }), _jsx(Text, { color: "cyan", children: selectedName }), _jsxs(Text, { dimColor: true, children: ["(", messages.length, " ", viewTab, " messages)"] })] }), _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 (press s)" }))] }), renderMessages()] }));
109
+ 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: selectedName }), _jsxs(Text, { dimColor: true, children: ["(", messages.length, " ", viewTab, " messages)"] })] }), _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 (press s)" }))] }), renderMessages()] }));
110
110
  }
@@ -3,6 +3,7 @@ interface SendMessageProps {
3
3
  password: string;
4
4
  onBack: () => void;
5
5
  contentHeight: number;
6
+ initialRecipient?: string;
6
7
  }
7
- export declare function SendMessage({ serverUrl, password, onBack, contentHeight }: SendMessageProps): import("react/jsx-runtime").JSX.Element | null;
8
+ export declare function SendMessage({ serverUrl, password, onBack, contentHeight, initialRecipient }: SendMessageProps): import("react/jsx-runtime").JSX.Element | null;
8
9
  export {};