jarvis-hq 0.0.6 → 0.0.7

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.
@@ -5,7 +5,7 @@ exports.html = `<!DOCTYPE html>
5
5
  <html lang="en">
6
6
  <head>
7
7
  <meta charset="UTF-8" />
8
- <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
8
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
9
9
  <title>Jarvis</title>
10
10
  <style>
11
11
  :root {
@@ -349,12 +349,25 @@ exports.html = `<!DOCTYPE html>
349
349
  .msg.user .who { text-align: right; }
350
350
 
351
351
  .tool {
352
- display: flex; align-items: center; gap: 9px; margin-top: 7px;
352
+ display: flex; align-items: center; gap: 9px; margin-top: 7px; width: 100%; text-align: left;
353
353
  background: var(--surface-2); border-radius: 10px; padding: 7px 11px;
354
354
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--muted);
355
+ cursor: default;
356
+ }
357
+ .tool b { color: var(--text); font-weight: 600; flex: none; align-self: flex-start; }
358
+ .tool-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
359
+ /* Only chips with more to show become interactive; the rest stay plain labels. */
360
+ .tool.more { cursor: pointer; }
361
+ .tool.more:hover { background: var(--border); }
362
+ .tool-chev { display: none; flex: none; align-self: flex-start; color: var(--faint); font-size: 15px; line-height: 1.3; transition: transform .15s ease; }
363
+ .tool.more .tool-chev { display: block; }
364
+ @media (prefers-reduced-motion: reduce) { .tool-chev { transition: none; } }
365
+ .tool.open { align-items: flex-start; }
366
+ .tool.open .tool-chev { transform: rotate(90deg); }
367
+ .tool.open .tool-text {
368
+ white-space: pre-wrap; overflow-wrap: anywhere; text-overflow: clip; cursor: auto;
369
+ max-height: 40vh; overflow-y: auto; overscroll-behavior: contain;
355
370
  }
356
- .tool b { color: var(--text); font-weight: 600; flex: none; }
357
- .tool span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
358
371
  .note { color: var(--faint); font-size: 12.5px; text-align: center; font-style: italic; }
359
372
 
