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.
@@ -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 { Select, Spinner, TextInput } from "@inkjs/ui";
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(true);
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(Select, { options: sessions.map((s) => ({ label: s.name, value: s.name })), onChange: handleRecipientSelect })] }));
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
- useEffect(() => {
13
- void run(listSessions);
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
- if (loading || rows.length === 0)
187
+ // Sub-views own Esc themselves; pass nothing else through
188
+ if (subView !== null)
18
189
  return;
19
- if (key.upArrow) {
20
- setCursor((c) => Math.max(0, c - 1));
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
- if (key.downArrow) {
23
- setCursor((c) => Math.min(rows.length - 1, c + 1));
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
- if (input === "r") {
26
- const id = rows[cursor]?.id;
27
- if (id == null)
28
- return;
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 (next.has(id)) {
32
- next.delete(id);
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
- if (loading) {
42
- return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Loading sessions..." }) }));
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 (error) {
45
- return (_jsxs(Box, { height: contentHeight, flexDirection: "column", gap: 1, children: [_jsx(Text, { color: "red", bold: true, children: "Error" }), _jsx(Text, { children: error })] }));
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
- return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { bold: true, children: ["Sessions ", _jsxs(Text, { dimColor: true, children: ["(", rows.length, ")"] })] }), rows.length === 0 ? (_jsx(Box, { height: contentHeight - 2, alignItems: "center", justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "No sessions yet. Create one first." }) })) : (_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: "OPENCODE SESSION ID".padEnd(36) }), _jsx(Text, { bold: true, color: "cyan", children: "AGENT CODE" })] }), rows.map((s, i) => {
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.session_id.padEnd(36) }), revealed ? (_jsx(Text, { color: "yellow", children: s.agent_code })) : (_jsx(Text, { dimColor: true, children: MASKED_CODE }))] }, s.id));
51
- })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate \u00B7 r reveal/hide code \u00B7 Esc back" }) })] }));
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 {};