open-agents-ai 0.187.221 → 0.187.223

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.
Files changed (2) hide show
  1. package/dist/index.js +181 -14
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -317580,6 +317580,10 @@ const conv = document.getElementById('conversation');
317580
317580
  const input = document.getElementById('input-area');
317581
317581
  const sendBtn = document.getElementById('send-btn');
317582
317582
  const modelSelect = document.getElementById('model-select');
317583
+ // Persist the selected model on every change so a browser refresh /
317584
+ // daemon restart can recall it. The change handler is added immediately
317585
+ // because modelSelect already exists in the DOM at this point.
317586
+ modelSelect.addEventListener('change', () => persistSelectedModel());
317583
317587
  const statusEl = document.getElementById('status');
317584
317588
  let apiKey = localStorage.getItem('oa-api-key') || '';
317585
317589
  let streaming = false;
@@ -317619,25 +317623,66 @@ async function checkHealth() {
317619
317623
  }
317620
317624
  }
317621
317625
 
317622
- // Load models
317626
+ // Load models. Recall the previously-selected model in this priority:
317627
+ // 1. Daemon's persisted config.model (server-side, survives daemon restart)
317628
+ // 2. localStorage 'oa.selectedModel' (client-side, survives browser refresh
317629
+ // even before the daemon answers /v1/config)
317630
+ // 3. First model whose name contains "9b" (sensible default)
317631
+ // 4. First model overall
317623
317632
  async function loadModels() {
317624
317633
  try {
317625
- const r = await fetch('/v1/models', { headers: headers() });
317626
- const d = await r.json();
317634
+ const [modelsResp, cfgResp] = await Promise.all([
317635
+ fetch('/v1/models', { headers: headers() }).then(r => r.json()).catch(() => ({})),
317636
+ fetch('/v1/config', { headers: headers() }).then(r => r.json()).catch(() => ({})),
317637
+ ]);
317627
317638
  modelSelect.innerHTML = '';
317628
- for (const m of (d.data || [])) {
317639
+ for (const m of (modelsResp.data || [])) {
317629
317640
  const opt = document.createElement('option');
317630
317641
  // Strip "local/" prefix for cleaner display
317631
317642
  opt.value = m.id.replace(/^local\\//, '');
317632
317643
  opt.textContent = m.id.replace(/^local\\//, '');
317633
317644
  modelSelect.appendChild(opt);
317634
317645
  }
317635
- // Default to first model with "9b" in name, or first overall
317636
- const preferred = Array.from(modelSelect.options).find(o => /9b/i.test(o.value));
317637
- if (preferred) modelSelect.value = preferred.value;
317646
+ // Resolve the recalled model in priority order
317647
+ const daemonModel = (cfgResp && cfgResp.config && cfgResp.config.model) || '';
317648
+ let cachedModel = '';
317649
+ try { cachedModel = localStorage.getItem('oa.selectedModel') || ''; } catch {}
317650
+ const candidates = [daemonModel, cachedModel].filter(Boolean);
317651
+ let recalled = null;
317652
+ for (const c of candidates) {
317653
+ const stripped = c.replace(/^local\\//, '');
317654
+ const hit = Array.from(modelSelect.options).find(o => o.value === stripped);
317655
+ if (hit) { recalled = hit.value; break; }
317656
+ }
317657
+ if (recalled) {
317658
+ modelSelect.value = recalled;
317659
+ } else {
317660
+ // Fall back to first 9b model, then first overall
317661
+ const preferred = Array.from(modelSelect.options).find(o => /9b/i.test(o.value));
317662
+ if (preferred) modelSelect.value = preferred.value;
317663
+ }
317664
+ // Persist the resolved value back to localStorage so a subsequent
317665
+ // refresh (before /v1/config has answered) can recall it instantly.
317666
+ try { localStorage.setItem('oa.selectedModel', modelSelect.value); } catch {}
317638
317667
  } catch { modelSelect.innerHTML = '<option>error loading models</option>'; }
317639
317668
  }
317640
317669
 
317670
+ // Persist user changes immediately and push to the daemon so a daemon
317671
+ // restart also recalls the same model. The change handler is wired
317672
+ // after DOMContentLoaded — see the addEventListener call further down.
317673
+ function persistSelectedModel() {
317674
+ const v = modelSelect.value;
317675
+ if (!v) return;
317676
+ try { localStorage.setItem('oa.selectedModel', v); } catch {}
317677
+ // Best-effort: push to /v1/config/model so the daemon also persists it.
317678
+ // Don't await — we don't want to block the UI on a slow daemon write.
317679
+ fetch('/v1/config/model', {
317680
+ method: 'PUT',
317681
+ headers: headers(),
317682
+ body: JSON.stringify({ model: v }),
317683
+ }).catch(() => { /* offline / read-only — localStorage still saved */ });
317684
+ }
317685
+
317641
317686
  function addMessage(role, content) {
317642
317687
  const div = document.createElement('div');
317643
317688
  div.className = 'msg ' + role;
@@ -317759,6 +317804,45 @@ function escHtml(s) {
317759
317804
  return String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
317760
317805
  }
317761
317806
 
317807
+ // Append text content to a parent element with a "Show more" / "Hide"
317808
+ // toggle when the text exceeds truncateAt characters. The truncated
317809
+ // preview is shown by default; the toggle button sits UNDERNEATH the
317810
+ // content (per user request) and swaps between expand/collapse states.
317811
+ // Returns the wrapper div so callers can chain styles.
317812
+ function appendExpandableContent(parent, fullText, opts) {
317813
+ opts = opts || {};
317814
+ const truncateAt = typeof opts.truncateAt === 'number' ? opts.truncateAt : 500;
317815
+ const monospace = opts.monospace !== false; // default true
317816
+ const baseStyle = (opts.baseStyle || '') +
317817
+ (monospace ? 'font-family:monospace;white-space:pre-wrap;word-break:break-word;' : 'white-space:pre-wrap;');
317818
+ const wrapper = document.createElement('div');
317819
+ wrapper.style.cssText = 'display:flex;flex-direction:column;gap:2px;';
317820
+ const text = String(fullText == null ? '' : fullText);
317821
+ const isLong = text.length > truncateAt;
317822
+
317823
+ const contentEl = document.createElement('div');
317824
+ contentEl.style.cssText = baseStyle;
317825
+ contentEl.textContent = isLong ? text.slice(0, truncateAt) + '\\u2026' : text;
317826
+ wrapper.appendChild(contentEl);
317827
+
317828
+ if (isLong) {
317829
+ const btn = document.createElement('button');
317830
+ btn.type = 'button';
317831
+ btn.style.cssText = 'align-self:flex-start;margin-top:2px;padding:2px 8px;background:#2a2a30;border:1px solid #3a3a42;color:#b2920a;font-size:0.6rem;border-radius:2px;cursor:pointer;font-family:inherit;';
317832
+ btn.textContent = 'Show more (' + text.length + ' chars)';
317833
+ let expanded = false;
317834
+ btn.addEventListener('click', (e) => {
317835
+ e.stopPropagation();
317836
+ expanded = !expanded;
317837
+ contentEl.textContent = expanded ? text : (text.slice(0, truncateAt) + '\\u2026');
317838
+ btn.textContent = expanded ? 'Hide' : 'Show more (' + text.length + ' chars)';
317839
+ });
317840
+ wrapper.appendChild(btn);
317841
+ }
317842
+ parent.appendChild(wrapper);
317843
+ return wrapper;
317844
+ }
317845
+
317762
317846
  async function sendMessage() {
317763
317847
  const text = input.value.trim();
317764
317848
  if (!text || streaming) return;
@@ -317855,6 +317939,27 @@ async function sendMessage() {
317855
317939
  try {
317856
317940
  const chunk = JSON.parse(data);
317857
317941
 
317942
+ // task_complete promotion: surface its summary as the final
317943
+ // assistant message instead of leaving it stuck inside the
317944
+ // tool dropdown. The agent uses task_complete to deliver its
317945
+ // final answer; without this hoist the user sees an empty
317946
+ // assistant bubble and a collapsed details element they have
317947
+ // to expand to read the result.
317948
+ if (chunk.type === 'tool_call' && chunk.tool === 'task_complete') {
317949
+ const ta = (chunk.args && typeof chunk.args === 'object') ? chunk.args : {};
317950
+ const summaryText = (typeof ta.summary === 'string' && ta.summary)
317951
+ || (typeof ta.message === 'string' && ta.message)
317952
+ || (typeof ta.result === 'string' && ta.result)
317953
+ || '';
317954
+ if (summaryText) {
317955
+ if (fullContent && !fullContent.endsWith('\\n')) fullContent += '\\n\\n';
317956
+ fullContent += summaryText;
317957
+ contentDiv.innerHTML = renderMarkdown(fullContent);
317958
+ conv.scrollTop = conv.scrollHeight;
317959
+ }
317960
+ // fall through so the dropdown still renders for inspection
317961
+ }
317962
+
317858
317963
  // Tool call event — show live as expandable section
317859
317964
  if (chunk.type === 'tool_call') {
317860
317965
  chatTools.push(chunk);
@@ -317936,14 +318041,26 @@ async function sendMessage() {
317936
318041
  } else {
317937
318042
  for (const [k, v] of Object.entries(chunk.args)) {
317938
318043
  const row = document.createElement('div');
317939
- row.style.cssText = 'padding:2px 0;display:flex;gap:8px';
318044
+ row.style.cssText = 'padding:2px 0;display:flex;gap:8px;align-items:flex-start';
318045
+ // Convert value to a displayable string with proper typing
317940
318046
  let vs;
317941
318047
  if (v === null || v === undefined) vs = String(v);
317942
318048
  else if (typeof v === 'string') vs = v;
317943
318049
  else if (typeof v === 'number' || typeof v === 'boolean') vs = String(v);
317944
318050
  else { try { vs = JSON.stringify(v, null, 2); } catch { vs = '[object]'; } }
317945
- if (vs.length > 500) vs = vs.slice(0, 497) + '…';
317946
- row.innerHTML = '<span style="color:#b2920a;min-width:60px">' + k + '</span><span style="color:#b0b0b0;word-break:break-all;white-space:pre-wrap">' + escHtml(vs) + '</span>';
318051
+
318052
+ const keyEl = document.createElement('span');
318053
+ keyEl.style.cssText = 'color:#b2920a;min-width:60px;flex-shrink:0';
318054
+ keyEl.textContent = k;
318055
+ row.appendChild(keyEl);
318056
+
318057
+ const valWrap = document.createElement('div');
318058
+ valWrap.style.cssText = 'flex:1;min-width:0;color:#b0b0b0';
318059
+ // Use the show-more helper so long values are collapsed
318060
+ // by default and the user can expand inline.
318061
+ appendExpandableContent(valWrap, vs, { truncateAt: 500, baseStyle: 'color:#b0b0b0;' });
318062
+ row.appendChild(valWrap);
318063
+
317947
318064
  argsDiv.appendChild(row);
317948
318065
  }
317949
318066
  }
@@ -317954,11 +318071,13 @@ async function sendMessage() {
317954
318071
  continue;
317955
318072
  }
317956
318073
 
317957
- // Tool result
318074
+ // Tool result — render with show-more/hide so the user can
318075
+ // expand to see the FULL output instead of being capped at
318076
+ // 150 chars. The button sits underneath the result block.
317958
318077
  if (chunk.type === 'tool_result') {
317959
318078
  const resultEl = document.createElement('div');
317960
- resultEl.style.cssText = 'background:#1e1e22;padding:2px 8px 2px 18px;margin:0 0 2px 0;color:#555;font-size:0.65rem;max-height:60px;overflow:hidden';
317961
- resultEl.textContent = (chunk.output || '').slice(0, 150);
318079
+ resultEl.style.cssText = 'background:#1e1e22;padding:4px 8px 4px 18px;margin:0 0 2px 0;color:#888;font-size:0.65rem';
318080
+ appendExpandableContent(resultEl, chunk.output || '', { truncateAt: 150, baseStyle: 'color:#888;' });
317962
318081
  toolsContainer.appendChild(resultEl);
317963
318082
  continue;
317964
318083
  }
@@ -322452,6 +322571,15 @@ ${task}` : task;
322452
322571
  done: false,
322453
322572
  _oa: { type: "tool_call", tool: evt.tool, args: evt.args }
322454
322573
  }) + "\n");
322574
+ try {
322575
+ const evtType = evt.tool === "sub_agent" || evt.tool === "full_sub_agent" ? "run.started" : "tool.called";
322576
+ publishEvent(evtType, {
322577
+ run_id: `ollama-${evt.tool}-${Date.now()}`,
322578
+ tool: evt.tool,
322579
+ parent: "chat"
322580
+ });
322581
+ } catch {
322582
+ }
322455
322583
  } else {
322456
322584
  finalLines.push(line);
322457
322585
  }
@@ -323757,6 +323885,16 @@ ${historyLines}
323757
323885
  tool: evt.tool,
323758
323886
  args: evt.args
323759
323887
  }) + "\n\n");
323888
+ try {
323889
+ const evtType = evt.tool === "sub_agent" || evt.tool === "full_sub_agent" ? "run.started" : "tool.called";
323890
+ publishEvent(evtType, {
323891
+ run_id: `${session.id.slice(0, 12)}-${evt.tool}-${Date.now()}`,
323892
+ tool: evt.tool,
323893
+ session_id: session.id,
323894
+ parent: "chat"
323895
+ }, { subject: session.id });
323896
+ } catch {
323897
+ }
323760
323898
  } else {
323761
323899
  finalLines.push(line);
323762
323900
  }
@@ -324768,7 +324906,7 @@ function createTaskCompleteTool(modelTier) {
324768
324906
  const summaryDesc = modelTier === "small" || modelTier === "medium" ? "Your complete response to the user. For questions/chat: put your FULL answer here (this is what the user will see). For coding tasks: brief summary of what was accomplished." : "Brief summary of what was accomplished";
324769
324907
  return {
324770
324908
  name: "task_complete",
324771
- description: "Signal that the task is complete.",
324909
+ description: "Signal that the task is complete. GUARDED: cannot fire while the active todo list (todo_write) has pending, in_progress, or blocked items. If you're truly done, first call todo_write to mark every remaining item completed. If you're not done, continue working down the list and call this only after the last item flips to completed.",
324772
324910
  parameters: {
324773
324911
  type: "object",
324774
324912
  properties: {
@@ -324777,6 +324915,35 @@ function createTaskCompleteTool(modelTier) {
324777
324915
  required: ["summary"]
324778
324916
  },
324779
324917
  async execute(args) {
324918
+ try {
324919
+ const sessionId = getTodoSessionId();
324920
+ const todos = readTodos(sessionId);
324921
+ if (todos.length > 0) {
324922
+ const incomplete = todos.filter(
324923
+ (t2) => t2.status === "pending" || t2.status === "in_progress" || t2.status === "blocked"
324924
+ );
324925
+ if (incomplete.length > 0) {
324926
+ const incompleteList = incomplete.slice(0, 10).map((t2) => ` - [${t2.status}] ${t2.content}${t2.blocker ? ` (blocked: ${t2.blocker})` : ""}`).join("\n");
324927
+ const more = incomplete.length > 10 ? `
324928
+ ... +${incomplete.length - 10} more` : "";
324929
+ return {
324930
+ success: false,
324931
+ output: "",
324932
+ error: `task_complete BLOCKED — ${incomplete.length} todo item(s) still incomplete. You must either:
324933
+ 1. Continue working on the remaining items, OR
324934
+ 2. If they're actually done, call todo_write with status='completed' for each one, THEN call task_complete again.
324935
+
324936
+ Incomplete items:
324937
+ ${incompleteList}${more}`
324938
+ };
324939
+ }
324940
+ try {
324941
+ writeTodos(sessionId, []);
324942
+ } catch {
324943
+ }
324944
+ }
324945
+ } catch {
324946
+ }
324780
324947
  return { success: true, output: args["summary"] || "Task completed." };
324781
324948
  }
324782
324949
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.187.221",
3
+ "version": "0.187.223",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",