agent-office 0.0.11 → 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,15 +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 forceNextScroll = false
432
+ let userScrolledUp = false
433
433
 
434
434
  function scrollToBottom() {
435
- if (outer) requestAnimationFrame(() => { 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
+ })
436
441
  }
437
442
 
438
- function isNearBottom() {
439
- return outer.scrollHeight - outer.scrollTop - outer.clientHeight < 150
440
- }
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
+ })
441
448
 
442
449
  // Auto-grow textarea
443
450
  input.addEventListener('input', function() {
@@ -454,7 +461,7 @@ function renderPage(coworker, msgs, humanName) {
454
461
  }
455
462
  }
456
463
 
457
- // After send: clear input, re-enable button, trigger refresh
464
+ // After send: clear input, re-enable button, force scroll and refresh
458
465
  function handleSent(event) {
459
466
  const form = event.target
460
467
  const btn = form.querySelector('.send-btn')
@@ -463,22 +470,20 @@ function renderPage(coworker, msgs, humanName) {
463
470
  input.value = ''
464
471
  input.style.height = 'auto'
465
472
  input.focus()
466
- // Force scroll on the next swap since we just sent a message
473
+ userScrolledUp = false
467
474
  lastSeenId = -1
468
- forceNextScroll = true
469
475
  htmx.trigger(document.getElementById('messages'), 'load')
470
476
  }
471
477
  }
472
478
 
473
- // Only scroll to bottom when new messages actually arrive
479
+ // Scroll to bottom whenever new messages arrive, unless user has scrolled up
474
480
  document.addEventListener('htmx:afterSwap', (e) => {
475
481
  if (e.detail.target.id !== 'messages') return
476
482
  const inner = document.getElementById('messages-inner')
477
483
  const newLastId = parseInt(inner?.dataset?.lastId ?? '0', 10)
478
484
  if (newLastId > lastSeenId) {
479
485
  lastSeenId = newLastId
480
- if (forceNextScroll || isNearBottom()) scrollToBottom()
481
- forceNextScroll = false
486
+ if (!userScrolledUp) scrollToBottom()
482
487
  }
483
488
  })
484
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.11",
3
+ "version": "0.0.12",
4
4
  "description": "Manage OpenCode sessions with named aliases",
5
5
  "type": "module",
6
6
  "license": "MIT",