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.
@@ -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,493 @@
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
+ if (part.type === "text" && part.text) {
38
+ const lines = part.text.split("\n");
39
+ for (let i = 0; i < lines.length; i++) {
40
+ messageLines.push({ role: msg.role, text: i === 0 ? lines[i] : ` ${lines[i]}` });
41
+ }
42
+ messageLines.push({ role: msg.role, text: "" });
43
+ }
44
+ else if (part.type === "tool") {
45
+ // Tool part has: tool (string), input, output
46
+ const toolName = typeof part.tool === "string" ? part.tool : String(part.tool ?? "unknown");
47
+ messageLines.push({ role: msg.role, text: "" }); // Blank line before tool
48
+ messageLines.push({ role: msg.role, text: `▶ Tool: ${toolName}` });
49
+ if (part.input !== undefined) {
50
+ // Try to pretty-print input if it's an object
51
+ let inputStr;
52
+ if (typeof part.input === "object" && part.input !== null) {
53
+ inputStr = JSON.stringify(part.input, null, 2);
54
+ }
55
+ else {
56
+ inputStr = String(part.input);
57
+ }
58
+ const lines = inputStr.split("\n");
59
+ for (let i = 0; i < Math.min(lines.length, 15); i++) {
60
+ messageLines.push({ role: msg.role, text: ` ${lines[i]}` });
61
+ }
62
+ if (lines.length > 15) {
63
+ messageLines.push({ role: msg.role, text: ` [...] (${lines.length - 15} more lines)` });
64
+ }
65
+ }
66
+ if (part.output !== undefined) {
67
+ // Show output type and preview
68
+ let outputPreview;
69
+ if (typeof part.output === "object" && part.output !== null) {
70
+ outputPreview = JSON.stringify(part.output, null, 2).slice(0, 200);
71
+ }
72
+ else if (typeof part.output === "string") {
73
+ outputPreview = part.output.slice(0, 200);
74
+ }
75
+ else {
76
+ outputPreview = String(part.output).slice(0, 200);
77
+ }
78
+ const outputType = typeof part.output === "object" ? "object" : typeof part.output;
79
+ messageLines.push({ role: msg.role, text: ` ${"—".repeat(40)}` });
80
+ messageLines.push({ role: msg.role, text: ` Result (${outputType}):` });
81
+ const lines = outputPreview.split("\n");
82
+ for (let i = 0; i < Math.min(lines.length, 5); i++) {
83
+ messageLines.push({ role: msg.role, text: ` ${lines[i]}` });
84
+ }
85
+ if (lines.length > 5 || outputPreview.length >= 200) {
86
+ messageLines.push({ role: msg.role, text: " [...]" });
87
+ }
88
+ }
89
+ messageLines.push({ role: msg.role, text: "" }); // Blank line after tool
90
+ }
91
+ }
92
+ }
93
+ const viewHeight = contentHeight - 3;
94
+ const maxOffset = Math.max(0, messageLines.length - viewHeight);
95
+ const clampedOffset = Math.min(scrollOffset, maxOffset);
96
+ const visible = messageLines.slice(clampedOffset, clampedOffset + viewHeight);
97
+ 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"] }))] }));
98
+ }
99
+ function InjectView({ serverUrl, password, sessionName, contentHeight, onClose }) {
100
+ const { injectText } = useApi(serverUrl, password);
101
+ const [stage, setStage] = useState("input");
102
+ const [error, setError] = useState(null);
103
+ const [submitted, setSubmitted] = useState(false);
104
+ useInput((_input, key) => {
105
+ if (key.escape && stage !== "submitting") {
106
+ onClose();
107
+ return;
108
+ }
109
+ if ((stage === "done" || stage === "error") && key.escape) {
110
+ onClose();
111
+ return;
112
+ }
113
+ });
114
+ // Auto-close after success
115
+ useEffect(() => {
116
+ if (stage === "done") {
117
+ const t = setTimeout(onClose, 1500);
118
+ return () => clearTimeout(t);
119
+ }
120
+ }, [stage, onClose]);
121
+ const handleSubmit = async (text) => {
122
+ const trimmed = text.trim();
123
+ if (!trimmed)
124
+ return;
125
+ setSubmitted(true);
126
+ setStage("submitting");
127
+ try {
128
+ await injectText(sessionName, trimmed);
129
+ setStage("done");
130
+ }
131
+ catch (err) {
132
+ setError(err instanceof Error ? err.message : String(err));
133
+ setStage("error");
134
+ }
135
+ };
136
+ 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" })] }))] }));
137
+ }
138
+ function CoworkerMailView({ serverUrl, password, sessionName, contentHeight, onClose }) {
139
+ const { getMailMessages } = useApi(serverUrl, password);
140
+ const [tab, setTab] = useState("received");
141
+ const [messages, setMessages] = useState([]);
142
+ const [loading, setLoading] = useState(true);
143
+ const [error, setError] = useState(null);
144
+ const [scrollOffset, setScrollOffset] = useState(0);
145
+ const loadTab = useCallback(async (t) => {
146
+ setLoading(true);
147
+ setScrollOffset(0);
148
+ try {
149
+ const msgs = await getMailMessages(sessionName, { sent: t === "sent" });
150
+ setMessages(msgs);
151
+ }
152
+ catch (err) {
153
+ setError(err instanceof Error ? err.message : String(err));
154
+ }
155
+ finally {
156
+ setLoading(false);
157
+ }
158
+ }, [sessionName]);
159
+ useEffect(() => { void loadTab("received"); }, [loadTab]);
160
+ useInput((input, key) => {
161
+ if (key.escape) {
162
+ onClose();
163
+ return;
164
+ }
165
+ if (!loading) {
166
+ if (key.upArrow)
167
+ setScrollOffset((o) => Math.max(0, o - 1));
168
+ if (key.downArrow)
169
+ setScrollOffset((o) => o + 1);
170
+ if (input === "r" && tab !== "received") {
171
+ setTab("received");
172
+ void loadTab("received");
173
+ }
174
+ if (input === "s" && tab !== "sent") {
175
+ setTab("sent");
176
+ void loadTab("sent");
177
+ }
178
+ }
179
+ });
180
+ const renderMessages = () => {
181
+ if (loading)
182
+ return _jsx(Spinner, { label: "Loading..." });
183
+ if (error)
184
+ return _jsxs(Text, { color: "red", children: ["Error: ", error] });
185
+ if (messages.length === 0)
186
+ return _jsxs(Text, { dimColor: true, children: ["No ", tab, " messages."] });
187
+ const lines = [];
188
+ const maxNameLen = Math.max(...messages.map((m) => m.from_name.length));
189
+ for (const msg of messages) {
190
+ const timestamp = new Date(msg.created_at).toLocaleString();
191
+ lines.push({ text: "─", color: "gray" });
192
+ lines.push({ text: `${msg.from_name.padEnd(maxNameLen)} → ${msg.to_name}`, color: "cyan" });
193
+ lines.push({ text: timestamp, color: "gray" });
194
+ if (!msg.read)
195
+ lines.push({ text: " [unread]", color: "yellow" });
196
+ lines.push({ text: "" });
197
+ for (const line of msg.body.split("\n")) {
198
+ lines.push({ text: ` ${line}` });
199
+ }
200
+ lines.push({ text: "" });
201
+ }
202
+ const viewHeight = contentHeight - 7;
203
+ const maxOffset = Math.max(0, lines.length - viewHeight);
204
+ const clamped = Math.min(scrollOffset, maxOffset);
205
+ const visible = lines.slice(clamped, clamped + viewHeight);
206
+ return (_jsx(Box, { flexDirection: "column", height: viewHeight, overflow: "hidden", children: visible.map((line, i) => (_jsx(Text, { color: line.color, children: line.text }, i))) }));
207
+ };
208
+ 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"
209
+ ? _jsx(Text, { bold: true, color: "green", children: "[Received]" })
210
+ : _jsx(Text, { dimColor: true, children: "Received (r)" }), tab === "sent"
211
+ ? _jsx(Text, { bold: true, color: "yellow", children: "[Sent]" })
212
+ : _jsx(Text, { dimColor: true, children: "Sent (s)" })] }), renderMessages()] }));
213
+ }
214
+ // ─── Main component ──────────────────────────────────────────────────────────
7
215
  export function SessionList({ serverUrl, password, contentHeight }) {
8
- const { listSessions } = useApi(serverUrl, password);
9
- const { data: sessions, loading, error, run } = useAsyncState();
216
+ const { listSessions, createSession, deleteSession, regenerateCode, getModes, revertToStart } = useApi(serverUrl, password);
217
+ const { data: sessions, loading, error: loadError, run } = useAsyncState();
10
218
  const [cursor, setCursor] = useState(0);
11
219
  const [revealedRows, setRevealedRows] = useState(new Set());
12
- useEffect(() => {
13
- void run(listSessions);
14
- }, []);
220
+ const [mode, setMode] = useState("browse");
221
+ const [subView, setSubView] = useState(null);
222
+ const [actionError, setActionError] = useState(null);
223
+ const [actionMsg, setActionMsg] = useState(null);
224
+ const [availableModes, setAvailableModes] = useState([]);
225
+ const [pendingMode, setPendingMode] = useState(null);
226
+ const [modeCursor, setModeCursor] = useState(0);
227
+ const reload = () => void run(listSessions);
228
+ useEffect(() => { reload(); }, []);
15
229
  const rows = sessions ?? [];
230
+ // Clamp cursor when list shrinks
231
+ useEffect(() => {
232
+ if (rows.length > 0)
233
+ setCursor((c) => Math.min(c, rows.length - 1));
234
+ }, [rows.length]);
16
235
  useInput((input, key) => {
17
- if (loading || rows.length === 0)
236
+ // Sub-views own Esc themselves; pass nothing else through
237
+ if (subView !== null)
238
+ return;
239
+ if (loading)
240
+ return;
241
+ if (mode === "browse") {
242
+ if (key.upArrow)
243
+ setCursor((c) => Math.max(0, c - 1));
244
+ if (key.downArrow)
245
+ setCursor((c) => Math.min(rows.length - 1, c + 1));
246
+ if (input === "r" && rows.length > 0) {
247
+ const id = rows[cursor]?.id;
248
+ if (id == null)
249
+ return;
250
+ setRevealedRows((prev) => {
251
+ const next = new Set(prev);
252
+ next.has(id) ? next.delete(id) : next.add(id);
253
+ return next;
254
+ });
255
+ }
256
+ if (input === "c") {
257
+ setActionError(null);
258
+ setActionMsg(null);
259
+ setPendingMode(null);
260
+ setModeCursor(0);
261
+ setMode("creating-loading");
262
+ getModes().then((modes) => {
263
+ const safeMode = Array.isArray(modes) ? modes : [];
264
+ setAvailableModes(safeMode);
265
+ setMode(safeMode.length > 0 ? "creating-pick-mode" : "creating-name");
266
+ }).catch(() => {
267
+ setAvailableModes([]);
268
+ setMode("creating-name");
269
+ });
270
+ }
271
+ if (input === "d" && rows.length > 0) {
272
+ setActionError(null);
273
+ setActionMsg(null);
274
+ setMode("confirm-delete");
275
+ }
276
+ if (input === "g" && rows.length > 0) {
277
+ setActionError(null);
278
+ setActionMsg(null);
279
+ setMode("confirm-regen");
280
+ }
281
+ if (input === "x" && rows.length > 0) {
282
+ setActionError(null);
283
+ setActionMsg(null);
284
+ setMode("confirm-revert");
285
+ }
286
+ if (rows.length > 0) {
287
+ if (input === "t")
288
+ setSubView("tail");
289
+ if (input === "i")
290
+ setSubView("inject");
291
+ if (input === "m")
292
+ setSubView("coworker-mail");
293
+ }
294
+ }
295
+ if (mode === "creating-pick-mode") {
296
+ // +1 for the "no mode" option at index 0
297
+ const total = availableModes.length + 1;
298
+ if (key.upArrow)
299
+ setModeCursor((c) => (c - 1 + total) % total);
300
+ if (key.downArrow)
301
+ setModeCursor((c) => (c + 1) % total);
302
+ if (key.return) {
303
+ const selected = modeCursor === 0 ? null : (availableModes[modeCursor - 1]?.name ?? null);
304
+ setPendingMode(selected);
305
+ setMode("creating-name");
306
+ }
307
+ if (key.escape) {
308
+ setMode("browse");
309
+ }
310
+ return;
311
+ }
312
+ if (mode === "creating-name" && key.escape) {
313
+ setMode("browse");
314
+ return;
315
+ }
316
+ // Dismiss feedback states with any key
317
+ if (mode === "create-done" || mode === "create-error" || mode === "delete-done" || mode === "delete-error") {
318
+ setMode("browse");
319
+ setActionError(null);
320
+ setActionMsg(null);
321
+ }
322
+ });
323
+ const handleCreate = async (name) => {
324
+ const trimmed = name.trim();
325
+ if (!trimmed)
326
+ return;
327
+ setMode("creating-busy");
328
+ try {
329
+ await createSession(trimmed, pendingMode ?? undefined);
330
+ const modeNote = pendingMode ? ` [${pendingMode}]` : "";
331
+ setActionMsg(`Coworker "${trimmed}"${modeNote} created.`);
332
+ setMode("create-done");
333
+ reload();
334
+ }
335
+ catch (err) {
336
+ setActionError(err instanceof Error ? err.message : String(err));
337
+ setMode("create-error");
338
+ }
339
+ };
340
+ const handleConfirmDelete = async (confirmed) => {
341
+ if (!confirmed) {
342
+ setMode("browse");
18
343
  return;
19
- if (key.upArrow) {
20
- setCursor((c) => Math.max(0, c - 1));
21
344
  }
22
- if (key.downArrow) {
23
- setCursor((c) => Math.min(rows.length - 1, c + 1));
345
+ const target = rows[cursor];
346
+ if (!target) {
347
+ setMode("browse");
348
+ return;
349
+ }
350
+ setMode("deleting");
351
+ try {
352
+ await deleteSession(target.name);
353
+ setActionMsg(`Coworker "${target.name}" deleted.`);
354
+ setMode("delete-done");
355
+ reload();
356
+ }
357
+ catch (err) {
358
+ setActionError(err instanceof Error ? err.message : String(err));
359
+ setMode("delete-error");
360
+ }
361
+ };
362
+ const handleConfirmRegen = async (confirmed) => {
363
+ if (!confirmed) {
364
+ setMode("browse");
365
+ return;
24
366
  }
25
- if (input === "r") {
26
- const id = rows[cursor]?.id;
27
- if (id == null)
28
- return;
367
+ const target = rows[cursor];
368
+ if (!target) {
369
+ setMode("browse");
370
+ return;
371
+ }
372
+ setMode("regenerating");
373
+ try {
374
+ await regenerateCode(target.name);
375
+ setActionMsg(`Agent code regenerated for "${target.name}".`);
29
376
  setRevealedRows((prev) => {
30
377
  const next = new Set(prev);
31
- if (next.has(id)) {
32
- next.delete(id);
33
- }
34
- else {
35
- next.add(id);
36
- }
378
+ if (target.id != null)
379
+ next.add(target.id);
37
380
  return next;
38
381
  });
382
+ setMode("create-done");
383
+ reload();
39
384
  }
40
- });
41
- if (loading) {
42
- return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Loading sessions..." }) }));
385
+ catch (err) {
386
+ setActionError(err instanceof Error ? err.message : String(err));
387
+ setMode("create-error");
388
+ }
389
+ };
390
+ const handleConfirmRevert = async (confirmed) => {
391
+ if (!confirmed) {
392
+ setMode("browse");
393
+ return;
394
+ }
395
+ const target = rows[cursor];
396
+ if (!target) {
397
+ setMode("browse");
398
+ return;
399
+ }
400
+ setMode("reverting");
401
+ try {
402
+ await revertToStart(target.name);
403
+ setActionMsg(`Session "${target.name}" reverted to first message and restarted.`);
404
+ setMode("create-done");
405
+ reload();
406
+ }
407
+ catch (err) {
408
+ setActionError(err instanceof Error ? err.message : String(err));
409
+ setMode("create-error");
410
+ }
411
+ };
412
+ // ── Full-screen create flow ───────────────────────────────────────────────
413
+ if (mode === "creating-loading") {
414
+ return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Loading agent configs..." }) }));
43
415
  }
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 })] }));
416
+ if (mode === "creating-pick-mode") {
417
+ const modeOptions = [
418
+ { label: "No mode (default)", description: "" },
419
+ ...availableModes.map((m) => ({ label: m.name, description: m.description })),
420
+ ];
421
+ 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) => {
422
+ const sel = i === modeCursor;
423
+ 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));
424
+ }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate \u00B7 Enter select \u00B7 Esc cancel" }) })] }));
425
+ }
426
+ if (mode === "creating-name") {
427
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Create Coworker" }), pendingMode
428
+ ? _jsxs(Text, { color: "yellow", children: ["[", pendingMode, "]"] })
429
+ : _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" })] }));
430
+ }
431
+ if (mode === "creating-busy") {
432
+ return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Creating coworker..." }) }));
433
+ }
434
+ if (mode === "create-done") {
435
+ 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" })] }));
436
+ }
437
+ if (mode === "create-error") {
438
+ 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" })] }));
439
+ }
440
+ // ── Sub-view rendering ────────────────────────────────────────────────────
441
+ const activeSession = rows[cursor];
442
+ if (subView !== null && activeSession) {
443
+ const closeSubView = () => setSubView(null);
444
+ if (subView === "tail") {
445
+ return (_jsx(TailView, { serverUrl: serverUrl, password: password, sessionName: activeSession.name, contentHeight: contentHeight, onClose: closeSubView }));
446
+ }
447
+ if (subView === "inject") {
448
+ return (_jsx(InjectView, { serverUrl: serverUrl, password: password, sessionName: activeSession.name, contentHeight: contentHeight, onClose: closeSubView }));
449
+ }
450
+ if (subView === "coworker-mail") {
451
+ return (_jsx(CoworkerMailView, { serverUrl: serverUrl, password: password, sessionName: activeSession.name, contentHeight: contentHeight, onClose: closeSubView }));
452
+ }
453
+ }
454
+ // ── Initial load ──────────────────────────────────────────────────────────
455
+ if (loading && rows.length === 0) {
456
+ return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Loading coworkers..." }) }));
457
+ }
458
+ if (loadError) {
459
+ return (_jsxs(Box, { height: contentHeight, flexDirection: "column", gap: 1, children: [_jsx(Text, { color: "red", bold: true, children: "Error" }), _jsx(Text, { children: loadError })] }));
46
460
  }
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) => {
461
+ // ── Inline action panel ───────────────────────────────────────────────────
462
+ const renderActionPanel = () => {
463
+ const target = rows[cursor];
464
+ if (mode === "confirm-delete" && target)
465
+ 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) }) })] }));
466
+ if (mode === "deleting")
467
+ return (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1, marginBottom: 1, children: _jsx(Spinner, { label: `Deleting "${rows[cursor]?.name}"...` }) }));
468
+ if (mode === "delete-done")
469
+ 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" })] }));
470
+ if (mode === "delete-error")
471
+ 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" })] }));
472
+ if (mode === "confirm-regen" && target)
473
+ 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) }) })] }));
474
+ if (mode === "regenerating")
475
+ return (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginBottom: 1, children: _jsx(Spinner, { label: "Generating new agent code..." }) }));
476
+ if (mode === "confirm-revert" && target)
477
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Revert to First Message" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Revert ", _jsx(Text, { color: "cyan", bold: true, children: target.name }), " to its first message and restart?", " ", _jsx(Text, { dimColor: true, children: "This clears all messages after the first one." })] }) }), _jsx(Box, { marginTop: 1, children: _jsx(ConfirmInput, { defaultChoice: "cancel", onConfirm: () => void handleConfirmRevert(true), onCancel: () => void handleConfirmRevert(false) }) })] }));
478
+ if (mode === "reverting")
479
+ return (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: _jsx(Spinner, { label: `Reverting "${rows[cursor]?.name}" and restarting...` }) }));
480
+ return null;
481
+ };
482
+ // ── Coworker table ────────────────────────────────────────────────────────
483
+ const actionPanel = renderActionPanel();
484
+ const panelHeight = actionPanel ? 5 : 0;
485
+ const tableHeight = contentHeight - panelHeight - 3;
486
+ 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: "STATUS".padEnd(20) }), _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
487
  const selected = i === cursor;