360
373
  .perm {
@@ -383,10 +396,46 @@ exports.html = `<!DOCTYPE html>
383
396
  .ask-other:focus { border-color: var(--accent); }
384
397
 
385
398
  /* --- Composer --- */
386
- .composer { border-top: 1px solid var(--border); background: var(--bg); }
399
+ .composer { border-top: 1px solid var(--border); background: var(--bg); position: relative; }
400
+ /* Only on screen once the reader has left the tail and missed something. */
401
+ .jump {
402
+ position: absolute; bottom: calc(100% + 12px); left: 0; right: 0; margin: 0 auto;
403
+ display: none; align-items: center; gap: 7px; width: max-content;
404
+ padding: 8px 15px; border-radius: 999px; border: none;
405
+ background: var(--accent); color: var(--accent-text);
406
+ font-size: 13px; font-weight: 600; box-shadow: var(--shadow-md);
407
+ }
408
+ .jump.show { display: flex; }
409
+ .jump .chev { font-size: 15px; line-height: 1; }
387
410
  .composer .wrap { padding-top: 12px; padding-bottom: 18px; }
388
411
  .activity { display: flex; align-items: center; gap: 8px; height: 22px; padding-left: 4px; color: var(--muted); font-size: 12.5px; }
389
412
  .activity:empty { display: none; }
413
+
414
+ /* --- Plan panel --- */
415
+ /* The agent's todo list, kept above the input so the current step is always
416
+ in view. Collapsed to a single line by default; the list it hides is capped
417
+ in height so a long plan can never crowd out the conversation. */
418
+ .plan { margin-bottom: 9px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface); overflow: hidden; }
419
+ .plan-bar {
420
+ display: flex; align-items: center; gap: 9px; width: 100%;
421
+ padding: 9px 12px; font-size: 12.5px; text-align: left;
422
+ }
423
+ .plan-now { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); }
424
+ .plan.active .plan-now { color: var(--accent); font-weight: 500; }
425
+ .plan-count { flex: none; color: var(--faint); font-variant-numeric: tabular-nums; }
426
+ .plan-chev { flex: none; color: var(--faint); font-size: 15px; line-height: 1; transition: transform .15s ease; }
427
+ .plan.open .plan-chev { transform: rotate(90deg); }
428
+ @media (prefers-reduced-motion: reduce) { .plan-chev { transition: none; } }
429
+ .plan-list {
430
+ margin: 0; padding: 3px 12px 10px; list-style: none;
431
+ max-height: 33vh; overflow-y: auto; border-top: 1px solid var(--border);
432
+ }
433
+ .plan-item { display: flex; gap: 9px; align-items: baseline; padding: 4px 0; font-size: 13px; }
434
+ .plan-mark { flex: none; width: 13px; text-align: center; color: var(--faint); }
435
+ .plan-item.in_progress .plan-mark, .plan-item.in_progress { color: var(--accent); }
436
+ .plan-item.in_progress { font-weight: 500; }
437
+ .plan-item.completed { color: var(--faint); text-decoration: line-through; }
438
+ .plan-item.completed .plan-mark { color: var(--ok); text-decoration: none; }
390
439
  .spinner {
391
440
  width: 11px; height: 11px; flex: none; border-radius: 50%;
392
441
  border: 1.5px solid var(--border); border-top-color: var(--accent); animation: spin .7s linear infinite;
@@ -399,7 +448,7 @@ exports.html = `<!DOCTYPE html>
399
448
  padding: 9px 9px 9px 16px; box-shadow: var(--shadow-sm);
400
449
  }
401
450
  .box:focus-within { border-color: var(--accent); }
402
- .box textarea { flex: 1; border: 0; background: none; resize: none; outline: none; max-height: 200px; padding: 6px 0; }
451
+ .box textarea { flex: 1; border: 0; background: none; resize: none; outline: none; max-height: 200px; padding: 6px 0; font-size: 16px; }
403
452
  .send {
404
453
  width: 36px; height: 36px; border-radius: 11px; flex: none;
405
454
  background: var(--accent); color: var(--accent-text); font-size: 17px;
@@ -597,14 +646,25 @@ exports.html = `<!DOCTYPE html>
597
646
  </div>
598
647
  </div></div>
599
648
  <div class="messages" id="messages"><div class="wrap narrow" id="messages-inner"></div></div>
600
- <div class="composer"><div class="wrap narrow">
601
- <div class="activity" id="activity"></div>
602
- <div class="box">
603
- <textarea id="input" rows="1" placeholder="Message…"></textarea>
604
- <button class="mic" id="mic" title="Dictate" aria-label="Dictate"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 15a4 4 0 0 0 4-4V6a4 4 0 0 0-8 0v5a4 4 0 0 0 4 4zm6-4a6 6 0 0 1-5 5.92V20h3v2H8v-2h3v-3.08A6 6 0 0 1 6 11h2a4 4 0 0 0 8 0h2z"/></svg></button>
605
- <button class="send" id="send" title="Send" aria-label="Send">&uarr;</button>
649
+ <div class="composer">
650
+ <button class="jump" id="jump"><span class="chev">&darr;</span> New messages</button>
651
+ <div class="wrap narrow">
652
+ <div class="plan" id="plan" hidden>
653
+ <button class="plan-bar" id="plan-bar" aria-expanded="false">
654
+ <span class="plan-now" id="plan-now"></span>
655
+ <span class="plan-count" id="plan-count"></span>
656
+ <span class="plan-chev" id="plan-chev">&rsaquo;</span>
657
+ </button>
658
+ <ul class="plan-list" id="plan-list" hidden></ul>
659
+ </div>
660
+ <div class="activity" id="activity"></div>
661
+ <div class="box">
662
+ <textarea id="input" rows="1" placeholder="Message…"></textarea>
663
+ <button class="mic" id="mic" title="Dictate" aria-label="Dictate"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 15a4 4 0 0 0 4-4V6a4 4 0 0 0-8 0v5a4 4 0 0 0 4 4zm6-4a6 6 0 0 1-5 5.92V20h3v2H8v-2h3v-3.08A6 6 0 0 1 6 11h2a4 4 0 0 0 8 0h2z"/></svg></button>
664
+ <button class="send" id="send" title="Send" aria-label="Send">&uarr;</button>
665
+ </div>
606
666
  </div>
607
- </div></div>
667
+ </div>
608
668
  </section>
609
669
 
610
670
  <script>
@@ -1346,6 +1406,65 @@ exports.html = `<!DOCTYPE html>
1346
1406
  box.hidden = false;
1347
1407
  }
