open-research 0.1.2 → 0.1.4

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/cli.js CHANGED
@@ -1,8 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
2
8
 
3
9
  // src/cli.ts
4
10
  import React4 from "react";
5
- import path18 from "path";
11
+ import path19 from "path";
6
12
  import { Command } from "commander";
7
13
  import { render } from "ink";
8
14
 
@@ -848,7 +854,7 @@ async function ensureOpenResearchConfig(options) {
848
854
  }
849
855
 
850
856
  // src/tui/app.tsx
851
- import path17 from "path";
857
+ import path18 from "path";
852
858
  import {
853
859
  startTransition,
854
860
  useDeferredValue,
@@ -1773,11 +1779,17 @@ function createOpenAIAuthProvider(credentials, onTokenRefresh, onValidationChang
1773
1779
  } else if (event.type === "response.completed") {
1774
1780
  const resp = event.data.response;
1775
1781
  if (resp?.usage) {
1776
- const usage = resp.usage;
1782
+ const u = resp.usage;
1783
+ const inputDetails = u.input_tokens_details;
1784
+ const outputDetails = u.output_tokens_details;
1785
+ const inputTokens = u.input_tokens ?? 0;
1786
+ const outputTokens = u.output_tokens ?? 0;
1777
1787
  usageData = {
1778
- promptTokens: usage.input_tokens ?? 0,
1779
- completionTokens: usage.output_tokens ?? 0,
1780
- totalTokens: (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0)
1788
+ promptTokens: inputTokens,
1789
+ completionTokens: outputTokens,
1790
+ totalTokens: u.total_tokens ?? inputTokens + outputTokens,
1791
+ cachedTokens: inputDetails?.cached_tokens ?? 0,
1792
+ reasoningTokens: outputDetails?.reasoning_tokens ?? 0
1781
1793
  };
1782
1794
  }
1783
1795
  if (resp?.model) {
@@ -1889,11 +1901,17 @@ function createOpenAIAuthProvider(credentials, onTokenRefresh, onValidationChang
1889
1901
  case "response.completed": {
1890
1902
  const resp = event.data.response;
1891
1903
  if (resp?.usage) {
1892
- const responseUsage = resp.usage;
1904
+ const u = resp.usage;
1905
+ const inputDetails = u.input_tokens_details;
1906
+ const outputDetails = u.output_tokens_details;
1907
+ const inputTokens = u.input_tokens ?? 0;
1908
+ const outputTokens = u.output_tokens ?? 0;
1893
1909
  usage = {
1894
- promptTokens: responseUsage.input_tokens ?? 0,
1895
- completionTokens: responseUsage.output_tokens ?? 0,
1896
- totalTokens: (responseUsage.input_tokens ?? 0) + (responseUsage.output_tokens ?? 0)
1910
+ promptTokens: inputTokens,
1911
+ completionTokens: outputTokens,
1912
+ totalTokens: u.total_tokens ?? inputTokens + outputTokens,
1913
+ cachedTokens: inputDetails?.cached_tokens ?? 0,
1914
+ reasoningTokens: outputDetails?.reasoning_tokens ?? 0
1897
1915
  };
1898
1916
  }
1899
1917
  break;
@@ -4501,35 +4519,65 @@ var MODEL_CONTEXT_WINDOWS = {
4501
4519
  "o4-mini": 2e5
4502
4520
  };
4503
4521
  var DEFAULT_CONTEXT_WINDOW = 128e3;
4504
- var COMPACT_THRESHOLD_PERCENT = 0.9;
4522
+ var AUTO_COMPACT_TOKEN_LIMIT = 25e4;
4505
4523
  function getContextWindow(model) {
4506
4524
  return MODEL_CONTEXT_WINDOWS[model] ?? DEFAULT_CONTEXT_WINDOW;
4507
4525
  }
4508
4526
  function getCompactThreshold(model) {
4509
- return Math.floor(getContextWindow(model) * COMPACT_THRESHOLD_PERCENT);
4527
+ const window = getContextWindow(model);
4528
+ return window > AUTO_COMPACT_TOKEN_LIMIT ? AUTO_COMPACT_TOKEN_LIMIT : Math.floor(window * 0.8);
4529
+ }
4530
+ function emptyBreakdown() {
4531
+ return { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 }, total: 0 };
4510
4532
  }
4511
4533
  function createSessionUsage() {
4512
4534
  return {
4535
+ cumulative: emptyBreakdown(),
4536
+ lastTurn: emptyBreakdown(),
4537
+ estimatedCurrentTokens: 0,
4538
+ compactionCount: 0,
4513
4539
  inputTokens: 0,
4514
4540
  outputTokens: 0,
4515
4541
  totalTokens: 0,
4516
- lastTurnTokens: 0,
4517
- estimatedCurrentTokens: 0,
4518
- compactionCount: 0
4542
+ lastTurnTokens: 0
4519
4543
  };
4520
4544
  }
4521
4545
  function updateUsageFromApi(usage, apiUsage) {
4522
- usage.inputTokens += apiUsage.promptTokens;
4523
- usage.outputTokens += apiUsage.completionTokens;
4524
- usage.totalTokens += apiUsage.totalTokens;
4546
+ const cached = apiUsage.cachedTokens ?? 0;
4547
+ const reasoning = apiUsage.reasoningTokens ?? 0;
4548
+ const adjustedInput = Math.max(0, apiUsage.promptTokens - cached);
4549
+ const adjustedOutput = Math.max(0, apiUsage.completionTokens - reasoning);
4550
+ usage.cumulative.input += adjustedInput;
4551
+ usage.cumulative.output += adjustedOutput;
4552
+ usage.cumulative.reasoning += reasoning;
4553
+ usage.cumulative.cache.read += cached;
4554
+ usage.cumulative.total += apiUsage.totalTokens;
4555
+ usage.lastTurn = {
4556
+ input: adjustedInput,
4557
+ output: adjustedOutput,
4558
+ reasoning,
4559
+ cache: { read: cached, write: 0 },
4560
+ total: apiUsage.totalTokens
4561
+ };
4562
+ usage.inputTokens = usage.cumulative.input;
4563
+ usage.outputTokens = usage.cumulative.output;
4564
+ usage.totalTokens = usage.cumulative.total;
4525
4565
  usage.lastTurnTokens = apiUsage.totalTokens;
4526
4566
  }
4527
4567
  var PRUNE_PROTECT_TOKENS = 4e4;
4528
4568
  var PRUNE_MIN_SAVINGS = 2e4;
4569
+ var PRUNE_SKIP_RECENT_USER_TURNS = 2;
4529
4570
  function pruneToolOutputs(messages) {
4571
+ const userIndices = [];
4572
+ for (let i = messages.length - 1; i >= 0; i--) {
4573
+ if (messages[i].role === "user") userIndices.push(i);
4574
+ }
4575
+ const protectBoundary = userIndices.length >= PRUNE_SKIP_RECENT_USER_TURNS ? userIndices[PRUNE_SKIP_RECENT_USER_TURNS - 1] : 0;
4530
4576
  const toolIndices = [];
4531
4577
  for (let i = 0; i < messages.length; i++) {
4532
- if (messages[i].role === "tool") toolIndices.push(i);
4578
+ if (messages[i].role === "tool" && i < protectBoundary) {
4579
+ toolIndices.push(i);
4580
+ }
4533
4581
  }
4534
4582
  if (toolIndices.length === 0) return { messages, savedTokens: 0 };
4535
4583
  let protectedTokens = 0;
@@ -4549,44 +4597,105 @@ function pruneToolOutputs(messages) {
4549
4597
  const idx = toolIndices[i];
4550
4598
  const msg = result[idx];
4551
4599
  const oldTokens = estimateMessageTokens(msg);
4552
- const pruned = "[output pruned \u2014 use read_file to re-read if needed]";
4553
- savedTokens += oldTokens - estimateTokens(pruned);
4554
- result[idx] = { ...msg, content: pruned };
4600
+ const stub = "[output pruned \u2014 use read_file to re-read if needed]";
4601
+ savedTokens += oldTokens - estimateTokens(stub);
4602
+ result[idx] = { ...msg, content: stub };
4555
4603
  }
4556
4604
  if (savedTokens < PRUNE_MIN_SAVINGS) {
4557
4605
  return { messages, savedTokens: 0 };
4558
4606
  }
4559
4607
  return { messages: result, savedTokens };
4560
4608
  }
4561
- async function compactConversation(messages, provider, model, signal) {
4609
+ var COMPACTION_SYSTEM_PROMPT = `You are a conversation summarizer for a research agent. Your job is to create a handoff summary that another agent instance can use to seamlessly continue the work.
4610
+
4611
+ Do not respond to any questions in the conversation. Only output the summary.
4612
+ Respond in the same language the user used.`;
4613
+ var COMPACTION_USER_TEMPLATE = `Provide a detailed summary of our conversation above for handoff to another agent that will continue the work.
4614
+
4615
+ Stick to this template:
4616
+
4617
+ ## Goal
4618
+ [What is the user trying to accomplish? Be specific.]
4619
+
4620
+ ## Instructions
4621
+ - [Important instructions or preferences the user gave]
4622
+ - [Research methodology constraints or requirements]
4623
+ - [If there is a research charter or plan, summarize its key points]
4624
+
4625
+ ## Discoveries
4626
+ - [Key findings from paper searches, data analysis, or experiments]
4627
+ - [Important facts, numbers, or evidence discovered]
4628
+ - [Any surprising or contradicting results]
4629
+
4630
+ ## Accomplished
4631
+ - [What work has been completed]
4632
+ - [What is currently in progress]
4633
+ - [What remains to be done]
4634
+
4635
+ ## Relevant Files
4636
+ [List workspace files that were read, created, or modified. Include what each contains.]
4637
+ - path/to/file.md \u2014 description of contents
4638
+ - experiments/script.py \u2014 what it does and its results
4639
+
4640
+ ## Active Context
4641
+ - [Current research question or hypothesis being investigated]
4642
+ - [Which skills are active]
4643
+ - [Any pending user decisions or questions]
4644
+
4645
+ ## Next Steps
4646
+ 1. [Most immediate next action]
4647
+ 2. [Following action]
4648
+ 3. [And so on]
4649
+
4650
+ {CUSTOM_INSTRUCTIONS}`;
4651
+ async function compactConversation(messages, provider, model, customInstructions, signal) {
4562
4652
  const systemMsg = messages.find((m) => m.role === "system");
4563
4653
  const conversationMsgs = messages.filter((m) => m.role !== "system");
4564
4654
  const conversationText = conversationMsgs.map((m) => {
4565
4655
  const role = m.role === "assistant" ? "Agent" : m.role === "user" ? "User" : "Tool";
4566
- const content = typeof m.content === "string" ? m.content : m.content ? JSON.stringify(m.content) : "[tool calls]";
4567
- return `[${role}]: ${content?.slice(0, 2e3)}`;
4656
+ let content;
4657
+ if (typeof m.content === "string") {
4658
+ content = m.content.length > 3e3 ? m.content.slice(0, 3e3) + "\n[... truncated]" : m.content;
4659
+ } else if (m.content) {
4660
+ content = JSON.stringify(m.content).slice(0, 1e3);
4661
+ } else if (m.tool_calls?.length) {
4662
+ content = m.tool_calls.map((tc) => `[tool: ${tc.function.name}]`).join(", ");
4663
+ } else {
4664
+ content = "[empty]";
4665
+ }
4666
+ return `[${role}]: ${content}`;
4568
4667
  }).join("\n\n");
4668
+ const customBlock = customInstructions ? `
4669
+
4670
+ Additional instructions: ${customInstructions}` : "";
4671
+ const userPrompt = COMPACTION_USER_TEMPLATE.replace("{CUSTOM_INSTRUCTIONS}", customBlock);
4672
+ const compactionModel = model.includes("5.4") ? "gpt-5.4-mini" : model;
4569
4673
  const summaryResponse = await provider.callLLM({
4570
4674
  messages: [
4571
- {
4572
- role: "system",
4573
- content: "You are performing a CONTEXT COMPACTION. Summarize the conversation into a concise handoff document. Include:\n1. **Goal**: What the user is trying to accomplish\n2. **Key discoveries**: Important findings, file paths, data points\n3. **Work completed**: What has been done so far\n4. **Next steps**: What should happen next\n5. **Active files**: Key file paths and their contents summary\n\nBe concise but preserve all actionable information. This summary will replace the full conversation history."
4574
- },
4675
+ { role: "system", content: COMPACTION_SYSTEM_PROMPT },
4575
4676
  {
4576
4677
  role: "user",
4577
- content: `Summarize this conversation:
4678
+ content: `Here is the conversation to summarize:
4578
4679
 
4579
- ${conversationText.slice(0, 1e5)}`
4680
+ ${conversationText.slice(0, 12e4)}
4681
+
4682
+ ---
4683
+
4684
+ ${userPrompt}`
4580
4685
  }
4581
4686
  ],
4582
- model,
4687
+ model: compactionModel,
4583
4688
  maxTokens: 4096
4584
4689
  });
4585
4690
  const compacted = [];
4586
4691
  if (systemMsg) compacted.push(systemMsg);
4587
4692
  compacted.push({
4588
4693
  role: "user",
4589
- content: "[Context compacted \u2014 previous conversation summarized below]\n\n" + summaryResponse.content
4694
+ content: "What have we accomplished so far in this research session?"
4695
+ });
4696
+ compacted.push({
4697
+ role: "assistant",
4698
+ content: summaryResponse.content
4590
4699
  });
4591
4700
  return compacted;
4592
4701
  }
@@ -4604,7 +4713,17 @@ async function maybeCompact(messages, model, provider, usage, signal) {
4604
4713
  usage.compactionCount++;
4605
4714
  return { messages: pruned, didCompact: true };
4606
4715
  }
4607
- const compacted = await compactConversation(pruned, provider, model, signal);
4716
+ const compacted = await compactConversation(pruned, provider, model, void 0, signal);
4717
+ usage.estimatedCurrentTokens = estimateConversationTokens(compacted);
4718
+ usage.compactionCount++;
4719
+ return { messages: compacted, didCompact: true };
4720
+ }
4721
+ async function manualCompact(messages, model, provider, usage, customInstructions, signal) {
4722
+ if (messages.length <= 2) {
4723
+ return { messages, didCompact: false };
4724
+ }
4725
+ const { messages: pruned } = pruneToolOutputs(messages);
4726
+ const compacted = await compactConversation(pruned, provider, model, customInstructions, signal);
4608
4727
  usage.estimatedCurrentTokens = estimateConversationTokens(compacted);
4609
4728
  usage.compactionCount++;
4610
4729
  return { messages: compacted, didCompact: true };
@@ -5294,6 +5413,324 @@ async function checkForUpdate() {
5294
5413
  }
5295
5414
  }
5296
5415
 
5416
+ // src/lib/preview/server.ts
5417
+ import http2 from "http";
5418
+ import fs18 from "fs";
5419
+ import path17 from "path";
5420
+
5421
+ // src/lib/preview/latex-to-html.ts
5422
+ function latexToHtml(latex) {
5423
+ let body = latex;
5424
+ const docMatch = body.match(/\\begin\{document\}([\s\S]*?)\\end\{document\}/);
5425
+ if (docMatch) body = docMatch[1];
5426
+ const titleMatch = latex.match(/\\title\{([^}]*)\}/);
5427
+ const authorMatch = latex.match(/\\author\{([^}]*)\}/);
5428
+ const dateMatch = latex.match(/\\date\{([^}]*)\}/);
5429
+ const abstractMatch = body.match(/\\begin\{abstract\}([\s\S]*?)\\end\{abstract\}/);
5430
+ body = body.replace(/\\maketitle/, "");
5431
+ body = body.replace(/\\begin\{abstract\}[\s\S]*?\\end\{abstract\}/, "");
5432
+ body = body.replace(/\\section\*?\{([^}]*)\}/g, '<h2 class="section">$1</h2>');
5433
+ body = body.replace(/\\subsection\*?\{([^}]*)\}/g, '<h3 class="subsection">$1</h3>');
5434
+ body = body.replace(/\\subsubsection\*?\{([^}]*)\}/g, '<h4 class="subsubsection">$1</h4>');
5435
+ body = body.replace(/\\paragraph\{([^}]*)\}/g, '<h5 class="paragraph">$1</h5>');
5436
+ body = body.replace(/\\textbf\{([^}]*)\}/g, "<strong>$1</strong>");
5437
+ body = body.replace(/\\textit\{([^}]*)\}/g, "<em>$1</em>");
5438
+ body = body.replace(/\\texttt\{([^}]*)\}/g, "<code>$1</code>");
5439
+ body = body.replace(/\\emph\{([^}]*)\}/g, "<em>$1</em>");
5440
+ body = body.replace(/\\underline\{([^}]*)\}/g, "<u>$1</u>");
5441
+ body = body.replace(/\\cite\{([^}]*)\}/g, '<span class="citation">[$1]</span>');
5442
+ body = body.replace(/\\citep\{([^}]*)\}/g, '<span class="citation">($1)</span>');
5443
+ body = body.replace(/\\citet\{([^}]*)\}/g, '<span class="citation">$1</span>');
5444
+ body = body.replace(/\\ref\{([^}]*)\}/g, '<span class="ref">[ref:$1]</span>');
5445
+ body = body.replace(/\\label\{([^}]*)\}/g, "");
5446
+ body = body.replace(/\\\[([\s\S]*?)\\\]/g, '<div class="math-display">\\[$1\\]</div>');
5447
+ body = body.replace(/\$\$([\s\S]*?)\$\$/g, '<div class="math-display">\\[$1\\]</div>');
5448
+ body = body.replace(
5449
+ /\\begin\{equation\*?\}([\s\S]*?)\\end\{equation\*?\}/g,
5450
+ '<div class="math-display">\\[$1\\]</div>'
5451
+ );
5452
+ body = body.replace(
5453
+ /\\begin\{align\*?\}([\s\S]*?)\\end\{align\*?\}/g,
5454
+ '<div class="math-display">\\[$1\\]</div>'
5455
+ );
5456
+ body = body.replace(/(?<!\$)\$(?!\$)([^$]+?)\$(?!\$)/g, '<span class="math-inline">\\($1\\)</span>');
5457
+ body = body.replace(/\\begin\{itemize\}/g, "<ul>");
5458
+ body = body.replace(/\\end\{itemize\}/g, "</ul>");
5459
+ body = body.replace(/\\begin\{enumerate\}/g, "<ol>");
5460
+ body = body.replace(/\\end\{enumerate\}/g, "</ol>");
5461
+ body = body.replace(/\\item\s*/g, "<li>");
5462
+ body = body.replace(
5463
+ /\\begin\{quote\}([\s\S]*?)\\end\{quote\}/g,
5464
+ "<blockquote>$1</blockquote>"
5465
+ );
5466
+ body = body.replace(
5467
+ /\\begin\{verbatim\}([\s\S]*?)\\end\{verbatim\}/g,
5468
+ "<pre><code>$1</code></pre>"
5469
+ );
5470
+ body = body.replace(
5471
+ /\\begin\{figure\}[\s\S]*?\\caption\{([^}]*)\}[\s\S]*?\\end\{figure\}/g,
5472
+ '<figure class="figure-placeholder"><figcaption>$1</figcaption></figure>'
5473
+ );
5474
+ body = body.replace(
5475
+ /\\begin\{table\}[\s\S]*?\\caption\{([^}]*)\}[\s\S]*?\\end\{table\}/g,
5476
+ '<figure class="table-placeholder"><figcaption>Table: $1</figcaption></figure>'
5477
+ );
5478
+ body = body.replace(/\\footnote\{([^}]*)\}/g, '<sup class="footnote" title="$1">[*]</sup>');
5479
+ body = body.replace(/\\bibliography\{[^}]*\}/g, "");
5480
+ body = body.replace(/\\bibliographystyle\{[^}]*\}/g, "");
5481
+ body = body.replace(/\\usepackage\{[^}]*\}/g, "");
5482
+ body = body.replace(/\\documentclass[^{]*\{[^}]*\}/g, "");
5483
+ body = body.replace(/\\begin\{document\}/g, "");
5484
+ body = body.replace(/\\end\{document\}/g, "");
5485
+ body = body.replace(/\\newcommand[^{]*\{[^}]*\}\{[^}]*\}/g, "");
5486
+ body = body.replace(/\\\\/g, "<br>");
5487
+ body = body.replace(/\\newline/g, "<br>");
5488
+ body = body.replace(/\\noindent\s*/g, "");
5489
+ body = body.replace(/\\vspace\{[^}]*\}/g, "");
5490
+ body = body.replace(/\\hspace\{[^}]*\}/g, "");
5491
+ body = body.replace(/\n\s*\n/g, "</p><p>");
5492
+ body = `<p>${body}</p>`;
5493
+ body = body.replace(/<p>\s*<\/p>/g, "");
5494
+ body = body.replace(/<p>\s*<(h[2-5])/g, "<$1");
5495
+ body = body.replace(/<\/(h[2-5])>\s*<\/p>/g, "</$1>");
5496
+ const titleHtml = titleMatch ? `<h1 class="title">${titleMatch[1]}</h1>` : "";
5497
+ const authorHtml = authorMatch ? `<p class="author">${authorMatch[1]}</p>` : "";
5498
+ const dateHtml = dateMatch ? `<p class="date">${dateMatch[1]}</p>` : "";
5499
+ const abstractHtml = abstractMatch ? `<div class="abstract"><h3>Abstract</h3><p>${abstractMatch[1].trim()}</p></div>` : "";
5500
+ return `${titleHtml}${authorHtml}${dateHtml}${abstractHtml}${body}`;
5501
+ }
5502
+
5503
+ // src/lib/preview/server.ts
5504
+ var HTML_TEMPLATE = `<!DOCTYPE html>
5505
+ <html lang="en">
5506
+ <head>
5507
+ <meta charset="UTF-8">
5508
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
5509
+ <title>Open Research \u2014 LaTeX Preview</title>
5510
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
5511
+ <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
5512
+ <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js"
5513
+ onload="renderMathInElement(document.body, {
5514
+ delimiters: [
5515
+ {left: '\\\\[', right: '\\\\]', display: true},
5516
+ {left: '\\\\(', right: '\\\\)', display: false}
5517
+ ],
5518
+ throwOnError: false
5519
+ });">
5520
+ </script>
5521
+ <style>
5522
+ :root {
5523
+ --bg: #1a1a2e;
5524
+ --surface: #16213e;
5525
+ --text: #e0e0e0;
5526
+ --text-dim: #8892b0;
5527
+ --accent: #64ffda;
5528
+ --heading: #ccd6f6;
5529
+ --citation: #64ffda;
5530
+ --border: #233554;
5531
+ }
5532
+ * { margin: 0; padding: 0; box-sizing: border-box; }
5533
+ body {
5534
+ font-family: 'Charter', 'Georgia', 'Times New Roman', serif;
5535
+ background: var(--bg);
5536
+ color: var(--text);
5537
+ line-height: 1.8;
5538
+ max-width: 780px;
5539
+ margin: 0 auto;
5540
+ padding: 3rem 2rem;
5541
+ }
5542
+ h1.title {
5543
+ font-size: 2rem;
5544
+ color: var(--heading);
5545
+ text-align: center;
5546
+ margin-bottom: 0.5rem;
5547
+ line-height: 1.3;
5548
+ }
5549
+ .author {
5550
+ text-align: center;
5551
+ color: var(--text-dim);
5552
+ font-style: italic;
5553
+ margin-bottom: 0.3rem;
5554
+ }
5555
+ .date {
5556
+ text-align: center;
5557
+ color: var(--text-dim);
5558
+ margin-bottom: 2rem;
5559
+ }
5560
+ .abstract {
5561
+ background: var(--surface);
5562
+ border-left: 3px solid var(--accent);
5563
+ padding: 1.2rem 1.5rem;
5564
+ margin: 2rem 0;
5565
+ border-radius: 0 4px 4px 0;
5566
+ }
5567
+ .abstract h3 {
5568
+ color: var(--accent);
5569
+ font-size: 0.9rem;
5570
+ text-transform: uppercase;
5571
+ letter-spacing: 0.1em;
5572
+ margin-bottom: 0.5rem;
5573
+ }
5574
+ h2.section {
5575
+ font-size: 1.4rem;
5576
+ color: var(--heading);
5577
+ margin: 2.5rem 0 1rem;
5578
+ padding-bottom: 0.3rem;
5579
+ border-bottom: 1px solid var(--border);
5580
+ }
5581
+ h3.subsection {
5582
+ font-size: 1.15rem;
5583
+ color: var(--heading);
5584
+ margin: 1.8rem 0 0.8rem;
5585
+ }
5586
+ h4.subsubsection {
5587
+ font-size: 1rem;
5588
+ color: var(--text-dim);
5589
+ margin: 1.2rem 0 0.5rem;
5590
+ }
5591
+ p { margin: 0.8rem 0; }
5592
+ strong { color: var(--heading); }
5593
+ code {
5594
+ background: var(--surface);
5595
+ padding: 0.15rem 0.4rem;
5596
+ border-radius: 3px;
5597
+ font-size: 0.9em;
5598
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
5599
+ }
5600
+ pre {
5601
+ background: var(--surface);
5602
+ padding: 1rem;
5603
+ border-radius: 4px;
5604
+ overflow-x: auto;
5605
+ margin: 1rem 0;
5606
+ }
5607
+ pre code { background: none; padding: 0; }
5608
+ blockquote {
5609
+ border-left: 3px solid var(--border);
5610
+ padding-left: 1rem;
5611
+ color: var(--text-dim);
5612
+ font-style: italic;
5613
+ margin: 1rem 0;
5614
+ }
5615
+ ul, ol { padding-left: 1.5rem; margin: 0.8rem 0; }
5616
+ li { margin: 0.3rem 0; }
5617
+ .citation {
5618
+ color: var(--citation);
5619
+ font-weight: 500;
5620
+ cursor: help;
5621
+ }
5622
+ .ref { color: var(--accent); font-style: italic; }
5623
+ .footnote { color: var(--accent); cursor: help; }
5624
+ .math-display {
5625
+ margin: 1.2rem 0;
5626
+ overflow-x: auto;
5627
+ text-align: center;
5628
+ }
5629
+ .figure-placeholder, .table-placeholder {
5630
+ background: var(--surface);
5631
+ border: 1px dashed var(--border);
5632
+ padding: 2rem;
5633
+ margin: 1.5rem 0;
5634
+ text-align: center;
5635
+ border-radius: 4px;
5636
+ }
5637
+ .figure-placeholder::before { content: '[Figure placeholder]'; display: block; color: var(--text-dim); margin-bottom: 0.5rem; }
5638
+ .table-placeholder::before { content: '[Table placeholder]'; display: block; color: var(--text-dim); margin-bottom: 0.5rem; }
5639
+ figcaption { font-style: italic; color: var(--text-dim); font-size: 0.9rem; }
5640
+
5641
+ /* Live reload indicator */
5642
+ .live-badge {
5643
+ position: fixed;
5644
+ top: 1rem;
5645
+ right: 1rem;
5646
+ background: #0d7337;
5647
+ color: white;
5648
+ padding: 0.3rem 0.8rem;
5649
+ border-radius: 20px;
5650
+ font-size: 0.75rem;
5651
+ font-family: sans-serif;
5652
+ opacity: 0.8;
5653
+ }
5654
+ .live-badge.disconnected { background: #7d3030; }
5655
+ </style>
5656
+ </head>
5657
+ <body>
5658
+ <div class="live-badge" id="status">LIVE</div>
5659
+ <div id="content">
5660
+ {{CONTENT}}
5661
+ </div>
5662
+ <script>
5663
+ // Auto-reload via polling (simple, no WebSocket dependency)
5664
+ let lastHash = "";
5665
+ async function checkForUpdates() {
5666
+ try {
5667
+ const res = await fetch("/__hash");
5668
+ const hash = await res.text();
5669
+ if (lastHash && hash !== lastHash) {
5670
+ location.reload();
5671
+ }
5672
+ lastHash = hash;
5673
+ document.getElementById("status").textContent = "LIVE";
5674
+ document.getElementById("status").className = "live-badge";
5675
+ } catch {
5676
+ document.getElementById("status").textContent = "DISCONNECTED";
5677
+ document.getElementById("status").className = "live-badge disconnected";
5678
+ }
5679
+ }
5680
+ setInterval(checkForUpdates, 1000);
5681
+ checkForUpdates();
5682
+ </script>
5683
+ </body>
5684
+ </html>`;
5685
+ function startPreviewServer(texPath) {
5686
+ const resolved = path17.resolve(texPath);
5687
+ let currentHash = "";
5688
+ function getContentHash() {
5689
+ try {
5690
+ const content = fs18.readFileSync(resolved, "utf8");
5691
+ return `${content.length}-${content.slice(0, 100)}-${content.slice(-100)}`;
5692
+ } catch {
5693
+ return "error";
5694
+ }
5695
+ }
5696
+ function renderPage() {
5697
+ try {
5698
+ const latex = fs18.readFileSync(resolved, "utf8");
5699
+ const htmlContent = latexToHtml(latex);
5700
+ currentHash = getContentHash();
5701
+ return HTML_TEMPLATE.replace("{{CONTENT}}", htmlContent);
5702
+ } catch (err) {
5703
+ return HTML_TEMPLATE.replace(
5704
+ "{{CONTENT}}",
5705
+ `<p style="color: #ff6b6b;">Error reading ${resolved}: ${err instanceof Error ? err.message : String(err)}</p>`
5706
+ );
5707
+ }
5708
+ }
5709
+ return new Promise((resolve) => {
5710
+ const server = http2.createServer((req, res) => {
5711
+ if (req.url === "/__hash") {
5712
+ const hash = getContentHash();
5713
+ res.writeHead(200, { "Content-Type": "text/plain", "Cache-Control": "no-cache" });
5714
+ res.end(hash);
5715
+ return;
5716
+ }
5717
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" });
5718
+ res.end(renderPage());
5719
+ });
5720
+ server.listen(0, "127.0.0.1", () => {
5721
+ const addr = server.address();
5722
+ if (!addr || typeof addr === "string") return;
5723
+ const port = addr.port;
5724
+ const url = `http://127.0.0.1:${port}`;
5725
+ resolve({
5726
+ url,
5727
+ port,
5728
+ close: () => server.close()
5729
+ });
5730
+ });
5731
+ });
5732
+ }
5733
+
5297
5734
  // src/tui/commands.ts
