jarvis-hq 0.0.5 → 0.0.6

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.
@@ -367,6 +367,20 @@ exports.html = `<!DOCTYPE html>
367
367
  color: var(--muted); white-space: pre-wrap; overflow-wrap: anywhere; max-height: 150px; overflow: auto;
368
368
  }
369
369
  .perm .acts { display: flex; gap: 8px; }
370
+ .ask-questions { display: flex; flex-direction: column; gap: 16px; margin-bottom: 14px; }
371
+ .ask-q { display: flex; flex-direction: column; gap: 8px; }
372
+ .ask-q-text { font-size: 13.5px; font-weight: 500; }
373
+ .ask-opts { display: flex; flex-direction: column; gap: 6px; }
374
+ .ask-opt-btn {
375
+ text-align: left; padding: 7px 12px; border-radius: 8px; font-size: 13px;
376
+ background: var(--surface-2); border: 1px solid var(--border); color: var(--text);
377
+ cursor: pointer; box-shadow: none;
378
+ }
379
+ .ask-opt-btn:hover { border-color: var(--accent); }
380
+ .ask-opt-btn.selected { background: color-mix(in srgb, var(--accent) 12%, var(--surface)); border-color: var(--accent); color: var(--text); }
381
+ .ask-opt-desc { font-size: 11.5px; color: var(--muted); margin-top: 1px; padding-left: 2px; }
382
+ .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; }
383
+ .ask-other:focus { border-color: var(--accent); }
370
384
 
371
385
  /* --- Composer --- */
372
386
  .composer { border-top: 1px solid var(--border); background: var(--bg); }
@@ -487,6 +501,11 @@ exports.html = `<!DOCTYPE html>
487
501
  box-shadow: var(--shadow-md); animation: rise .18s ease;
488
502
  }
489
503
  @keyframes rise { from { opacity: 0; transform: translate(-50%, 8px); } }
504
+ .toast-action {
505
+ margin-left: 10px; padding: 2px 10px; border-radius: 999px;
506
+ background: var(--bg); color: var(--text); border: none;
507
+ font-size: 13px; font-weight: 600; cursor: pointer;
508
+ }
490
509
 
491
510
  @media (max-width: 640px) {
492
511
  .wrap { padding: 0 16px; }
@@ -1731,6 +1750,7 @@ exports.html = `<!DOCTYPE html>
1731
1750
  }
1732
1751
 
1733
1752
  function renderPermission(data) {
1753
+ if (data.toolName === "AskUserQuestion") { renderAskUser(data); return; }
1734
1754
  var card = el("div", "perm");
1735
1755
  card.appendChild(el("div", "head", "Allow " + data.toolName + "?"));
1736
1756
  card.appendChild(el("pre", null, JSON.stringify(data.input, null, 2)));
@@ -1756,6 +1776,100 @@ exports.html = `<!DOCTYPE html>
1756
1776
  scrollDown();
1757
1777
  }
1758
1778
 