1348
1408
 
1409
+ // --- Plan panel ---
1410
+ // The agent's own todo list, mirrored above the input so the step it is on is
1411
+ // always in view. TodoWrite always writes the whole plan, so each call simply
1412
+ // replaces what the panel shows; there is nothing to merge or diff. Collapsed
1413
+ // to one line by default, and the line itself is the cue — it changes as the
1414
+ // agent moves through the list.
1415
+
1416
+ var planOpen = false; // sticky across updates, reset when a thread opens
1417
+ var PLAN_MARKS = { completed: "\\u2713", in_progress: "\\u25b8", pending: "\\u2610" };
1418
+
1419
+ function renderTodos(todos) {
1420
+ var box = $("plan");
1421
+ if (!todos || !todos.length) { box.hidden = true; return; }
1422
+
1423
+ var done = 0, current = null, firstPending = null;
1424
+ todos.forEach(function (t) {
1425
+ if (t.status === "completed") done++;
1426
+ else if (t.status === "in_progress" && !current) current = t;
1427
+ else if (t.status === "pending" && !firstPending) firstPending = t;
1428
+ });
1429
+ var shown = current || firstPending;
1430
+
1431
+ // activeForm is the present-continuous wording the tool asks for, which is
1432
+ // exactly what a status line wants; content is the fallback.
1433
+ $("plan-now").textContent = shown ? (shown.activeForm || shown.content) : "Plan complete";
1434
+ $("plan-count").textContent = done + "/" + todos.length;
1435
+
1436
+ var list = $("plan-list");
1437
+ list.innerHTML = "";
1438
+ todos.forEach(function (t) {
1439
+ var li = el("li", "plan-item " + t.status);
1440
+ li.appendChild(el("span", "plan-mark", PLAN_MARKS[t.status] || PLAN_MARKS.pending));
1441
+ li.appendChild(el("span", null, t.content));
1442
+ list.appendChild(li);
1443
+ });
1444
+
1445
+ setPlanOpen(planOpen);
1446
+ box.hidden = false;
1447
+ }
1448
+
1449
+ function setPlanOpen(open) {
1450
+ planOpen = open;
1451
+ var box = $("plan");
1452
+ var lit = box.querySelector(".plan-item.in_progress");
1453
+ box.className = "plan" + (lit ? " active" : "") + (open ? " open" : "");
1454
+ $("plan-list").hidden = !open;
1455
+ $("plan-bar").setAttribute("aria-expanded", open ? "true" : "false");
1456
+ }
1457
+
1458
+ /** The last plan the thread ever wrote, so reopening it days later still
1459
+ * shows where the work got to rather than an empty panel. */
1460
+ function seedPlan(msgs) {
1461
+ var last = null;
1462
+ msgs.forEach(function (m) {
1463
+ (m.content || []).forEach(function (b) { if (b.todos) last = b.todos; });
1464
+ });
1465
+ renderTodos(last);
1466
+ }
1467
+
1349
1468
  // --- Thread ---
