agent-office 0.0.10 → 0.0.12

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.
@@ -429,14 +429,22 @@ function renderPage(coworker, msgs, humanName) {
429
429
  const input = document.getElementById('msg-input')
430
430
 
431
431
  let lastSeenId = parseInt(document.querySelector('#messages-inner')?.dataset?.lastId ?? '0', 10)
432
+ let userScrolledUp = false
432
433
 
433
434
  function scrollToBottom() {
434
- if (outer) outer.scrollTop = outer.scrollHeight
435
+ if (!outer) return
436
+ requestAnimationFrame(() => {
437
+ outer.scrollTop = outer.scrollHeight
438
+ // After snapping, clear the flag so future messages auto-scroll again
439
+ userScrolledUp = false
440
+ })
435
441
  }
436
442
 
437
- function isNearBottom() {
438
- return outer.scrollHeight - outer.scrollTop - outer.clientHeight < 80
439
- }
443
+ // Detect when the user manually scrolls up
444
+ outer.addEventListener('scroll', () => {
445
+ const distFromBottom = outer.scrollHeight - outer.scrollTop - outer.clientHeight
446
+ userScrolledUp = distFromBottom > 80
447
+ })
440
448
 
441
449
  // Auto-grow textarea
442
450
  input.addEventListener('input', function() {
@@ -453,7 +461,7 @@ function renderPage(coworker, msgs, humanName) {
453
461
  }
454
462
  }
455
463
 
456
- // After send: clear input, re-enable button, trigger refresh
464
+ // After send: clear input, re-enable button, force scroll and refresh
457
465
  function handleSent(event) {
458
466
  const form = event.target
459
467
  const btn = form.querySelector('.send-btn')
@@ -462,20 +470,20 @@ function renderPage(coworker, msgs, humanName) {
462
470
  input.value = ''
463
471
  input.style.height = 'auto'
464
472
  input.focus()
465
- // Force scroll on the next swap since we just sent a message
473
+ userScrolledUp = false
466
474
  lastSeenId = -1
467
475
  htmx.trigger(document.getElementById('messages'), 'load')
468
476
  }
469
477
  }
470
478
 
471
- // Only scroll to bottom when new messages actually arrive
479
+ // Scroll to bottom whenever new messages arrive, unless user has scrolled up
472
480
  document.addEventListener('htmx:afterSwap', (e) => {
473
481
  if (e.detail.target.id !== 'messages') return
474
482
  const inner = document.getElementById('messages-inner')
475
483
  const newLastId = parseInt(inner?.dataset?.lastId ?? '0', 10)
476
484
  if (newLastId > lastSeenId) {
477
485
  lastSeenId = newLastId
478
- if (isNearBottom()) scrollToBottom()
486
+ if (!userScrolledUp) scrollToBottom()
479
487
  }
480
488
  })
481
489
 
@@ -39,7 +39,7 @@ export function CronList({ serverUrl, password, onBack, contentHeight, sessionNa
39
39
  onBack();
40
40
  return;
41
41
  }
42
- if (key.escape && (mode === "confirm-delete" || mode === "confirm-enable" || mode === "confirm-disable" || mode === "history")) {
42
+ if (key.escape && (mode === "confirm-delete" || mode === "confirm-enable" || mode === "confirm-disable" || mode === "history" || mode === "view-message")) {
43
43
  setMode("list");
44
44
  return;
45
45
  }
@@ -81,6 +81,9 @@ export function CronList({ serverUrl, password, onBack, contentHeight, sessionNa
81
81
  setActionError(null);
82
82
  loadHistory(selected.id);
83
83
  }
84
+ if (input === "v") {
85
+ setMode("view-message");
86
+ }
84
87
  }
85
88
  }
86
89
  if (mode === "confirm-delete" || mode === "confirm-enable" || mode === "confirm-disable") {
@@ -290,6 +293,14 @@ export function CronList({ serverUrl, password, onBack, contentHeight, sessionNa
290
293
  }
291
294
  return null;
292
295
  };
296
+ const renderViewMessage = () => {
297
+ if (mode !== "view-message")
298
+ return null;
299
+ const selected = filteredCrons[cursor];
300
+ if (!selected)
301
+ return null;
302
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Message Preview" }), _jsx(Text, { dimColor: true, children: selected.name })] }), _jsx(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 0, flexDirection: "column", children: _jsx(Text, { wrap: "wrap", children: selected.message }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Esc back" }) })] }));
303
+ };
293
304
  const renderHistory = () => {
294
305
  if (mode !== "history")
295
306
  return null;
@@ -301,10 +312,10 @@ export function CronList({ serverUrl, password, onBack, contentHeight, sessionNa
301
312
  const actionPanel = renderActionPanel();
302
313
  const panelHeight = actionPanel ? 5 : 0;
303
314
  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
315
+ return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [renderCreateSelectSession(), renderCreateFields(), renderViewMessage(), renderHistory(), (mode === "list" || mode === "confirm-delete" || mode === "confirm-enable" || mode === "confirm-disable" || mode === "deleting" || mode === "toggling" || mode === "view-message") && (_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
316
  ? `No cron jobs for ${selectedSession}. Press c to create one.`
306
317
  : "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
318
  const selected = idx === cursor;
308
319
  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"] }) })] }))] }));
320
+ })] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["c create \u00B7 d delete \u00B7 e enable/disable \u00B7 h history \u00B7 v view message \u00B7 f", " ", selectedSession ? "show all" : "filter by coworker", " \u00B7 Esc back"] }) })] }))] }));
310
321
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-office",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "Manage OpenCode sessions with named aliases",
5
5
  "type": "module",
6
6
  "license": "MIT",