1779
+ function renderAskUser(data) {
1780
+ var input = data.input || {};
1781
+ var questions = Array.isArray(input.questions) ? input.questions : [];
1782
+ var answers = {}; // question text -> selected label(s)
1783
+
1784
+ var card = el("div", "perm");
1785
+ card.appendChild(el("div", "head", "Questions"));
1786
+
1787
+ var qsDiv = el("div", "ask-questions");
1788
+
1789
+ questions.forEach(function (q, qi) {
1790
+ answers[q.question] = q.multiSelect ? [] : null;
1791
+
1792
+ var qDiv = el("div", "ask-q");
1793
+ qDiv.appendChild(el("div", "ask-q-text", q.question));
1794
+
1795
+ var optsDiv = el("div", "ask-opts");
1796
+
1797
+ (q.options || []).forEach(function (opt) {
1798
+ var row = el("div");
1799
+ var btn = el("button", "ask-opt-btn", opt.label);
1800
+ btn.onclick = function () {
1801
+ if (q.multiSelect) {
1802
+ var arr = answers[q.question];
1803
+ var idx = arr.indexOf(opt.label);
1804
+ if (idx >= 0) { arr.splice(idx, 1); btn.classList.remove("selected"); }
1805
+ else { arr.push(opt.label); btn.classList.add("selected"); }
1806
+ } else {
1807
+ answers[q.question] = opt.label;
1808
+ optsDiv.querySelectorAll(".ask-opt-btn").forEach(function (b) { b.classList.remove("selected"); });
1809
+ btn.classList.add("selected");
1810
+ }
1811
+ };
1812
+ row.appendChild(btn);
1813
+ if (opt.description) row.appendChild(el("div", "ask-opt-desc", opt.description));
1814
+ optsDiv.appendChild(row);
1815
+ });
1816
+
1817
+ var otherInput = el("input");
1818
+ otherInput.type = "text";
1819
+ otherInput.placeholder = "Other\u2026";
1820
+ otherInput.className = "ask-other";
1821
+ otherInput.oninput = function () {
1822
+ var val = otherInput.value.trim();
1823
+ if (q.multiSelect) {
1824
+ // handled on submit
1825
+ } else {
1826
+ if (val) {
1827
+ answers[q.question] = val;
1828
+ optsDiv.querySelectorAll(".ask-opt-btn").forEach(function (b) { b.classList.remove("selected"); });
1829
+ } else if (!answers[q.question] || typeof answers[q.question] === "string") {
1830
+ answers[q.question] = null;
1831
+ }
1832
+ }
1833
+ };
1834
+ optsDiv.appendChild(otherInput);
1835
+ qDiv.appendChild(optsDiv);
1836
+ qsDiv.appendChild(qDiv);
1837
+ });
1838
+
1839
+ card.appendChild(qsDiv);
1840
+
1841
+ var acts = el("div", "acts");
1842
+ var submit = el("button", "btn primary", "Submit");
1843
+ submit.onclick = function () {
1844
+ // Collect "other" text inputs for multi-select questions
1845
+ var qDivs = qsDiv.querySelectorAll(".ask-q");
1846
+ questions.forEach(function (q, qi) {
1847
+ if (q.multiSelect) {
1848
+ var inp = qDivs[qi].querySelector(".ask-other");
1849
+ var val = inp ? inp.value.trim() : "";
1850
+ if (val && answers[q.question].indexOf(val) === -1) answers[q.question].push(val);
1851
+ }
1852
+ });
1853
+ submit.disabled = true;
1854
+ api("/sessions/" + encodeURIComponent(sessionId) + "/permission", {
1855
+ method: "POST",
1856
+ body: { toolUseID: data.toolUseID, answers: answers }
1857
+ }).then(function () {
1858
+ var summary = questions.map(function (q) {
1859
+ var a = answers[q.question];
1860
+ return Array.isArray(a) ? a.join(", ") : (a || "\u2014");
1861
+ }).join(" \u00b7 ");
1862
+ card.replaceWith(el("div", "note", summary));
1863
+ pendingPerms = Math.max(0, pendingPerms - 1);
1864
+ if (!pendingPerms && state === "running") setActivity("Thinking\u2026");
1865
+ }).catch(showError);
1866
+ };
1867
+ acts.appendChild(submit);
1868
+ card.appendChild(acts);
1869
+ $("messages-inner").appendChild(card);
1870
+ scrollDown();
1871
+ }
1872
+
1759
1873
  function finishTurn() {
1760
1874
  closeStream();
1761
1875
  liveBubble = null;
@@ -1924,6 +2038,70 @@ exports.html = `<!DOCTYPE html>
1924
2038
  });
1925
2039
  }
1926
2040
 
2041
+ // IndexedDB helpers — save the raw audio before the API call so a failed
2042
+ // transcription can be retried without re-recording.
2043
+ var _dictDb = null;
2044
+ function openDictationDb() {
2045
+ if (_dictDb) return Promise.resolve(_dictDb);
2046
+ return new Promise(function (resolve, reject) {
2047
+ var req = indexedDB.open("dictation", 1);
2048
+ req.onupgradeneeded = function () { req.result.createObjectStore("pending"); };
2049
+ req.onsuccess = function () { _dictDb = req.result; resolve(_dictDb); };
2050
+ req.onerror = function () { reject(req.error); };
2051
+ });
2052
+ }
2053
+ function savePendingAudio(audio, mimeType) {
2054
+ return openDictationDb().then(function (db) {
2055
+ return new Promise(function (resolve, reject) {
2056
+ var tx = db.transaction("pending", "readwrite");
2057
+ tx.objectStore("pending").put({ audio: audio, mimeType: mimeType }, "clip");
2058
+ tx.oncomplete = resolve;
2059
+ tx.onerror = function () { reject(tx.error); };
2060
+ });
2061
+ }).catch(function () {}); // best-effort
2062
+ }
2063
+ function deletePendingAudio() {
2064
+ return openDictationDb().then(function (db) {
2065
+ return new Promise(function (resolve) {
2066
+ var tx = db.transaction("pending", "readwrite");
2067
+ tx.objectStore("pending").delete("clip");
2068
+ tx.oncomplete = resolve;
2069
+ tx.onerror = resolve;
2070
+ });
2071
+ }).catch(function () {});
2072
+ }
2073
+ function loadPendingAudio() {
2074
+ return openDictationDb().then(function (db) {
2075
+ return new Promise(function (resolve) {
2076
+ var req = db.transaction("pending", "readonly").objectStore("pending").get("clip");
2077
+ req.onsuccess = function () { resolve(req.result || null); };
2078
+ req.onerror = function () { resolve(null); };
2079
+ });
2080
+ }).catch(function () { return null; });
2081
+ }
2082
+
2083
+ function sendDictation(audio, mimeType, btn, textarea) {
2084
+ return api("/dictate", { method: "POST", body: { audio: audio, mimeType: mimeType } })
2085
+ .then(function (d) {
2086
+ deletePendingAudio();
2087
+ insertDictation(textarea, d.text);
2088
+ }).catch(function (err) {
2089
+ toast(err.message || "Dictation failed", {
2090
+ label: "Retry",
2091
+ fn: function () {
2092
+ loadPendingAudio().then(function (saved) {
2093
+ if (!saved) { toast("No saved clip to retry"); return; }
2094
+ btn.classList.add("busy");
2095
+ sendDictation(saved.audio, saved.mimeType, btn, textarea)
2096
+ .then(function () { btn.classList.remove("busy"); });
2097
+ });
2098
+ },
2099
+ });
2100
+ }).then(function () {
2101
+ btn.classList.remove("busy");
2102
+ });
2103
+ }
2104
+
1927
2105
  function stopRecording() {
1928
2106
  var rec = recording;
1929
2107
  if (!rec) return;
@@ -1941,6 +2119,7 @@ exports.html = `<!DOCTYPE html>
1941
2119
  if (!window.isSecureContext) {
1942
2120
  toast("Dictation needs https or localhost"); return;
1943
2121
  }
2122
+ deletePendingAudio();
1944
2123
  navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) {
1945
2124
  var mime = recordingMime();
1946
2125
  var recorder = mime ? new MediaRecorder(stream, { mimeType: mime }) : new MediaRecorder(stream);
@@ -1951,12 +2130,11 @@ exports.html = `<!DOCTYPE html>
1951
2130
  var blob = new Blob(chunks, { type: type });
1952
2131
  if (blob.size < 1000) { btn.classList.remove("busy"); return; } // tapped twice by accident
1953
2132
  blobToBase64(blob).then(function (audio) {
1954
- return api("/dictate", { method: "POST", body: { audio: audio, mimeType: type } });
1955
- }).then(function (d) {
1956
- insertDictation(textarea, d.text);
2133
+ return savePendingAudio(audio, type).then(function () {
2134
+ return sendDictation(audio, type, btn, textarea);
2135
+ });
1957
2136
  }).catch(function (err) {
1958
2137
  toast(err.message || "Dictation failed");
1959
- }).then(function () {
1960
2138
  btn.classList.remove("busy");
1961
2139
  });
1962
2140
  };
@@ -1977,12 +2155,19 @@ exports.html = `<!DOCTYPE html>
1977
2155
  };