1350
1469
  function openThread(thread) {
1351
1470
  activeThread = thread;
@@ -1354,6 +1473,8 @@ exports.html = `<!DOCTYPE html>
1354
1473
  $("thread-sub").textContent = shortPath(thread.repoPath, 30);
1355
1474
  $("thread-sub").title = thread.repoPath || "";
1356
1475
  renderContext(thread.context);
1476
+ planOpen = false;
1477
+ renderTodos(null);
1357
1478
  $("to-bot-label").textContent = threadBack === "jarvis" ? "Jarvis" : (activeBot ? activeBot.name : "Back");
1358
1479
  var av = $("thread-avatar");
1359
1480
  av.textContent = activeBot ? (activeBot.emoji || "\\u{1F916}") : "\\u{1F916}";
@@ -1382,6 +1503,7 @@ exports.html = `<!DOCTYPE html>
1382
1503
  function renderTranscript(msgs) {
1383
1504
  var box = $("messages-inner");
1384
1505
  box.innerHTML = "";
1506
+ forceBottom(); // a freshly opened transcript always starts at its tail
1385
1507
  if (!msgs.length) {
1386
1508
  var e = el("div", "empty");
1387
1509
  e.appendChild(el("h3", null, "Say the first thing"));
@@ -1389,6 +1511,7 @@ exports.html = `<!DOCTYPE html>
1389
1511
  box.appendChild(e);
1390
1512
  return;
1391
1513
  }
1514
+ seedPlan(msgs);
1392
1515
  msgs.forEach(function (m) {
1393
1516
  var content = startMessage(m.role === "user" ? "user" : "assistant");
1394
1517
  (m.content || []).forEach(function (b) {
@@ -1397,6 +1520,7 @@ exports.html = `<!DOCTYPE html>
1397
1520
  else if (b.type === "image_url") {
1398
1521
  var img = document.createElement("img");
1399
1522
  img.src = b.url; img.style.maxWidth = "100%"; img.style.borderRadius = "12px";
1523
+ img.onload = scrollDown; // an image finishing late grows the list under us
1400
1524
  content.appendChild(img);
1401
1525
  }
1402
1526
  });
@@ -1466,21 +1590,105 @@ exports.html = `<!DOCTYPE html>
1466
1590
  }
1467
1591
  }
1468
1592
 
1593
+ var TOOL_PEEK = 200; // how much of the input the collapsed one-liner carries
1594
+
1595
+ /** A chip's full text, laid out to be read: dense JSON gets indented, anything
1596
+ * else is shown as the agent wrote it. */
1597
+ function toolFullText(summary) {
1598
+ var t = summary.charAt(0);
1599
+ if (t === "{" || t === "[") {
1600
+ try { return JSON.stringify(JSON.parse(summary), null, 2); } catch (e) {}
1601
+ }
1602
+ return summary;
1603
+ }
1604
+
1469
1605
  function appendTool(content, name, input) {
1470
1606
  var chip = el("div", "tool");
1471
1607
  // Jarvis's own tools read better without the MCP plumbing in the name.
1472
1608
  chip.appendChild(el("b", null, name.replace(/^mcp__jarvis__/, "")));
1473
1609
  var summary = typeof input === "string" ? input : JSON.stringify(input || {});
1474
- chip.appendChild(el("span", null, summary.slice(0, 200)));
1610
+ var text = el("span", "tool-text", summary.slice(0, TOOL_PEEK));
1611
+ chip.appendChild(text);
1612
+ chip.appendChild(el("span", "tool-chev", "\u203A"));
1475
1613
  content.appendChild(chip);
1614
+
1615
+ // The chip only becomes clickable when the one-liner is actually hiding
1616
+ // something: text cut by the peek, newlines flattened into it, or an
1617
+ // ellipsis from the overflow. Measured now that it is laid out.
1618
+ var hidden = summary.length > TOOL_PEEK || summary.indexOf("\\n") >= 0
1619
+ || text.scrollWidth > text.clientWidth + 1;
1620
+ if (hidden) {
1621
+ var open = false;
1622
+ chip.className = "tool more";
1623
+ chip.setAttribute("role", "button");
1624
+ chip.setAttribute("tabindex", "0");
1625
+ chip.setAttribute("aria-expanded", "false");
1626
+ chip.title = "Show the full input";
1627
+ var toggle = function () {
1628
+ open = !open;
1629
+ chip.className = "tool more" + (open ? " open" : "");
1630
+ chip.setAttribute("aria-expanded", open ? "true" : "false");
1631
+ chip.title = open ? "Hide the full input" : "Show the full input";
1632
+ text.textContent = open ? toolFullText(summary) : summary.slice(0, TOOL_PEEK);
1633
+ };
1634
+ // Once open the text belongs to the reader: clicks land in it to select or
1635
+ // scroll it, and the name and chevron are what close it again.
1636
+ chip.onclick = function (ev) {
1637
+ if (!open || !text.contains(ev.target)) toggle();
1638
+ };
1639
+ chip.onkeydown = function (ev) {
1640
+ if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); toggle(); }
1641
+ };
1642
+ }
1643
+
1476
1644
  scrollDown();
1477
1645
  }
1478
1646
 
1479
- function scrollDown() {
1647
+ // --- Sticking to the tail ---
1648
+ // The list follows new content only while the reader is already at the bottom.
1649
+ // Scroll up to read something and the conversation stays where you put it,
1650
+ // with a pill offering the way back down once it has moved on without you.
1651
+ var stick = true; // following the tail?
1652
+ var missed = false; // has anything arrived since we stopped following?
1653
+ var SLACK = 64; // px from the bottom that still counts as "at the bottom"
1654
+
1655
+ function atBottom() {
1656
+ var m = $("messages");
1657
+ return m.scrollHeight - m.scrollTop - m.clientHeight <= SLACK;
1658
+ }
1659
+
1660
+ function pin() {
1480
1661
  var m = $("messages");
1481
1662
  m.scrollTop = m.scrollHeight;
1482
1663
  }
1483
1664
 
1665
+ function renderJump() {
1666
+ $("jump").classList.toggle("show", !stick && missed);
1667
+ }
1668
+
1669
+ /** New content arrived: follow it, or note that the reader has missed it. */
1670
+ function scrollDown() {
1671
+ if (stick) pin();
1672
+ else if (!missed) { missed = true; renderJump(); }
1673
+ }
1674
+
1675
+ /** Go to the tail whatever the reader was doing — for the moments that mean
1676
+ * "show me the latest": opening a thread, sending, tapping the pill. */
1677
+ function forceBottom() {
1678
+ stick = true;
1679
+ missed = false;
1680
+ pin();
1681
+ renderJump();
1682
+ }
1683
+
1684
+ function watchScroll() {
1685
+ var here = atBottom();
1686
+ if (here === stick) return;
1687
+ stick = here;
1688
+ if (stick) missed = false;
1689
+ renderJump();
1690
+ }
1691
+
1484
1692
  function showError(err) {
1485
1693
  var content = startMessage("error");
1486
1694
  appendText(content, err && err.message ? err.message : String(err));
@@ -1500,6 +1708,7 @@ exports.html = `<!DOCTYPE html>
1500
1708
  function sendText(text) {
1501
1709
  if (!text || !activeThread || state !== "idle") return;
1502
1710
  appendText(startMessage("user"), text);
1711
+ forceBottom(); // your own message always brings you back down
1503
1712
  setState("starting");
1504
1713
  setActivity("Sending\u2026");
1505
1714
 
@@ -1629,6 +1838,7 @@ exports.html = `<!DOCTYPE html>
1629
1838
  case "tool_use":
1630
1839
  if (!liveBubble) liveBubble = startMessage("assistant");
1631
1840
  appendTool(liveBubble, data.tool_name, data.tool_input);
1841
+ if (data.todos) renderTodos(data.todos);
1632
1842
  break;
1633
1843
 
1634
1844
  case "context":
@@ -2450,6 +2660,7 @@ exports.html = `<!DOCTYPE html>
2450
2660
  $("to-jarvis").onclick = function () { goJarvis(); };
2451
2661
  $("to-projects").onclick = function () { goHome(); };
2452
2662
  $("to-bot").onclick = function () { if (threadBack === "jarvis") goJarvis(); else goBotView(); };
2663
+ $("plan-bar").onclick = function () { setPlanOpen(!planOpen); };
2453
2664
  $("ask-send").onclick = function () { askJarvis($("ask-input").value); };
2454
2665
  var ask = $("ask-input");
2455
2666
  ask.addEventListener("input", function () {
@@ -2463,10 +2674,16 @@ exports.html = `<!DOCTYPE html>
2463
2674
  wireMic($("ask-mic"), $("ask-input"));
2464
2675
  wireMic($("mic"), $("input"));
2465
2676
 
2677
+ $("messages").addEventListener("scroll", watchScroll, { passive: true });
2678
+ $("jump").onclick = forceBottom;
2679
+ // The on-screen keyboard and rotation both shrink the list out from under us.
2680
+ window.addEventListener("resize", function () { if (stick) pin(); });
2681
+
2466
2682
  var input = $("input");
2467
2683
  input.addEventListener("input", function () {
2468
2684
  input.style.height = "auto";
2469
2685
  input.style.height = Math.min(input.scrollHeight, 200) + "px";
2686
+ if (stick) pin(); // a growing composer eats into the list; hold the tail
2470
2687
  });
2471
2688
  input.addEventListener("keydown", function (ev) {
2472
2689
  if (ev.key === "Enter" && !ev.shiftKey) { ev.preventDefault(); if (state === "idle") send(); }
@@ -332,6 +332,7 @@ function formatMessage(msg) {
332
332
  type: "tool_use",
333
333
  tool_name: block.name,
334
334
  tool_input: formatToolInput(block.name, block.input),
335
+ ...todoList(block.name, block.input),
335
336
  });
336
337
  }
337
338
  }
@@ -445,7 +446,7 @@ async function loadTranscript(sessionId, cwd) {
445
446
  catch {
446
447
  tool_input = JSON.stringify(b.input) ?? "";
447
448
  }
448
- blocks.push({ type: "tool_use", tool_name: b.name, tool_input });
449
+ blocks.push({ type: "tool_use", tool_name: b.name, tool_input, ...todoList(b.name, b.input) });
449
450
  }
450
451
  }
451
452
  }
@@ -537,6 +538,25 @@ async function listSessions(cwd) {
537
538
  return [];
538
539
  }
539
540
  }
541
+ /**
542
+ * The plan panel needs the list as a list, not as the one-line summary the tool
543
+ * chip shows. Carried as a separate field so the chip keeps rendering unchanged,
544
+ * and spread in so every other tool's payload stays exactly as it was.
545
+ */
546
+ function todoList(toolName, input) {
547
+ if (toolName !== "TodoWrite")
548
+ return {};
549
+ const todos = input.todos;
550
+ if (!Array.isArray(todos))
551
+ return {};
552
+ return {
553
+ todos: todos.map((t) => ({
554
+ content: String(t?.content ?? ""),
555
+ status: String(t?.status ?? "pending"),
556
+ activeForm: String(t?.activeForm ?? ""),
557
+ })),
558
+ };
559
+ }
540
560
  function formatToolInput(toolName, input) {
541
561
  switch (toolName) {
542
562
  case "Bash":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jarvis-hq",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Jarvis — a persistent AI engineering partner. One entry point from any device; project-aware handoff and delegation to Claude Code running on your machine.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {