jarvis-hq 0.0.5 → 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.
- package/dist/client-html.js +423 -21
- package/dist/dictation.js +8 -6
- package/dist/server.js +15 -4
- package/dist/start-claude-code.js +21 -1
- package/package.json +1 -1
package/dist/client-html.js
CHANGED
|
@@ -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 {
|
|
@@ -367,12 +380,62 @@ exports.html = `<!DOCTYPE html>
|
|
|
367
380
|
color: var(--muted); white-space: pre-wrap; overflow-wrap: anywhere; max-height: 150px; overflow: auto;
|
|
368
381
|
}
|
|
369
382
|
.perm .acts { display: flex; gap: 8px; }
|
|
383
|
+
.ask-questions { display: flex; flex-direction: column; gap: 16px; margin-bottom: 14px; }
|
|
384
|
+
.ask-q { display: flex; flex-direction: column; gap: 8px; }
|
|
385
|
+
.ask-q-text { font-size: 13.5px; font-weight: 500; }
|
|
386
|
+
.ask-opts { display: flex; flex-direction: column; gap: 6px; }
|
|
387
|
+
.ask-opt-btn {
|
|
388
|
+
text-align: left; padding: 7px 12px; border-radius: 8px; font-size: 13px;
|
|
389
|
+
background: var(--surface-2); border: 1px solid var(--border); color: var(--text);
|
|
390
|
+
cursor: pointer; box-shadow: none;
|
|
391
|
+
}
|
|
392
|
+
.ask-opt-btn:hover { border-color: var(--accent); }
|
|
393
|
+
.ask-opt-btn.selected { background: color-mix(in srgb, var(--accent) 12%, var(--surface)); border-color: var(--accent); color: var(--text); }
|
|
394
|
+
.ask-opt-desc { font-size: 11.5px; color: var(--muted); margin-top: 1px; padding-left: 2px; }
|
|
395
|
+
.ask-other { width: 100%; padding: 7px 12px; border-radius: 8px; font-size: 13px; border: 1px solid var(--border); background: var(--surface-2); color: var(--text); outline: none; }
|
|
396
|
+
.ask-other:focus { border-color: var(--accent); }
|
|
370
397
|
|
|
371
398
|
/* --- Composer --- */
|
|
372
|
-
.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; }
|
|
373
410
|
.composer .wrap { padding-top: 12px; padding-bottom: 18px; }
|
|
374
411
|
.activity { display: flex; align-items: center; gap: 8px; height: 22px; padding-left: 4px; color: var(--muted); font-size: 12.5px; }
|
|
375
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; }
|
|
376
439
|
.spinner {
|
|
377
440
|
width: 11px; height: 11px; flex: none; border-radius: 50%;
|
|
378
441
|
border: 1.5px solid var(--border); border-top-color: var(--accent); animation: spin .7s linear infinite;
|
|
@@ -385,7 +448,7 @@ exports.html = `<!DOCTYPE html>
|
|
|
385
448
|
padding: 9px 9px 9px 16px; box-shadow: var(--shadow-sm);
|
|
386
449
|
}
|
|
387
450
|
.box:focus-within { border-color: var(--accent); }
|
|
388
|
-
.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; }
|
|
389
452
|
.send {
|
|
390
453
|
width: 36px; height: 36px; border-radius: 11px; flex: none;
|
|
391
454
|
background: var(--accent); color: var(--accent-text); font-size: 17px;
|
|
@@ -487,6 +550,11 @@ exports.html = `<!DOCTYPE html>
|
|
|
487
550
|
box-shadow: var(--shadow-md); animation: rise .18s ease;
|
|
488
551
|
}
|
|
489
552
|
@keyframes rise { from { opacity: 0; transform: translate(-50%, 8px); } }
|
|
553
|
+
.toast-action {
|
|
554
|
+
margin-left: 10px; padding: 2px 10px; border-radius: 999px;
|
|
555
|
+
background: var(--bg); color: var(--text); border: none;
|
|
556
|
+
font-size: 13px; font-weight: 600; cursor: pointer;
|
|
557
|
+
}
|
|
490
558
|
|
|
491
559
|
@media (max-width: 640px) {
|
|
492
560
|
.wrap { padding: 0 16px; }
|
|
@@ -578,14 +646,25 @@ exports.html = `<!DOCTYPE html>
|
|
|
578
646
|
</div>
|
|
579
647
|
</div></div>
|
|
580
648
|
<div class="messages" id="messages"><div class="wrap narrow" id="messages-inner"></div></div>
|
|
581
|
-
<div class="composer"
|
|
582
|
-
<
|
|
583
|
-
<div class="
|
|
584
|
-
<
|
|
585
|
-
|
|
586
|
-
|
|
649
|
+
<div class="composer">
|
|
650
|
+
<button class="jump" id="jump"><span class="chev">↓</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">›</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">↑</button>
|
|
665
|
+
</div>
|
|
587
666
|
</div>
|
|
588
|
-
</div
|
|
667
|
+
</div>
|
|
589
668
|
</section>
|
|
590
669
|
|
|
591
670
|
<script>
|
|
@@ -1327,6 +1406,65 @@ exports.html = `<!DOCTYPE html>
|
|
|
1327
1406
|
box.hidden = false;
|
|
1328
1407
|
}
|
|
1329
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
|
+
|
|
1330
1468
|
// --- Thread ---
|
|
1331
1469
|
function openThread(thread) {
|
|
1332
1470
|
activeThread = thread;
|
|
@@ -1335,6 +1473,8 @@ exports.html = `<!DOCTYPE html>
|
|
|
1335
1473
|
$("thread-sub").textContent = shortPath(thread.repoPath, 30);
|
|
1336
1474
|
$("thread-sub").title = thread.repoPath || "";
|
|
1337
1475
|
renderContext(thread.context);
|
|
1476
|
+
planOpen = false;
|
|
1477
|
+
renderTodos(null);
|
|
1338
1478
|
$("to-bot-label").textContent = threadBack === "jarvis" ? "Jarvis" : (activeBot ? activeBot.name : "Back");
|
|
1339
1479
|
var av = $("thread-avatar");
|
|
1340
1480
|
av.textContent = activeBot ? (activeBot.emoji || "\\u{1F916}") : "\\u{1F916}";
|
|
@@ -1363,6 +1503,7 @@ exports.html = `<!DOCTYPE html>
|
|
|
1363
1503
|
function renderTranscript(msgs) {
|
|
1364
1504
|
var box = $("messages-inner");
|
|
1365
1505
|
box.innerHTML = "";
|
|
1506
|
+
forceBottom(); // a freshly opened transcript always starts at its tail
|
|
1366
1507
|
if (!msgs.length) {
|
|
1367
1508
|
var e = el("div", "empty");
|
|
1368
1509
|
e.appendChild(el("h3", null, "Say the first thing"));
|
|
@@ -1370,6 +1511,7 @@ exports.html = `<!DOCTYPE html>
|
|
|
1370
1511
|
box.appendChild(e);
|
|
1371
1512
|
return;
|
|
1372
1513
|
}
|
|
1514
|
+
seedPlan(msgs);
|
|
1373
1515
|
msgs.forEach(function (m) {
|
|
1374
1516
|
var content = startMessage(m.role === "user" ? "user" : "assistant");
|
|
1375
1517
|
(m.content || []).forEach(function (b) {
|
|
@@ -1378,6 +1520,7 @@ exports.html = `<!DOCTYPE html>
|
|
|
1378
1520
|
else if (b.type === "image_url") {
|
|
1379
1521
|
var img = document.createElement("img");
|
|
1380
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
|
|
1381
1524
|
content.appendChild(img);
|
|
1382
1525
|
}
|
|
1383
1526
|
});
|
|
@@ -1447,21 +1590,105 @@ exports.html = `<!DOCTYPE html>
|
|
|
1447
1590
|
}
|
|
1448
1591
|
}
|
|
1449
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
|
+
|
|
1450
1605
|
function appendTool(content, name, input) {
|
|
1451
1606
|
var chip = el("div", "tool");
|
|
1452
1607
|
// Jarvis's own tools read better without the MCP plumbing in the name.
|
|
1453
1608
|
chip.appendChild(el("b", null, name.replace(/^mcp__jarvis__/, "")));
|
|
1454
1609
|
var summary = typeof input === "string" ? input : JSON.stringify(input || {});
|
|
1455
|
-
|
|
1610
|
+
var text = el("span", "tool-text", summary.slice(0, TOOL_PEEK));
|
|
1611
|
+
chip.appendChild(text);
|
|
1612
|
+
chip.appendChild(el("span", "tool-chev", "\u203A"));
|
|
1456
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
|
+
|
|
1457
1644
|
scrollDown();
|
|
1458
1645
|
}
|
|
1459
1646
|
|
|
1460
|
-
|
|
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() {
|
|
1461
1661
|
var m = $("messages");
|
|
1462
1662
|
m.scrollTop = m.scrollHeight;
|
|
1463
1663
|
}
|
|
1464
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
|
+
|
|
1465
1692
|
function showError(err) {
|
|
1466
1693
|
var content = startMessage("error");
|
|
1467
1694
|
appendText(content, err && err.message ? err.message : String(err));
|
|
@@ -1481,6 +1708,7 @@ exports.html = `<!DOCTYPE html>
|
|
|
1481
1708
|
function sendText(text) {
|
|
1482
1709
|
if (!text || !activeThread || state !== "idle") return;
|
|
1483
1710
|
appendText(startMessage("user"), text);
|
|
1711
|
+
forceBottom(); // your own message always brings you back down
|
|
1484
1712
|
setState("starting");
|
|
1485
1713
|
setActivity("Sending\u2026");
|
|
1486
1714
|
|
|
@@ -1610,6 +1838,7 @@ exports.html = `<!DOCTYPE html>
|
|
|
1610
1838
|
case "tool_use":
|
|
1611
1839
|
if (!liveBubble) liveBubble = startMessage("assistant");
|
|
1612
1840
|
appendTool(liveBubble, data.tool_name, data.tool_input);
|
|
1841
|
+
if (data.todos) renderTodos(data.todos);
|
|
1613
1842
|
break;
|
|
1614
1843
|
|
|
1615
1844
|
case "context":
|
|
@@ -1731,6 +1960,7 @@ exports.html = `<!DOCTYPE html>
|
|
|
1731
1960
|
}
|
|
1732
1961
|
|
|
1733
1962
|
function renderPermission(data) {
|
|
1963
|
+
if (data.toolName === "AskUserQuestion") { renderAskUser(data); return; }
|
|
1734
1964
|
var card = el("div", "perm");
|
|
1735
1965
|
card.appendChild(el("div", "head", "Allow " + data.toolName + "?"));
|
|
1736
1966
|
card.appendChild(el("pre", null, JSON.stringify(data.input, null, 2)));
|
|
@@ -1756,6 +1986,100 @@ exports.html = `<!DOCTYPE html>
|
|
|
1756
1986
|
scrollDown();
|
|
1757
1987
|
}
|
|
1758
1988
|
|
|
1989
|
+
function renderAskUser(data) {
|
|
1990
|
+
var input = data.input || {};
|
|
1991
|
+
var questions = Array.isArray(input.questions) ? input.questions : [];
|
|
1992
|
+
var answers = {}; // question text -> selected label(s)
|
|
1993
|
+
|
|
1994
|
+
var card = el("div", "perm");
|
|
1995
|
+
card.appendChild(el("div", "head", "Questions"));
|
|
1996
|
+
|
|
1997
|
+
var qsDiv = el("div", "ask-questions");
|
|
1998
|
+
|
|
1999
|
+
questions.forEach(function (q, qi) {
|
|
2000
|
+
answers[q.question] = q.multiSelect ? [] : null;
|
|
2001
|
+
|
|
2002
|
+
var qDiv = el("div", "ask-q");
|
|
2003
|
+
qDiv.appendChild(el("div", "ask-q-text", q.question));
|
|
2004
|
+
|
|
2005
|
+
var optsDiv = el("div", "ask-opts");
|
|
2006
|
+
|
|
2007
|
+
(q.options || []).forEach(function (opt) {
|
|
2008
|
+
var row = el("div");
|
|
2009
|
+
var btn = el("button", "ask-opt-btn", opt.label);
|
|
2010
|
+
btn.onclick = function () {
|
|
2011
|
+
if (q.multiSelect) {
|
|
2012
|
+
var arr = answers[q.question];
|
|
2013
|
+
var idx = arr.indexOf(opt.label);
|
|
2014
|
+
if (idx >= 0) { arr.splice(idx, 1); btn.classList.remove("selected"); }
|
|
2015
|
+
else { arr.push(opt.label); btn.classList.add("selected"); }
|
|
2016
|
+
} else {
|
|
2017
|
+
answers[q.question] = opt.label;
|
|
2018
|
+
optsDiv.querySelectorAll(".ask-opt-btn").forEach(function (b) { b.classList.remove("selected"); });
|
|
2019
|
+
btn.classList.add("selected");
|
|
2020
|
+
}
|
|
2021
|
+
};
|
|
2022
|
+
row.appendChild(btn);
|
|
2023
|
+
if (opt.description) row.appendChild(el("div", "ask-opt-desc", opt.description));
|
|
2024
|
+
optsDiv.appendChild(row);
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
var otherInput = el("input");
|
|
2028
|
+
otherInput.type = "text";
|
|
2029
|
+
otherInput.placeholder = "Other\u2026";
|
|
2030
|
+
otherInput.className = "ask-other";
|
|
2031
|
+
otherInput.oninput = function () {
|
|
2032
|
+
var val = otherInput.value.trim();
|
|
2033
|
+
if (q.multiSelect) {
|
|
2034
|
+
// handled on submit
|
|
2035
|
+
} else {
|
|
2036
|
+
if (val) {
|
|
2037
|
+
answers[q.question] = val;
|
|
2038
|
+
optsDiv.querySelectorAll(".ask-opt-btn").forEach(function (b) { b.classList.remove("selected"); });
|
|
2039
|
+
} else if (!answers[q.question] || typeof answers[q.question] === "string") {
|
|
2040
|
+
answers[q.question] = null;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
};
|
|
2044
|
+
optsDiv.appendChild(otherInput);
|
|
2045
|
+
qDiv.appendChild(optsDiv);
|
|
2046
|
+
qsDiv.appendChild(qDiv);
|
|
2047
|
+
});
|
|
2048
|
+
|
|
2049
|
+
card.appendChild(qsDiv);
|
|
2050
|
+
|
|
2051
|
+
var acts = el("div", "acts");
|
|
2052
|
+
var submit = el("button", "btn primary", "Submit");
|
|
2053
|
+
submit.onclick = function () {
|
|
2054
|
+
// Collect "other" text inputs for multi-select questions
|
|
2055
|
+
var qDivs = qsDiv.querySelectorAll(".ask-q");
|
|
2056
|
+
questions.forEach(function (q, qi) {
|
|
2057
|
+
if (q.multiSelect) {
|
|
2058
|
+
var inp = qDivs[qi].querySelector(".ask-other");
|
|
2059
|
+
var val = inp ? inp.value.trim() : "";
|
|
2060
|
+
if (val && answers[q.question].indexOf(val) === -1) answers[q.question].push(val);
|
|
2061
|
+
}
|
|
2062
|
+
});
|
|
2063
|
+
submit.disabled = true;
|
|
2064
|
+
api("/sessions/" + encodeURIComponent(sessionId) + "/permission", {
|
|
2065
|
+
method: "POST",
|
|
2066
|
+
body: { toolUseID: data.toolUseID, answers: answers }
|
|
2067
|
+
}).then(function () {
|
|
2068
|
+
var summary = questions.map(function (q) {
|
|
2069
|
+
var a = answers[q.question];
|
|
2070
|
+
return Array.isArray(a) ? a.join(", ") : (a || "\u2014");
|
|
2071
|
+
}).join(" \u00b7 ");
|
|
2072
|
+
card.replaceWith(el("div", "note", summary));
|
|
2073
|
+
pendingPerms = Math.max(0, pendingPerms - 1);
|
|
2074
|
+
if (!pendingPerms && state === "running") setActivity("Thinking\u2026");
|
|
2075
|
+
}).catch(showError);
|
|
2076
|
+
};
|
|
2077
|
+
acts.appendChild(submit);
|
|
2078
|
+
card.appendChild(acts);
|
|
2079
|
+
$("messages-inner").appendChild(card);
|
|
2080
|
+
scrollDown();
|
|
2081
|
+
}
|
|
2082
|
+
|
|
1759
2083
|
function finishTurn() {
|
|
1760
2084
|
closeStream();
|
|
1761
2085
|
liveBubble = null;
|
|
@@ -1924,6 +2248,70 @@ exports.html = `<!DOCTYPE html>
|
|
|
1924
2248
|
});
|
|
1925
2249
|
}
|
|
1926
2250
|
|
|
2251
|
+
// IndexedDB helpers — save the raw audio before the API call so a failed
|
|
2252
|
+
// transcription can be retried without re-recording.
|
|
2253
|
+
var _dictDb = null;
|
|
2254
|
+
function openDictationDb() {
|
|
2255
|
+
if (_dictDb) return Promise.resolve(_dictDb);
|
|
2256
|
+
return new Promise(function (resolve, reject) {
|
|
2257
|
+
var req = indexedDB.open("dictation", 1);
|
|
2258
|
+
req.onupgradeneeded = function () { req.result.createObjectStore("pending"); };
|
|
2259
|
+
req.onsuccess = function () { _dictDb = req.result; resolve(_dictDb); };
|
|
2260
|
+
req.onerror = function () { reject(req.error); };
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
function savePendingAudio(audio, mimeType) {
|
|
2264
|
+
return openDictationDb().then(function (db) {
|
|
2265
|
+
return new Promise(function (resolve, reject) {
|
|
2266
|
+
var tx = db.transaction("pending", "readwrite");
|
|
2267
|
+
tx.objectStore("pending").put({ audio: audio, mimeType: mimeType }, "clip");
|
|
2268
|
+
tx.oncomplete = resolve;
|
|
2269
|
+
tx.onerror = function () { reject(tx.error); };
|
|
2270
|
+
});
|
|
2271
|
+
}).catch(function () {}); // best-effort
|
|
2272
|
+
}
|
|
2273
|
+
function deletePendingAudio() {
|
|
2274
|
+
return openDictationDb().then(function (db) {
|
|
2275
|
+
return new Promise(function (resolve) {
|
|
2276
|
+
var tx = db.transaction("pending", "readwrite");
|
|
2277
|
+
tx.objectStore("pending").delete("clip");
|
|
2278
|
+
tx.oncomplete = resolve;
|
|
2279
|
+
tx.onerror = resolve;
|
|
2280
|
+
});
|
|
2281
|
+
}).catch(function () {});
|
|
2282
|
+
}
|
|
2283
|
+
function loadPendingAudio() {
|
|
2284
|
+
return openDictationDb().then(function (db) {
|
|
2285
|
+
return new Promise(function (resolve) {
|
|
2286
|
+
var req = db.transaction("pending", "readonly").objectStore("pending").get("clip");
|
|
2287
|
+
req.onsuccess = function () { resolve(req.result || null); };
|
|
2288
|
+
req.onerror = function () { resolve(null); };
|
|
2289
|
+
});
|
|
2290
|
+
}).catch(function () { return null; });
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
function sendDictation(audio, mimeType, btn, textarea) {
|
|
2294
|
+
return api("/dictate", { method: "POST", body: { audio: audio, mimeType: mimeType } })
|
|
2295
|
+
.then(function (d) {
|
|
2296
|
+
deletePendingAudio();
|
|
2297
|
+
insertDictation(textarea, d.text);
|
|
2298
|
+
}).catch(function (err) {
|
|
2299
|
+
toast(err.message || "Dictation failed", {
|
|
2300
|
+
label: "Retry",
|
|
2301
|
+
fn: function () {
|
|
2302
|
+
loadPendingAudio().then(function (saved) {
|
|
2303
|
+
if (!saved) { toast("No saved clip to retry"); return; }
|
|
2304
|
+
btn.classList.add("busy");
|
|
2305
|
+
sendDictation(saved.audio, saved.mimeType, btn, textarea)
|
|
2306
|
+
.then(function () { btn.classList.remove("busy"); });
|
|
2307
|
+
});
|
|
2308
|
+
},
|
|
2309
|
+
});
|
|
2310
|
+
}).then(function () {
|
|
2311
|
+
btn.classList.remove("busy");
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
|
|
1927
2315
|
function stopRecording() {
|
|
1928
2316
|
var rec = recording;
|
|
1929
2317
|
if (!rec) return;
|
|
@@ -1941,6 +2329,7 @@ exports.html = `<!DOCTYPE html>
|
|
|
1941
2329
|
if (!window.isSecureContext) {
|
|
1942
2330
|
toast("Dictation needs https or localhost"); return;
|
|
1943
2331
|
}
|
|
2332
|
+
deletePendingAudio();
|
|
1944
2333
|
navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) {
|
|
1945
2334
|
var mime = recordingMime();
|
|
1946
2335
|
var recorder = mime ? new MediaRecorder(stream, { mimeType: mime }) : new MediaRecorder(stream);
|
|
@@ -1951,12 +2340,11 @@ exports.html = `<!DOCTYPE html>
|
|
|
1951
2340
|
var blob = new Blob(chunks, { type: type });
|
|
1952
2341
|
if (blob.size < 1000) { btn.classList.remove("busy"); return; } // tapped twice by accident
|
|
1953
2342
|
blobToBase64(blob).then(function (audio) {
|
|
1954
|
-
return
|
|
1955
|
-
|
|
1956
|
-
|
|
2343
|
+
return savePendingAudio(audio, type).then(function () {
|
|
2344
|
+
return sendDictation(audio, type, btn, textarea);
|
|
2345
|
+
});
|
|
1957
2346
|
}).catch(function (err) {
|
|
1958
2347
|
toast(err.message || "Dictation failed");
|
|
1959
|
-
}).then(function () {
|
|
1960
2348
|
btn.classList.remove("busy");
|
|
1961
2349
|
});
|
|
1962
2350
|
};
|
|
@@ -1977,12 +2365,19 @@ exports.html = `<!DOCTYPE html>
|
|
|
1977
2365
|
};
|
|
1978
2366
|
}
|
|
1979
2367
|
|
|
1980
|
-
function toast(text) {
|
|
2368
|
+
function toast(text, action) {
|
|
1981
2369
|
var old = document.querySelector(".toast");
|
|
1982
2370
|
if (old) old.remove();
|
|
1983
2371
|
var t = el("div", "toast", text);
|
|
2372
|
+
if (action) {
|
|
2373
|
+
var btn = document.createElement("button");
|
|
2374
|
+
btn.className = "toast-action";
|
|
2375
|
+
btn.textContent = action.label;
|
|
2376
|
+
btn.onclick = function () { t.remove(); action.fn(); };
|
|
2377
|
+
t.appendChild(btn);
|
|
2378
|
+
}
|
|
1984
2379
|
document.body.appendChild(t);
|
|
1985
|
-
setTimeout(function () { t.remove(); }, 2400);
|
|
2380
|
+
setTimeout(function () { if (t.parentNode) t.remove(); }, action ? 7000 : 2400);
|
|
1986
2381
|
}
|
|
1987
2382
|
|
|
1988
2383
|
function copyText(text) {
|
|
@@ -2265,6 +2660,7 @@ exports.html = `<!DOCTYPE html>
|
|
|
2265
2660
|
$("to-jarvis").onclick = function () { goJarvis(); };
|
|
2266
2661
|
$("to-projects").onclick = function () { goHome(); };
|
|
2267
2662
|
$("to-bot").onclick = function () { if (threadBack === "jarvis") goJarvis(); else goBotView(); };
|
|
2663
|
+
$("plan-bar").onclick = function () { setPlanOpen(!planOpen); };
|
|
2268
2664
|
$("ask-send").onclick = function () { askJarvis($("ask-input").value); };
|
|
2269
2665
|
var ask = $("ask-input");
|
|
2270
2666
|
ask.addEventListener("input", function () {
|
|
@@ -2278,10 +2674,16 @@ exports.html = `<!DOCTYPE html>
|
|
|
2278
2674
|
wireMic($("ask-mic"), $("ask-input"));
|
|
2279
2675
|
wireMic($("mic"), $("input"));
|
|
2280
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
|
+
|
|
2281
2682
|
var input = $("input");
|
|
2282
2683
|
input.addEventListener("input", function () {
|
|
2283
2684
|
input.style.height = "auto";
|
|
2284
2685
|
input.style.height = Math.min(input.scrollHeight, 200) + "px";
|
|
2686
|
+
if (stick) pin(); // a growing composer eats into the list; hold the tail
|
|
2285
2687
|
});
|
|
2286
2688
|
input.addEventListener("keydown", function (ev) {
|
|
2287
2689
|
if (ev.key === "Enter" && !ev.shiftKey) { ev.preventDefault(); if (state === "idle") send(); }
|
package/dist/dictation.js
CHANGED
|
@@ -21,15 +21,17 @@ const OPENAI_URL = "https://api.openai.com/v1/chat/completions";
|
|
|
21
21
|
const CLEANUP_MODEL = "gpt-4o-mini";
|
|
22
22
|
// Roughly two minutes of opus; anything longer is a mistake, not a message.
|
|
23
23
|
const MAX_AUDIO_BYTES = 8 * 1024 * 1024;
|
|
24
|
-
const CLEANUP_PROMPT = `You are
|
|
25
|
-
The user will provide a
|
|
24
|
+
const CLEANUP_PROMPT = `You are a voice transcription cleanup assistant.
|
|
25
|
+
The user will provide a raw voice transcript. Your job is light editing only — preserve their exact words and phrasing as much as possible.
|
|
26
26
|
|
|
27
27
|
Your task:
|
|
28
|
-
1.
|
|
29
|
-
2.
|
|
30
|
-
3.
|
|
28
|
+
1. Remove filler words ("uh", "um", "er", "like") and false starts.
|
|
29
|
+
2. If the user spells out a word (e.g., "t-h-r-e-s-h-o-l-d"), replace the previous misspelled or unclear attempt with the correctly spelled word and drop the spelling itself.
|
|
30
|
+
3. Resolve simple self-corrections (e.g., "delete the file, actually remove the folder" → "remove the folder").
|
|
31
|
+
4. Apply light markdown formatting where it clearly fits: wrap code, file names, variable names, and commands in backticks; use a bullet list if the user enumerates multiple items.
|
|
32
|
+
5. Output ONLY the cleaned transcript. No explanations, no quotes, no commentary.
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
Do not paraphrase, summarize, reorder, or interpret — just clean up the words they said.`;
|
|
33
35
|
function dictationConfigured() {
|
|
34
36
|
return {
|
|
35
37
|
deepgram: Boolean(process.env.DEEPGRAM_API_KEY),
|
package/dist/server.js
CHANGED
|
@@ -221,7 +221,7 @@ async function handleRequest(req, res, availableAgents, workspaceCwd) {
|
|
|
221
221
|
return;
|
|
222
222
|
}
|
|
223
223
|
const body = await (0, server_common_1.readBody)(req);
|
|
224
|
-
const { toolUseID, approved, updatedInput } = body;
|
|
224
|
+
const { toolUseID, approved, updatedInput, answers } = body;
|
|
225
225
|
if (!toolUseID) {
|
|
226
226
|
(0, server_common_1.jsonError)(res, 400, "toolUseID is required");
|
|
227
227
|
return;
|
|
@@ -232,9 +232,20 @@ async function handleRequest(req, res, availableAgents, workspaceCwd) {
|
|
|
232
232
|
if (pending) {
|
|
233
233
|
store.pendingPermissions.delete(toolUseID);
|
|
234
234
|
(0, server_common_1.notifyPermissionsChanged)();
|
|
235
|
-
pending.
|
|
236
|
-
|
|
237
|
-
|
|
235
|
+
if (pending.toolName === "AskUserQuestion" && answers) {
|
|
236
|
+
const questions = pending.input?.questions ?? [];
|
|
237
|
+
const lines = questions.map((q) => {
|
|
238
|
+
const a = answers[q.question];
|
|
239
|
+
const answer = Array.isArray(a) ? (a.length ? a.join(", ") : "(none selected)") : (a || "(no answer)");
|
|
240
|
+
return `- ${q.question}: ${answer}`;
|
|
241
|
+
});
|
|
242
|
+
pending.resolve({ behavior: "deny", message: "User's answers:\n" + lines.join("\n") });
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
pending.resolve(approved
|
|
246
|
+
? { behavior: "allow", updatedInput: updatedInput ?? pending.input }
|
|
247
|
+
: { behavior: "deny", message: "User denied" });
|
|
248
|
+
}
|
|
238
249
|
}
|
|
239
250
|
}
|
|
240
251
|
else if (store.agent === "opencode" && store.sdkSessionId) {
|
|
@@ -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.
|
|
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": {
|