5298
5735
  var SLASH_COMMANDS = [
5299
5736
  { name: "auth", aliases: ["/connect", "/login"], description: "Connect your OpenAI account via browser OAuth", category: "auth" },
@@ -5306,6 +5743,14 @@ var SLASH_COMMANDS = [
5306
5743
  { name: "clear", aliases: ["/new"], description: "Clear conversation and start fresh", category: "session" },
5307
5744
  { name: "help", aliases: ["/commands"], description: "Show available commands", category: "system" },
5308
5745
  { name: "config", aliases: ["/settings"], description: "View or change settings (e.g. /config theme dark)", category: "system" },
5746
+ { name: "compact", aliases: [], description: "Manually compress conversation to save context (e.g. /compact keep the statistics)", category: "session" },
5747
+ { name: "cost", aliases: ["/tokens", "/usage"], description: "Show token usage and cost for the current session", category: "system" },
5748
+ { name: "context", aliases: [], description: "Show context window usage \u2014 how full it is", category: "system" },
5749
+ { name: "btw", aliases: ["/aside"], description: "Ask a side question without affecting the main conversation", category: "session" },
5750
+ { name: "export", aliases: [], description: "Export conversation as markdown to a file", category: "session" },
5751
+ { name: "diff", aliases: ["/changes"], description: "Show files the agent has changed in this session", category: "workspace" },
5752
+ { name: "doctor", aliases: [], description: "Diagnose auth, connectivity, and tool availability", category: "system" },
5753
+ { name: "preview", aliases: [], description: "Live preview a LaTeX file in browser (e.g. /preview papers/draft.tex)", category: "workspace" },
5309
5754
  { name: "memory", aliases: ["/memories"], description: "View or clear stored memories about you", category: "system" },
5310
5755
  { name: "exit", aliases: ["/quit", "/q"], description: "Exit Open Research", category: "system" }
5311
5756
  ];
@@ -5843,6 +6288,7 @@ function App({
5843
6288
  const deferredPendingUpdates = useDeferredValue(pendingUpdates);
5844
6289
  const activityFrame = useAnimatedFrame(busy);
5845
6290
  const [agentQuestion, setAgentQuestion] = useState3(null);
6291
+ const previewRef = useRef(null);
5846
6292
  const isHome = deferredMessages.length === 0 && !busy;
5847
6293
  const hasWorkspace = workspacePath !== null;
5848
6294
  const hasAuth = authStatus === "connected";
@@ -6132,6 +6578,201 @@ function App({
6132
6578
  addSystemMessage(" Esc unfocus prompt");
6133
6579
  break;
6134
6580
  }
6581
+ case "compact": {
6582
+ if (history.length === 0) {
6583
+ addSystemMessage("Nothing to compact \u2014 conversation is empty.");
6584
+ break;
6585
+ }
6586
+ const customInstructions = args || void 0;
6587
+ addSystemMessage(customInstructions ? `Compacting conversation (preserving: ${customInstructions})...` : "Compacting conversation...");
6588
+ setBusy(true);
6589
+ try {
6590
+ const provider = await createProviderFromStoredAuth({ homeDir });
6591
+ const msgs = [{ role: "system", content: "compaction" }, ...history.map((m) => m)];
6592
+ const { messages: compacted, didCompact } = await manualCompact(
6593
+ msgs,
6594
+ config?.defaults.model ?? "gpt-5.4",
6595
+ provider,
6596
+ sessionTokens,
6597
+ customInstructions
6598
+ );
6599
+ if (didCompact) {
6600
+ const newHistory = compacted.filter((m) => m.role !== "system").map((m) => ({
6601
+ role: m.role,
6602
+ content: m.content
6603
+ }));
6604
+ setHistory(newHistory);
6605
+ const k = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n);
6606
+ setTokenDisplay(`${k(sessionTokens.estimatedCurrentTokens)} ctx \xB7 ${k(sessionTokens.totalTokens)} total`);
6607
+ addSystemMessage(`Compacted. Context reduced to ~${Math.round(sessionTokens.estimatedCurrentTokens / 1e3)}k tokens.`);
6608
+ } else {
6609
+ addSystemMessage("Nothing to compact \u2014 conversation too short.");
6610
+ }
6611
+ } catch (err) {
6612
+ addSystemMessage(`Compaction failed: ${err instanceof Error ? err.message : String(err)}`);
6613
+ } finally {
6614
+ setBusy(false);
6615
+ }
6616
+ break;
6617
+ }
6618
+ case "cost": {
6619
+ const k = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n);
6620
+ const c = sessionTokens.cumulative;
6621
+ addSystemMessage("Session token usage:");
6622
+ addSystemMessage(` Input: ${k(c.input)} tokens`);
6623
+ addSystemMessage(` Output: ${k(c.output)} tokens`);
6624
+ if (c.reasoning > 0) addSystemMessage(` Reasoning: ${k(c.reasoning)} tokens`);
6625
+ if (c.cache.read > 0) addSystemMessage(` Cache read: ${k(c.cache.read)} tokens`);
6626
+ if (c.cache.write > 0) addSystemMessage(` Cache write: ${k(c.cache.write)} tokens`);
6627
+ addSystemMessage(` Total: ${k(c.total)} tokens`);
6628
+ addSystemMessage(` Context: ~${k(sessionTokens.estimatedCurrentTokens)} (current window)`);
6629
+ addSystemMessage(` Compactions: ${sessionTokens.compactionCount}`);
6630
+ break;
6631
+ }
6632
+ case "context": {
6633
+ const model = config?.defaults.model ?? "gpt-5.4";
6634
+ const window = getContextWindow(model);
6635
+ const threshold = getCompactThreshold(model);
6636
+ const current = sessionTokens.estimatedCurrentTokens || estimateConversationTokens(
6637
+ history.map((m) => m)
6638
+ );
6639
+ const pct = Math.round(current / window * 100);
6640
+ const barWidth = 40;
6641
+ const filled = Math.round(pct / 100 * barWidth);
6642
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(barWidth - filled);
6643
+ const color = pct > 90 ? "red" : pct > 70 ? "yellow" : "green";
6644
+ addSystemMessage(`Context window: ${model} (${(window / 1e3).toFixed(0)}k)`);
6645
+ addSystemMessage(` [${bar}] ${pct}%`);
6646
+ addSystemMessage(` ${(current / 1e3).toFixed(1)}k / ${(window / 1e3).toFixed(0)}k tokens used`);
6647
+ addSystemMessage(` Auto-compact at ${(threshold / 1e3).toFixed(0)}k (90%)`);
6648
+ if (pct > 80) {
6649
+ addSystemMessage(" Tip: run /compact to free space, or /clear to start fresh.");
6650
+ }
6651
+ break;
6652
+ }
6653
+ case "btw": {
6654
+ if (!args) {
6655
+ addSystemMessage("Usage: /btw <your side question>");
6656
+ break;
6657
+ }
6658
+ if (!hasAuth) {
6659
+ addSystemMessage("Not connected. Run /auth first.");
6660
+ break;
6661
+ }
6662
+ addSystemMessage(`Side question: ${args}`);
6663
+ setBusy(true);
6664
+ try {
6665
+ const provider = await createProviderFromStoredAuth({ homeDir });
6666
+ const response = await provider.callLLM({
6667
+ messages: [
6668
+ { role: "system", content: "Answer this quick side question concisely. Do not reference any prior conversation." },
6669
+ { role: "user", content: args }
6670
+ ],
6671
+ model: config?.defaults.model ?? "gpt-5.4",
6672
+ maxTokens: 1e3
6673
+ });
6674
+ addSystemMessage(`Answer: ${response.content}`);
6675
+ } catch (err) {
6676
+ addSystemMessage(`Error: ${err instanceof Error ? err.message : String(err)}`);
6677
+ } finally {
6678
+ setBusy(false);
6679
+ }
6680
+ break;
6681
+ }
6682
+ case "export": {
6683
+ const fileName = args?.trim() || "conversation-export.md";
6684
+ const exportPath = __require("path").resolve(workspacePath ?? process.cwd(), fileName);
6685
+ const lines = [`# Open Research \u2014 Conversation Export
6686
+ `];
6687
+ for (const msg of messages) {
6688
+ if (msg.role === "user") lines.push(`## You
6689
+ ${msg.text}
6690
+ `);
6691
+ else if (msg.role === "assistant") lines.push(`## Agent
6692
+ ${msg.text}
6693
+ `);
6694
+ else lines.push(`> ${msg.text}
6695
+ `);
6696
+ }
6697
+ try {
6698
+ const fsModule = __require("fs/promises");
6699
+ await fsModule.writeFile(exportPath, lines.join("\n"), "utf8");
6700
+ addSystemMessage(`Exported ${messages.length} messages to ${exportPath}`);
6701
+ } catch (err) {
6702
+ addSystemMessage(`Export failed: ${err instanceof Error ? err.message : String(err)}`);
6703
+ }
6704
+ break;
6705
+ }
6706
+ case "diff": {
6707
+ if (!workspacePath) {
6708
+ addSystemMessage("No workspace active.");
6709
+ break;
6710
+ }
6711
+ try {
6712
+ const { execSync } = __require("child_process");
6713
+ const gitStatus = execSync("git status --short 2>/dev/null || echo 'Not a git repo'", {
6714
+ cwd: workspacePath,
6715
+ encoding: "utf8"
6716
+ }).trim();
6717
+ if (!gitStatus || gitStatus === "Not a git repo") {
6718
+ addSystemMessage("No changes detected (not a git repo or no modifications).");
6719
+ } else {
6720
+ addSystemMessage("Changed files:");
6721
+ for (const line of gitStatus.split("\n")) {
6722
+ addSystemMessage(` ${line}`);
6723
+ }
6724
+ }
6725
+ } catch {
6726
+ addSystemMessage("Could not check changes.");
6727
+ }
6728
+ break;
6729
+ }
6730
+ case "doctor": {
6731
+ addSystemMessage("Running diagnostics...");
6732
+ const authResult = await getAuthStatus({ homeDir });
6733
+ addSystemMessage(` Auth: ${authResult.connected ? "connected" : "not connected"} \u2014 ${authResult.message}`);
6734
+ addSystemMessage(` Workspace: ${workspacePath ? workspacePath : "none"}`);
6735
+ addSystemMessage(` Files: ${workspaceFiles.length}`);
6736
+ addSystemMessage(` Skills: ${skills2.length} loaded`);
6737
+ const mems = await loadMemories({ homeDir });
6738
+ addSystemMessage(` Memories: ${mems.length} stored`);
6739
+ addSystemMessage(` Node: ${process.version}`);
6740
+ const toolChecks = ["python3 --version", "pdflatex --version", "git --version"];
6741
+ for (const cmd2 of toolChecks) {
6742
+ try {
6743
+ const { execSync } = __require("child_process");
6744
+ const out = execSync(cmd2 + " 2>&1", { encoding: "utf8", timeout: 3e3 }).trim().split("\n")[0];
6745
+ addSystemMessage(` ${cmd2.split(" ")[0]}: ${out}`);
6746
+ } catch {
6747
+ addSystemMessage(` ${cmd2.split(" ")[0]}: not found`);
6748
+ }
6749
+ }
6750
+ addSystemMessage("Diagnostics complete.");
6751
+ break;
6752
+ }
6753
+ case "preview": {
6754
+ if (!args) {
6755
+ addSystemMessage("Usage: /preview <path-to-tex-file>");
6756
+ addSystemMessage("Example: /preview papers/draft.tex");
6757
+ break;
6758
+ }
6759
+ const texPath = args.trim();
6760
+ const resolvedTex = __require("path").isAbsolute(texPath) ? texPath : __require("path").resolve(workspacePath ?? process.cwd(), texPath);
6761
+ try {
6762
+ if (previewRef.current) {
6763
+ previewRef.current.close();
6764
+ }
6765
+ const preview = await startPreviewServer(resolvedTex);
6766
+ previewRef.current = preview;
6767
+ addSystemMessage(`Live preview started at ${preview.url}`);
6768
+ addSystemMessage("Auto-reloads when the file changes. Close with /preview stop");
6769
+ const openModule = await import("open");
6770
+ await openModule.default(preview.url);
6771
+ } catch (err) {
6772
+ addSystemMessage(`Preview failed: ${err instanceof Error ? err.message : String(err)}`);
6773
+ }
6774
+ break;
6775
+ }
6135
6776
  case "memory": {
6136
6777
  if (args === "clear") {
6137
6778
  await clearMemories({ homeDir });
@@ -6807,7 +7448,7 @@ function App({
6807
7448
  statusParts,
6808
7449
  statusColor,
6809
7450
  tokenDisplay,
6810
- workspaceName: hasWorkspace ? path17.basename(workspacePath) : process.cwd(),
7451
+ workspaceName: hasWorkspace ? path18.basename(workspacePath) : process.cwd(),
6811
7452
  mode: agentMode,
6812
7453
  planningStatus: planningState.status
6813
7454
  }
@@ -6819,7 +7460,7 @@ function App({
6819
7460
  var program = new Command();
6820
7461
  program.name("open-research").description("Local-first research CLI powered by ChatGPT/Codex auth.").argument("[workspacePath]", "Optional workspace path to open").action(async (workspacePath) => {
6821
7462
  await ensureOpenResearchConfig();
6822
- const target = workspacePath ? path18.resolve(workspacePath) : process.cwd();
7463
+ const target = workspacePath ? path19.resolve(workspacePath) : process.cwd();
6823
7464
  const project = await loadWorkspaceProject(target);
6824
7465
  const auth2 = await loadStoredAuth();
6825
7466
  render(
@@ -6841,7 +7482,7 @@ program.name("open-research").description("Local-first research CLI powered by C
6841
7482
  });
6842
7483
  program.command("init").argument("[workspacePath]").description("Initialize an Open Research workspace.").action(async (workspacePath) => {
6843
7484
  await ensureOpenResearchConfig();
6844
- const target = path18.resolve(workspacePath ?? process.cwd());
7485
+ const target = path19.resolve(workspacePath ?? process.cwd());
6845
7486
  const project = await initWorkspace({ workspaceDir: target });
6846
7487
  console.log(`Initialized workspace: ${target}`);
6847
7488
  console.log(`Title: ${project.title}`);
@@ -6910,8 +7551,8 @@ skills.command("create").argument("[name]").description("Scaffold a new user ski
6910
7551
  });
6911
7552
  skills.command("edit").argument("<name>").description("Open a user skill in $EDITOR.").action(async (name) => {
6912
7553
  await ensureOpenResearchConfig();
6913
- const skillDir = path18.join(getOpenResearchSkillsDir(), name);
6914
- openInEditor(path18.join(skillDir, "SKILL.md"));
7554
+ const skillDir = path19.join(getOpenResearchSkillsDir(), name);
7555
+ openInEditor(path19.join(skillDir, "SKILL.md"));
6915
7556
  const validation = await validateSkillDirectory({ skillDir });
6916
7557
  if (!validation.ok) {
6917
7558
  console.error(validation.errors.join("\n"));
@@ -6922,9 +7563,9 @@ skills.command("edit").argument("<name>").description("Open a user skill in $EDI
6922
7563
  });
6923
7564
  skills.command("validate").argument("[nameOrPath]").description("Validate one user skill.").action(async (nameOrPath) => {
6924
7565
  await ensureOpenResearchConfig();
6925
- const skillDir = nameOrPath ? path18.isAbsolute(nameOrPath) ? nameOrPath : path18.join(getOpenResearchSkillsDir(), nameOrPath) : getOpenResearchSkillsDir();
7566
+ const skillDir = nameOrPath ? path19.isAbsolute(nameOrPath) ? nameOrPath : path19.join(getOpenResearchSkillsDir(), nameOrPath) : getOpenResearchSkillsDir();
6926
7567
  const stat = await import("fs/promises").then(
6927
- (fs18) => fs18.stat(skillDir).catch(() => null)
7568
+ (fs19) => fs19.stat(skillDir).catch(() => null)
6928
7569
  );
6929
7570
  if (!stat) {
6930
7571
  throw new Error(`Skill path not found: ${skillDir}`);