49
488
  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" }) })] }));
489
+ 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 ? "cyan" : undefined, dimColor: !selected && !s.status, children: (s.status ?? "—").padEnd(20) }), _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
490
+ ? _jsx(Text, { color: "yellow", children: s.agent_code })
491
+ : _jsx(Text, { dimColor: true, children: MASKED_CODE })] }, s.id));
492
+ })] }))] }));
52
493
  }
@@ -6,11 +6,17 @@ export function SessionSidebar({ serverUrl, password }) {
6
6
  const { listSessions } = useApi(serverUrl, password);
7
7
  const [sessions, setSessions] = useState([]);
8
8
  useEffect(() => {
9
+ // Initial fetch
9
10
  listSessions().then((s) => setSessions(s));
11
+ // Poll every 5 seconds to refresh statuses
12
+ const interval = setInterval(() => {
13
+ listSessions().then((s) => setSessions(s));
14
+ }, 5000);
15
+ return () => clearInterval(interval);
10
16
  }, [listSessions]);
11
17
  if (sessions.length === 0)
12
18
  return null;
13
- return (_jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, flexDirection: "column", gap: 0, children: [_jsx(Text, { bold: true, color: "cyan", children: "Sessions" }), sessions.map((session, i) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: getNodeColor(i), children: "\u25CF" }), _jsx(Text, { children: session.name })] }, session.id)))] }));
19
+ return (_jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, flexDirection: "column", gap: 0, children: [_jsx(Text, { bold: true, color: "cyan", children: "Sessions" }), sessions.map((session, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, flexDirection: "row", children: [_jsx(Text, { color: getNodeColor(i), children: "\u25CF" }), _jsx(Text, { children: session.name })] }), session.status && (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: session.status }) }))] }, session.id)))] }));
14
20
  }
15
21
  function getNodeColor(index) {
16
22
  const colors = ["green", "blue", "yellow", "magenta", "cyan", "red", "white"];