1978
2156
  }
1979
2157
 
1980
- function toast(text) {
2158
+ function toast(text, action) {
1981
2159
  var old = document.querySelector(".toast");
1982
2160
  if (old) old.remove();
1983
2161
  var t = el("div", "toast", text);
2162
+ if (action) {
2163
+ var btn = document.createElement("button");
2164
+ btn.className = "toast-action";
2165
+ btn.textContent = action.label;
2166
+ btn.onclick = function () { t.remove(); action.fn(); };
2167
+ t.appendChild(btn);
2168
+ }
1984
2169
  document.body.appendChild(t);
1985
- setTimeout(function () { t.remove(); }, 2400);
2170
+ setTimeout(function () { if (t.parentNode) t.remove(); }, action ? 7000 : 2400);
1986
2171
  }
1987
2172
 
1988
2173
  function copyText(text) {
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 an intent extraction engine for a voice assistant.
25
- The user will provide a messy, unedited voice transcript containing filler words, pauses, and self-corrections.
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. Analyze the spoken words to find the user's ultimate intent.
29
- 2. Completely remove filler words ("uh", "um", "like") and resolve self-corrections (e.g., "set a meeting for 5, no, make it 6 PM" becomes "6 PM").
30
- 3. Output ONLY the final, polished instructions. Do not include introductory text, explanations, or quotes.
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
- Keep the user's wording and voice where it is already clear; do not summarize, expand, or answer the request.`;
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.resolve(approved
236
- ? { behavior: "allow", updatedInput: updatedInput ?? pending.input }
237
- : { behavior: "deny", message: "User denied" });
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jarvis-hq",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
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": {