reasonix 0.4.14 → 0.4.15

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/index.js CHANGED
@@ -32,6 +32,13 @@ function loadApiKey(path = defaultConfigPath()) {
32
32
  if (process.env.DEEPSEEK_API_KEY) return process.env.DEEPSEEK_API_KEY;
33
33
  return readConfig(path).apiKey;
34
34
  }
35
+ function searchEnabled(path = defaultConfigPath()) {
36
+ const env = process.env.REASONIX_SEARCH;
37
+ if (env === "off" || env === "false" || env === "0") return false;
38
+ const cfg = readConfig(path).search;
39
+ if (cfg === false) return false;
40
+ return true;
41
+ }
35
42
  function saveApiKey(key, path = defaultConfigPath()) {
36
43
  const cfg = readConfig(path);
37
44
  cfg.apiKey = key.trim();
@@ -2232,6 +2239,187 @@ function lineDiff(a, b) {
2232
2239
  return out;
2233
2240
  }
2234
2241
 
2242
+ // src/tools/web.ts
2243
+ var DEFAULT_FETCH_MAX_CHARS = 32e3;
2244
+ var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
2245
+ var DEFAULT_TOPK = 5;
2246
+ var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
2247
+ var MOJEEK_ENDPOINT = "https://www.mojeek.com/search";
2248
+ async function webSearch(query, opts = {}) {
2249
+ const topK = Math.max(1, Math.min(10, opts.topK ?? DEFAULT_TOPK));
2250
+ const resp = await fetch(`${MOJEEK_ENDPOINT}?q=${encodeURIComponent(query)}`, {
2251
+ headers: {
2252
+ "User-Agent": USER_AGENT,
2253
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9",
2254
+ "Accept-Language": "en-US,en;q=0.9"
2255
+ },
2256
+ signal: opts.signal,
2257
+ redirect: "follow"
2258
+ });
2259
+ if (!resp.ok) throw new Error(`web_search ${resp.status}`);
2260
+ const html = await resp.text();
2261
+ const results = parseMojeekResults(html).slice(0, topK);
2262
+ if (results.length === 0) {
2263
+ if (/no results found|did not match any documents/i.test(html)) return [];
2264
+ if (/captcha|verify you are human|access denied|forbidden/i.test(html)) {
2265
+ throw new Error("web_search: Mojeek anti-bot page \u2014 rate-limited or blocked");
2266
+ }
2267
+ throw new Error(
2268
+ `web_search: 0 results but response doesn't look like a real empty page (${html.length} chars, first 120: ${html.slice(0, 120).replace(/\s+/g, " ")})`
2269
+ );
2270
+ }
2271
+ return results;
2272
+ }
2273
+ function parseMojeekResults(html) {
2274
+ const titles = [];
2275
+ const titleAnchorRe = /<a\b[^>]*\bclass="title"[^>]*>[\s\S]*?<\/a>/g;
2276
+ let m;
2277
+ while (true) {
2278
+ m = titleAnchorRe.exec(html);
2279
+ if (m === null) break;
2280
+ titles.push(m[0]);
2281
+ }
2282
+ const snippets = [];
2283
+ const snippetRe = /<p\b[^>]*\bclass="s"[^>]*>([\s\S]*?)<\/p>/g;
2284
+ while (true) {
2285
+ m = snippetRe.exec(html);
2286
+ if (m === null) break;
2287
+ snippets.push(m[1] ?? "");
2288
+ }
2289
+ const hrefRe = /href="([^"]+)"/;
2290
+ const innerRe = /<a\b[^>]*>([\s\S]*?)<\/a>/;
2291
+ const results = [];
2292
+ for (let i = 0; i < titles.length; i++) {
2293
+ const anchor = titles[i];
2294
+ const hrefMatch = anchor.match(hrefRe);
2295
+ const innerMatch = anchor.match(innerRe);
2296
+ if (!hrefMatch?.[1]) continue;
2297
+ results.push({
2298
+ title: decodeHtmlEntities(stripHtml(innerMatch?.[1] ?? "")).trim(),
2299
+ url: hrefMatch[1],
2300
+ snippet: decodeHtmlEntities(stripHtml(snippets[i] ?? "")).replace(/\s+/g, " ").trim()
2301
+ });
2302
+ }
2303
+ return results;
2304
+ }
2305
+ async function webFetch(url, opts = {}) {
2306
+ const maxChars = opts.maxChars ?? DEFAULT_FETCH_MAX_CHARS;
2307
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
2308
+ const ctl = new AbortController();
2309
+ const timer = setTimeout(() => ctl.abort(), timeoutMs);
2310
+ const cancel = () => ctl.abort();
2311
+ opts.signal?.addEventListener("abort", cancel, { once: true });
2312
+ let resp;
2313
+ try {
2314
+ resp = await fetch(url, {
2315
+ headers: { "User-Agent": USER_AGENT, Accept: "text/html,text/plain,*/*" },
2316
+ signal: ctl.signal,
2317
+ redirect: "follow"
2318
+ });
2319
+ } finally {
2320
+ clearTimeout(timer);
2321
+ opts.signal?.removeEventListener("abort", cancel);
2322
+ }
2323
+ if (!resp.ok) throw new Error(`web_fetch ${resp.status} for ${url}`);
2324
+ const contentType = resp.headers.get("content-type") ?? "";
2325
+ const raw = await resp.text();
2326
+ const title = extractTitle(raw);
2327
+ const text = contentType.includes("text/html") ? htmlToText(raw) : raw;
2328
+ const truncated = text.length > maxChars;
2329
+ const finalText = truncated ? `${text.slice(0, maxChars)}
2330
+
2331
+ [\u2026 truncated ${text.length - maxChars} chars \u2026]` : text;
2332
+ return { url, title, text: finalText, truncated };
2333
+ }
2334
+ function htmlToText(html) {
2335
+ let s = html;
2336
+ s = s.replace(/<script[\s\S]*?<\/script>/gi, "");
2337
+ s = s.replace(/<style[\s\S]*?<\/style>/gi, "");
2338
+ s = s.replace(/<noscript[\s\S]*?<\/noscript>/gi, "");
2339
+ s = s.replace(/<nav[\s\S]*?<\/nav>/gi, "");
2340
+ s = s.replace(/<footer[\s\S]*?<\/footer>/gi, "");
2341
+ s = s.replace(/<aside[\s\S]*?<\/aside>/gi, "");
2342
+ s = s.replace(/<svg[\s\S]*?<\/svg>/gi, "");
2343
+ s = s.replace(/<\/?(p|div|br|h[1-6]|li|tr|section|article)\b[^>]*>/gi, "\n");
2344
+ s = s.replace(/<[^>]+>/g, "");
2345
+ s = decodeHtmlEntities(s);
2346
+ s = s.replace(/[ \t]+/g, " ");
2347
+ s = s.replace(/\n[ \t]+/g, "\n");
2348
+ s = s.replace(/\n{3,}/g, "\n\n");
2349
+ return s.trim();
2350
+ }
2351
+ function stripHtml(s) {
2352
+ return s.replace(/<[^>]+>/g, "");
2353
+ }
2354
+ function decodeHtmlEntities(s) {
2355
+ return s.replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
2356
+ }
2357
+ function extractTitle(html) {
2358
+ const m = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
2359
+ if (!m?.[1]) return void 0;
2360
+ return m[1].replace(/\s+/g, " ").trim() || void 0;
2361
+ }
2362
+ function registerWebTools(registry, opts = {}) {
2363
+ const defaultTopK = opts.defaultTopK ?? DEFAULT_TOPK;
2364
+ const maxFetchChars = opts.maxFetchChars ?? DEFAULT_FETCH_MAX_CHARS;
2365
+ registry.register({
2366
+ name: "web_search",
2367
+ description: "Search the public web. Returns ranked results with title, url, and snippet. Use this when the question needs information more current than your training data, when you're unsure of a factual detail, or when the user asks about a specific webpage/library/release you haven't seen.",
2368
+ parameters: {
2369
+ type: "object",
2370
+ properties: {
2371
+ query: { type: "string", description: "Natural-language search query." },
2372
+ topK: {
2373
+ type: "integer",
2374
+ description: `Number of results to return (1..10). Default ${defaultTopK}.`
2375
+ }
2376
+ },
2377
+ required: ["query"]
2378
+ },
2379
+ fn: async (args, ctx) => {
2380
+ const results = await webSearch(args.query, {
2381
+ topK: args.topK ?? defaultTopK,
2382
+ signal: ctx?.signal
2383
+ });
2384
+ return formatSearchResults(args.query, results);
2385
+ }
2386
+ });
2387
+ registry.register({
2388
+ name: "web_fetch",
2389
+ description: "Download a URL and return its visible text content (HTML pages get scripts/styles/nav stripped). Truncated at the tool-result cap. Use after web_search when a snippet isn't enough.",
2390
+ parameters: {
2391
+ type: "object",
2392
+ properties: {
2393
+ url: { type: "string", description: "Absolute http:// or https:// URL." }
2394
+ },
2395
+ required: ["url"]
2396
+ },
2397
+ fn: async (args, ctx) => {
2398
+ if (!/^https?:\/\//i.test(args.url)) {
2399
+ throw new Error("web_fetch: url must start with http:// or https://");
2400
+ }
2401
+ const page = await webFetch(args.url, { maxChars: maxFetchChars, signal: ctx?.signal });
2402
+ const header = page.title ? `${page.title}
2403
+ ${page.url}` : page.url;
2404
+ return `${header}
2405
+
2406
+ ${page.text}`;
2407
+ }
2408
+ });
2409
+ return registry;
2410
+ }
2411
+ function formatSearchResults(query, results) {
2412
+ const lines = [`query: ${query}`, `
2413
+ results (${results.length}):`];
2414
+ results.forEach((r, i) => {
2415
+ lines.push(`
2416
+ ${i + 1}. ${r.title}`);
2417
+ lines.push(` ${r.url}`);
2418
+ if (r.snippet) lines.push(` ${r.snippet}`);
2419
+ });
2420
+ return lines.join("\n");
2421
+ }
2422
+
2235
2423
  // src/env.ts
2236
2424
  import { readFileSync as readFileSync3 } from "fs";
2237
2425
  import { resolve as resolve2 } from "path";
@@ -3519,15 +3707,15 @@ function sep() {
3519
3707
  }
3520
3708
 
3521
3709
  // src/index.ts
3522
- var VERSION = "0.4.14";
3710
+ var VERSION = "0.4.15";
3523
3711
 
3524
3712
  // src/cli/commands/chat.tsx
3525
3713
  import { render } from "ink";
3526
- import React10, { useState as useState4 } from "react";
3714
+ import React10, { useState as useState5 } from "react";
3527
3715
 
3528
3716
  // src/cli/ui/App.tsx
3529
3717
  import { Box as Box7, Static, Text as Text7, useApp, useInput as useInput2 } from "ink";
3530
- import React8, { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
3718
+ import React8, { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState3 } from "react";
3531
3719
 
3532
3720
  // src/cli/ui/EventLog.tsx
3533
3721
  import { Box as Box3, Text as Text3 } from "ink";
@@ -3960,7 +4148,7 @@ function StreamingAssistant({ event }) {
3960
4148
  label = parts.join(" \xB7 ");
3961
4149
  labelColor = "green";
3962
4150
  }
3963
- return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React4.createElement(Pulse, null), /* @__PURE__ */ React4.createElement(Text3, { color: labelColor }, ` ${label} `), /* @__PURE__ */ React4.createElement(Elapsed, null)), reasoningTail ? /* @__PURE__ */ React4.createElement(Text3, { dimColor: true, italic: true }, "\u21B3 thinking: ", reasoningTail) : null, tail ? /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "\u25B8 ", tail) : reasoningOnly ? /* @__PURE__ */ React4.createElement(Text3, { color: "yellow", dimColor: true }, " R1 is thinking before it speaks \u2014 body text starts when reasoning completes (typically 20-90s).") : /* @__PURE__ */ React4.createElement(Text3, { dimColor: true, italic: true }, " connection open, first byte typically in 5-60s depending on model + load"));
4151
+ return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React4.createElement(Pulse, null), /* @__PURE__ */ React4.createElement(Text3, { color: labelColor }, ` ${label} `), /* @__PURE__ */ React4.createElement(Elapsed, null)), reasoningTail ? /* @__PURE__ */ React4.createElement(Text3, { dimColor: true, italic: true }, "\u21B3 thinking: ", reasoningTail) : null, tail ? /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "\u25B8 ", tail) : preFirstByte ? /* @__PURE__ */ React4.createElement(Text3, { dimColor: true, italic: true }, " connection open, first byte typically in 5-60s depending on model + load") : reasoningOnly ? /* @__PURE__ */ React4.createElement(Text3, { color: "yellow", dimColor: true }, " R1 is thinking before it speaks \u2014 body text starts when reasoning completes (typically 20-90s).") : toolCallOnly ? /* @__PURE__ */ React4.createElement(Text3, { color: "magenta", dimColor: true }, " tool-call arguments streaming \u2014 the model is about to dispatch a tool") : event.reasoning ? /* @__PURE__ */ React4.createElement(Text3, { color: "yellow", dimColor: true }, " R1 still reasoning \u2014 body text or tool call arrives when thinking completes") : null);
3964
4152
  }
3965
4153
  function Pulse() {
3966
4154
  const tick = useTick();
@@ -3982,40 +4170,119 @@ function truncate2(s, max) {
3982
4170
 
3983
4171
  // src/cli/ui/PromptInput.tsx
3984
4172
  import { Box as Box4, Text as Text4, useInput } from "ink";
3985
- import React5 from "react";
4173
+ import React5, { useRef, useState as useState2 } from "react";
3986
4174
 
3987
4175
  // src/cli/ui/multiline-keys.ts
3988
4176
  var BACKSLASH_SUFFIX = /\\$/;
3989
- function processMultilineKey(value, key) {
3990
- if (key.tab || key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.escape || key.pageUp || key.pageDown) {
3991
- return { next: null, submit: false };
4177
+ var NOOP = { next: null, cursor: null, submit: false };
4178
+ function processMultilineKey(value, cursor, key) {
4179
+ if (key.tab || key.escape || key.pageUp || key.pageDown) {
4180
+ return NOOP;
4181
+ }
4182
+ if (value.length === 0 && (key.upArrow || key.downArrow)) {
4183
+ return NOOP;
4184
+ }
4185
+ if (key.leftArrow) {
4186
+ return { next: null, cursor: Math.max(0, cursor - 1), submit: false };
4187
+ }
4188
+ if (key.rightArrow) {
4189
+ return { next: null, cursor: Math.min(value.length, cursor + 1), submit: false };
4190
+ }
4191
+ if (key.upArrow) {
4192
+ const moved = moveCursorUp(value, cursor);
4193
+ return moved === cursor ? NOOP : { next: null, cursor: moved, submit: false };
4194
+ }
4195
+ if (key.downArrow) {
4196
+ const moved = moveCursorDown(value, cursor);
4197
+ return moved === cursor ? NOOP : { next: null, cursor: moved, submit: false };
4198
+ }
4199
+ if (key.ctrl && key.input === "a") {
4200
+ return { next: null, cursor: startOfLine(value, cursor), submit: false };
4201
+ }
4202
+ if (key.ctrl && key.input === "e") {
4203
+ return { next: null, cursor: endOfLine(value, cursor), submit: false };
3992
4204
  }
3993
4205
  if (key.input === "\n" || key.ctrl && key.input === "j") {
3994
- return { next: `${value}
3995
- `, submit: false };
4206
+ return insertAt(value, cursor, "\n");
3996
4207
  }
3997
4208
  if (key.return) {
3998
- if (key.shift) {
3999
- return { next: `${value}
4000
- `, submit: false };
4001
- }
4002
- if (BACKSLASH_SUFFIX.test(value)) {
4003
- return { next: `${value.slice(0, -1)}
4004
- `, submit: false };
4209
+ if (key.shift) return insertAt(value, cursor, "\n");
4210
+ if (cursor === value.length && BACKSLASH_SUFFIX.test(value)) {
4211
+ const replaced = `${value.slice(0, -1)}
4212
+ `;
4213
+ return { next: replaced, cursor: replaced.length, submit: false };
4005
4214
  }
4006
- return { next: null, submit: true, submitValue: value };
4215
+ return { next: null, cursor: null, submit: true, submitValue: value };
4007
4216
  }
4008
- if (key.backspace || key.delete) {
4009
- if (value.length === 0) return { next: null, submit: false };
4010
- return { next: value.slice(0, -1), submit: false };
4217
+ if (key.backspace) {
4218
+ if (cursor === 0) return NOOP;
4219
+ return {
4220
+ next: value.slice(0, cursor - 1) + value.slice(cursor),
4221
+ cursor: cursor - 1,
4222
+ submit: false
4223
+ };
4224
+ }
4225
+ if (key.delete) {
4226
+ if (cursor === value.length) return NOOP;
4227
+ return {
4228
+ next: value.slice(0, cursor) + value.slice(cursor + 1),
4229
+ cursor,
4230
+ submit: false
4231
+ };
4011
4232
  }
4012
- if ((key.ctrl || key.meta) && key.input.length === 0) {
4013
- return { next: null, submit: false };
4233
+ if ((key.ctrl || key.meta) && key.input.length === 0) return NOOP;
4234
+ if (key.ctrl || key.meta) return NOOP;
4235
+ if (key.input.length > 0) {
4236
+ return insertAt(value, cursor, key.input);
4014
4237
  }
4015
- if (key.input.length > 0 && !key.ctrl && !key.meta) {
4016
- return { next: value + key.input, submit: false };
4238
+ return NOOP;
4239
+ }
4240
+ function insertAt(value, cursor, insert) {
4241
+ return {
4242
+ next: value.slice(0, cursor) + insert + value.slice(cursor),
4243
+ cursor: cursor + insert.length,
4244
+ submit: false
4245
+ };
4246
+ }
4247
+ function lineAndColumn(value, cursor) {
4248
+ let line = 0;
4249
+ let col = 0;
4250
+ const n = Math.min(cursor, value.length);
4251
+ for (let i = 0; i < n; i++) {
4252
+ if (value[i] === "\n") {
4253
+ line++;
4254
+ col = 0;
4255
+ } else {
4256
+ col++;
4257
+ }
4017
4258
  }
4018
- return { next: null, submit: false };
4259
+ return { line, col };
4260
+ }
4261
+ function startOfLine(value, cursor) {
4262
+ return value.lastIndexOf("\n", cursor - 1) + 1;
4263
+ }
4264
+ function endOfLine(value, cursor) {
4265
+ const nl = value.indexOf("\n", cursor);
4266
+ return nl === -1 ? value.length : nl;
4267
+ }
4268
+ function moveCursorUp(value, cursor) {
4269
+ const curStart = startOfLine(value, cursor);
4270
+ if (curStart === 0) return cursor;
4271
+ const col = cursor - curStart;
4272
+ const prevEnd = curStart - 1;
4273
+ const prevStart = value.lastIndexOf("\n", prevEnd - 1) + 1;
4274
+ const prevLen = prevEnd - prevStart;
4275
+ return prevStart + Math.min(col, prevLen);
4276
+ }
4277
+ function moveCursorDown(value, cursor) {
4278
+ const nextNl = value.indexOf("\n", cursor);
4279
+ if (nextNl === -1) return cursor;
4280
+ const curStart = startOfLine(value, cursor);
4281
+ const col = cursor - curStart;
4282
+ const nextStart = nextNl + 1;
4283
+ const followingNl = value.indexOf("\n", nextStart);
4284
+ const nextLen = (followingNl === -1 ? value.length : followingNl) - nextStart;
4285
+ return nextStart + Math.min(col, nextLen);
4019
4286
  }
4020
4287
 
4021
4288
  // src/cli/ui/PromptInput.tsx
@@ -4026,11 +4293,19 @@ function PromptInput({
4026
4293
  disabled,
4027
4294
  placeholder
4028
4295
  }) {
4296
+ const [cursor, setCursor] = useState2(value.length);
4297
+ const lastLocalValueRef = useRef(value);
4298
+ if (value !== lastLocalValueRef.current) {
4299
+ lastLocalValueRef.current = value;
4300
+ if (cursor !== value.length) {
4301
+ setCursor(value.length);
4302
+ }
4303
+ }
4029
4304
  const tick = useTick();
4030
4305
  const showCursor = disabled ? false : Math.floor(tick / 4) % 2 === 0;
4031
4306
  useInput(
4032
4307
  (input, key) => {
4033
- const keyEvent = {
4308
+ const ke = {
4034
4309
  input,
4035
4310
  return: key.return,
4036
4311
  shift: key.shift,
@@ -4047,8 +4322,14 @@ function PromptInput({
4047
4322
  pageUp: key.pageUp,
4048
4323
  pageDown: key.pageDown
4049
4324
  };
4050
- const action = processMultilineKey(value, keyEvent);
4051
- if (action.next !== null) onChange(action.next);
4325
+ const action = processMultilineKey(value, cursor, ke);
4326
+ if (action.next !== null) {
4327
+ lastLocalValueRef.current = action.next;
4328
+ onChange(action.next);
4329
+ }
4330
+ if (action.cursor !== null) {
4331
+ setCursor(action.cursor);
4332
+ }
4052
4333
  if (action.submit) onSubmit(action.submitValue ?? value);
4053
4334
  },
4054
4335
  { isActive: !disabled }
@@ -4056,16 +4337,39 @@ function PromptInput({
4056
4337
  const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026" : placeholder ?? "type a message, or /command \xB7 Ctrl+J for newline";
4057
4338
  const lines = value.length > 0 ? value.split("\n") : [""];
4058
4339
  const borderColor = disabled ? "gray" : "cyan";
4340
+ const { line: cursorLine, col: cursorCol } = lineAndColumn(value, cursor);
4059
4341
  return /* @__PURE__ */ React5.createElement(Box4, { borderStyle: "round", borderColor, paddingX: 1, flexDirection: "column" }, lines.map((line, i) => {
4060
- const isLast = i === lines.length - 1;
4061
4342
  const isFirst = i === 0;
4062
4343
  const showPlaceholder = isFirst && value.length === 0;
4344
+ const isCursorLine = i === cursorLine;
4063
4345
  return (
4064
4346
  // biome-ignore lint/suspicious/noArrayIndexKey: stable by construction — lines are derived from `value.split("\n")` and never reordered
4065
- /* @__PURE__ */ React5.createElement(Box4, { key: i }, isFirst ? /* @__PURE__ */ React5.createElement(Text4, { bold: true, color: borderColor }, "you \u203A", " ") : /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, " "), showPlaceholder && isLast && !disabled ? /* @__PURE__ */ React5.createElement(Text4, { color: borderColor }, showCursor ? "\u258C" : " ") : null, showPlaceholder ? /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, effectivePlaceholder) : /* @__PURE__ */ React5.createElement(Text4, null, line), !showPlaceholder && isLast && !disabled ? /* @__PURE__ */ React5.createElement(Text4, { color: borderColor }, showCursor ? "\u258C" : " ") : null)
4347
+ /* @__PURE__ */ React5.createElement(Box4, { key: i }, isFirst ? /* @__PURE__ */ React5.createElement(Text4, { bold: true, color: borderColor }, "you \u203A", " ") : /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, " "), showPlaceholder ? /* @__PURE__ */ React5.createElement(React5.Fragment, null, isCursorLine && !disabled ? /* @__PURE__ */ React5.createElement(Text4, { color: borderColor }, showCursor ? "\u258C" : " ") : null, /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, effectivePlaceholder)) : isCursorLine && !disabled ? /* @__PURE__ */ React5.createElement(
4348
+ LineWithCursor,
4349
+ {
4350
+ line,
4351
+ col: cursorCol,
4352
+ showCursor,
4353
+ borderColor
4354
+ }
4355
+ ) : /* @__PURE__ */ React5.createElement(Text4, null, line))
4066
4356
  );
4067
4357
  }));
4068
4358
  }
4359
+ function LineWithCursor({
4360
+ line,
4361
+ col,
4362
+ showCursor,
4363
+ borderColor
4364
+ }) {
4365
+ const before = line.slice(0, col);
4366
+ const atCursor = line.slice(col, col + 1);
4367
+ const after = line.slice(col + 1);
4368
+ if (atCursor.length === 0) {
4369
+ return /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(Text4, null, before), /* @__PURE__ */ React5.createElement(Text4, { color: borderColor }, showCursor ? "\u258C" : " "));
4370
+ }
4371
+ return /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(Text4, null, before), /* @__PURE__ */ React5.createElement(Text4, { inverse: showCursor }, atCursor), /* @__PURE__ */ React5.createElement(Text4, null, after));
4372
+ }
4069
4373
 
4070
4374
  // src/cli/ui/SlashSuggestions.tsx
4071
4375
  import { Box as Box5, Text as Text5 } from "ink";
@@ -4577,23 +4881,23 @@ function App({
4577
4881
  codeMode
4578
4882
  }) {
4579
4883
  const { exit } = useApp();
4580
- const [historical, setHistorical] = useState2([]);
4581
- const [streaming, setStreaming] = useState2(null);
4582
- const [input, setInput] = useState2("");
4583
- const [busy, setBusy] = useState2(false);
4584
- const abortedThisTurn = useRef(false);
4585
- const [ongoingTool, setOngoingTool] = useState2(null);
4586
- const [toolProgress, setToolProgress] = useState2(null);
4587
- const [statusLine, setStatusLine] = useState2(null);
4588
- const [balance, setBalance] = useState2(null);
4589
- const lastEditSnapshots = useRef(null);
4590
- const pendingEdits = useRef([]);
4591
- const promptHistory = useRef([]);
4592
- const historyCursor = useRef(-1);
4593
- const assistantIterCounter = useRef(0);
4594
- const toolHistoryRef = useRef([]);
4595
- const [slashSelected, setSlashSelected] = useState2(0);
4596
- const [summary, setSummary] = useState2({
4884
+ const [historical, setHistorical] = useState3([]);
4885
+ const [streaming, setStreaming] = useState3(null);
4886
+ const [input, setInput] = useState3("");
4887
+ const [busy, setBusy] = useState3(false);
4888
+ const abortedThisTurn = useRef2(false);
4889
+ const [ongoingTool, setOngoingTool] = useState3(null);
4890
+ const [toolProgress, setToolProgress] = useState3(null);
4891
+ const [statusLine, setStatusLine] = useState3(null);
4892
+ const [balance, setBalance] = useState3(null);
4893
+ const lastEditSnapshots = useRef2(null);
4894
+ const pendingEdits = useRef2([]);
4895
+ const promptHistory = useRef2([]);
4896
+ const historyCursor = useRef2(-1);
4897
+ const assistantIterCounter = useRef2(0);
4898
+ const toolHistoryRef = useRef2([]);
4899
+ const [slashSelected, setSlashSelected] = useState3(0);
4900
+ const [summary, setSummary] = useState3({
4597
4901
  turns: 0,
4598
4902
  totalCostUsd: 0,
4599
4903
  totalInputCostUsd: 0,
@@ -4603,7 +4907,7 @@ function App({
4603
4907
  cacheHitRatio: 0,
4604
4908
  lastPromptTokens: 0
4605
4909
  });
4606
- const transcriptRef = useRef(null);
4910
+ const transcriptRef = useRef2(null);
4607
4911
  if (transcript && !transcriptRef.current) {
4608
4912
  transcriptRef.current = openTranscriptFile(transcript, {
4609
4913
  version: 1,
@@ -4628,7 +4932,7 @@ function App({
4628
4932
  return prev;
4629
4933
  });
4630
4934
  }, [slashMatches]);
4631
- const loopRef = useRef(null);
4935
+ const loopRef = useRef2(null);
4632
4936
  const loop = useMemo(() => {
4633
4937
  if (loopRef.current) return loopRef.current;
4634
4938
  const client = new DeepSeekClient();
@@ -4665,7 +4969,7 @@ function App({
4665
4969
  if (progressSink.current) progressSink.current = null;
4666
4970
  };
4667
4971
  }, [progressSink]);
4668
- const sessionBannerShown = useRef(false);
4972
+ const sessionBannerShown = useRef2(false);
4669
4973
  useEffect2(() => {
4670
4974
  if (sessionBannerShown.current) return;
4671
4975
  sessionBannerShown.current = true;
@@ -4721,20 +5025,22 @@ function App({
4721
5025
  return;
4722
5026
  }
4723
5027
  }
4724
- const hist = promptHistory.current;
4725
- if (key.upArrow) {
4726
- if (hist.length === 0) return;
4727
- const nextCursor = Math.min(historyCursor.current + 1, hist.length - 1);
4728
- historyCursor.current = nextCursor;
4729
- setInput(hist[hist.length - 1 - nextCursor] ?? "");
4730
- return;
4731
- }
4732
- if (key.downArrow) {
4733
- if (historyCursor.current < 0) return;
4734
- const nextCursor = historyCursor.current - 1;
4735
- historyCursor.current = nextCursor;
4736
- setInput(nextCursor < 0 ? "" : hist[hist.length - 1 - nextCursor] ?? "");
4737
- return;
5028
+ if (input.length === 0) {
5029
+ const hist = promptHistory.current;
5030
+ if (key.upArrow) {
5031
+ if (hist.length === 0) return;
5032
+ const nextCursor = Math.min(historyCursor.current + 1, hist.length - 1);
5033
+ historyCursor.current = nextCursor;
5034
+ setInput(hist[hist.length - 1 - nextCursor] ?? "");
5035
+ return;
5036
+ }
5037
+ if (key.downArrow) {
5038
+ if (historyCursor.current < 0) return;
5039
+ const nextCursor = historyCursor.current - 1;
5040
+ historyCursor.current = nextCursor;
5041
+ setInput(nextCursor < 0 ? "" : hist[hist.length - 1 - nextCursor] ?? "");
5042
+ return;
5043
+ }
4738
5044
  }
4739
5045
  });
4740
5046
  const codeUndo = useCallback(() => {
@@ -5140,10 +5446,10 @@ function describeRepair(repair) {
5140
5446
  // src/cli/ui/Setup.tsx
5141
5447
  import { Box as Box8, Text as Text8, useApp as useApp2 } from "ink";
5142
5448
  import TextInput from "ink-text-input";
5143
- import React9, { useState as useState3 } from "react";
5449
+ import React9, { useState as useState4 } from "react";
5144
5450
  function Setup({ onReady }) {
5145
- const [value, setValue] = useState3("");
5146
- const [error, setError] = useState3(null);
5451
+ const [value, setValue] = useState4("");
5452
+ const [error, setError] = useState4(null);
5147
5453
  const { exit } = useApp2();
5148
5454
  const handleSubmit = (raw) => {
5149
5455
  const trimmed = raw.trim();
@@ -5178,7 +5484,7 @@ function Setup({ onReady }) {
5178
5484
 
5179
5485
  // src/cli/commands/chat.tsx
5180
5486
  function Root({ initialKey, tools, mcpSpecs, mcpServers, progressSink, ...appProps }) {
5181
- const [key, setKey] = useState4(initialKey);
5487
+ const [key, setKey] = useState5(initialKey);
5182
5488
  if (!key) {
5183
5489
  return /* @__PURE__ */ React10.createElement(
5184
5490
  Setup,
@@ -5274,6 +5580,10 @@ async function chatCommand(opts) {
5274
5580
  }
5275
5581
  }
5276
5582
  const mcpSpecs = successfulSpecs;
5583
+ if (searchEnabled()) {
5584
+ if (!tools) tools = new ToolRegistry();
5585
+ registerWebTools(tools);
5586
+ }
5277
5587
  const { waitUntilExit } = render(
5278
5588
  /* @__PURE__ */ React10.createElement(
5279
5589
  Root,
@@ -5329,7 +5639,7 @@ import React13 from "react";
5329
5639
 
5330
5640
  // src/cli/ui/DiffApp.tsx
5331
5641
  import { Box as Box10, Static as Static2, Text as Text10, useApp as useApp3, useInput as useInput3 } from "ink";
5332
- import React12, { useState as useState5 } from "react";
5642
+ import React12, { useState as useState6 } from "react";
5333
5643
 
5334
5644
  // src/cli/ui/RecordView.tsx
5335
5645
  import { Box as Box9, Text as Text9 } from "ink";
@@ -5372,7 +5682,7 @@ function DiffApp({ report }) {
5372
5682
  const { exit } = useApp3();
5373
5683
  const maxIdx = Math.max(0, report.pairs.length - 1);
5374
5684
  const initialIdx = report.firstDivergenceTurn ? report.pairs.findIndex((p) => p.turn === report.firstDivergenceTurn) : 0;
5375
- const [idx, setIdx] = useState5(Math.max(0, initialIdx));
5685
+ const [idx, setIdx] = useState6(Math.max(0, initialIdx));
5376
5686
  useInput3((input, key) => {
5377
5687
  if (input === "q" || key.ctrl && input === "c") {
5378
5688
  exit();
@@ -5619,11 +5929,11 @@ import React15 from "react";
5619
5929
 
5620
5930
  // src/cli/ui/ReplayApp.tsx
5621
5931
  import { Box as Box11, Static as Static3, Text as Text11, useApp as useApp4, useInput as useInput4 } from "ink";
5622
- import React14, { useMemo as useMemo2, useState as useState6 } from "react";
5932
+ import React14, { useMemo as useMemo2, useState as useState7 } from "react";
5623
5933
  function ReplayApp({ meta, pages }) {
5624
5934
  const { exit } = useApp4();
5625
5935
  const maxIdx = Math.max(0, pages.length - 1);
5626
- const [idx, setIdx] = useState6(maxIdx);
5936
+ const [idx, setIdx] = useState7(maxIdx);
5627
5937
  useInput4((input, key) => {
5628
5938
  if (input === "q" || key.ctrl && input === "c") {
5629
5939
  exit();
@@ -5984,11 +6294,11 @@ import React18 from "react";
5984
6294
  // src/cli/ui/Wizard.tsx
5985
6295
  import { Box as Box13, Text as Text13, useApp as useApp5, useInput as useInput6 } from "ink";
5986
6296
  import TextInput2 from "ink-text-input";
5987
- import React17, { useState as useState8 } from "react";
6297
+ import React17, { useState as useState9 } from "react";
5988
6298
 
5989
6299
  // src/cli/ui/Select.tsx
5990
6300
  import { Box as Box12, Text as Text12, useInput as useInput5 } from "ink";
5991
- import React16, { useState as useState7 } from "react";
6301
+ import React16, { useState as useState8 } from "react";
5992
6302
  function SingleSelect({
5993
6303
  items,
5994
6304
  initialValue,
@@ -5999,7 +6309,7 @@ function SingleSelect({
5999
6309
  0,
6000
6310
  items.findIndex((i) => i.value === initialValue && !i.disabled)
6001
6311
  );
6002
- const [index, setIndex] = useState7(initialIndex === -1 ? 0 : initialIndex);
6312
+ const [index, setIndex] = useState8(initialIndex === -1 ? 0 : initialIndex);
6003
6313
  useInput5((_input, key) => {
6004
6314
  if (key.upArrow) {
6005
6315
  setIndex((i) => findNextEnabled(items, i, -1));
@@ -6029,11 +6339,11 @@ function MultiSelect({
6029
6339
  onCancel,
6030
6340
  footer
6031
6341
  }) {
6032
- const [index, setIndex] = useState7(() => {
6342
+ const [index, setIndex] = useState8(() => {
6033
6343
  const first = items.findIndex((i) => !i.disabled);
6034
6344
  return first === -1 ? 0 : first;
6035
6345
  });
6036
- const [selected, setSelected] = useState7(new Set(initialSelected));
6346
+ const [selected, setSelected] = useState8(new Set(initialSelected));
6037
6347
  useInput5((input, key) => {
6038
6348
  if (key.upArrow) {
6039
6349
  setIndex((i) => findNextEnabled(items, i, -1));
@@ -6112,14 +6422,14 @@ var PRESET_DESCRIPTIONS = {
6112
6422
  var CATALOG_BY_NAME = new Map(MCP_CATALOG.map((e) => [e.name, e]));
6113
6423
  function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
6114
6424
  const { exit } = useApp5();
6115
- const [step, setStep] = useState8(existingApiKey ? "preset" : "apiKey");
6116
- const [data, setData] = useState8({
6425
+ const [step, setStep] = useState9(existingApiKey ? "preset" : "apiKey");
6426
+ const [data, setData] = useState9({
6117
6427
  apiKey: existingApiKey ?? "",
6118
6428
  preset: initial?.preset ?? "fast",
6119
6429
  selectedCatalog: deriveInitialCatalog(initial?.mcp ?? []),
6120
6430
  catalogArgs: {}
6121
6431
  });
6122
- const [error, setError] = useState8(null);
6432
+ const [error, setError] = useState9(null);
6123
6433
  useInput6((_input, key) => {
6124
6434
  if (key.escape && step !== "saved" && onCancel) onCancel();
6125
6435
  });
@@ -6236,7 +6546,7 @@ function ApiKeyStep({
6236
6546
  error,
6237
6547
  onError
6238
6548
  }) {
6239
- const [value, setValue] = useState8("");
6549
+ const [value, setValue] = useState9("");
6240
6550
  return /* @__PURE__ */ React17.createElement(Box13, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React17.createElement(Text13, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text13, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React17.createElement(Text13, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React17.createElement(Text13, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text13, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React17.createElement(
6241
6551
  TextInput2,
6242
6552
  {
@@ -6262,7 +6572,7 @@ function McpArgsStep({
6262
6572
  onSubmit,
6263
6573
  onError
6264
6574
  }) {
6265
- const [value, setValue] = useState8("");
6575
+ const [value, setValue] = useState9("");
6266
6576
  return /* @__PURE__ */ React17.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React17.createElement(Box13, { flexDirection: "column" }, /* @__PURE__ */ React17.createElement(Text13, null, entry.summary), entry.note ? /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text13, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text13, null, "Required parameter: "), /* @__PURE__ */ React17.createElement(Text13, { bold: true }, entry.userArgs)), /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text13, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React17.createElement(
6267
6577
  TextInput2,
6268
6578
  {