@dugleelabs/copair 1.2.0 → 1.3.0

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/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { join as join15 } from "path";
5
+ import { existsSync as existsSync18, readFileSync as readFileSync10 } from "fs";
5
6
 
6
7
  // src/cli/args.ts
7
8
  import { Command } from "commander";
@@ -4757,14 +4758,65 @@ var AgentBridge = class extends EventEmitter {
4757
4758
  };
4758
4759
 
4759
4760
  // src/cli/ui/app.tsx
4760
- import { useState as useState4, useEffect as useEffect3, useCallback as useCallback3, useImperativeHandle, forwardRef, useRef as useRef2 } from "react";
4761
- import { render, Box as Box6, Text as Text6, Static, useApp, useInput as useInput3 } from "ink";
4761
+ import { useState as useState6, useEffect as useEffect4, useCallback as useCallback3, useImperativeHandle, forwardRef, useRef as useRef2 } from "react";
4762
+ import { render, Box as Box8, Text as Text10, Static, useApp, useInput as useInput4 } from "ink";
4762
4763
 
4763
4764
  // src/cli/ui/bordered-input.tsx
4764
4765
  import { useState, useEffect, useCallback, useRef } from "react";
4765
- import { Box, Text, useStdout, useInput } from "ink";
4766
- import TextInput from "ink-text-input";
4767
- import { jsx, jsxs } from "react/jsx-runtime";
4766
+ import { Box, Text as Text2, useStdout, useInput } from "ink";
4767
+
4768
+ // src/cli/ui/cursor-text.tsx
4769
+ import { Text } from "ink";
4770
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4771
+ function CursorText({ value, cursorPos, active }) {
4772
+ if (!active) return /* @__PURE__ */ jsx(Text, { children: value });
4773
+ const chars = [...value];
4774
+ const before = chars.slice(0, cursorPos).join("");
4775
+ const at = chars[cursorPos] ?? " ";
4776
+ const after = chars.slice(cursorPos + 1).join("");
4777
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
4778
+ /* @__PURE__ */ jsx(Text, { children: before }),
4779
+ /* @__PURE__ */ jsx(Text, { inverse: true, children: at }),
4780
+ /* @__PURE__ */ jsx(Text, { children: after })
4781
+ ] });
4782
+ }
4783
+
4784
+ // src/cli/ui/cursor-utils.ts
4785
+ function detectWordNav(input) {
4786
+ if (input === "\x1B[1;3D" || input === "\x1Bb" || input === "\x1B[1;5D") return "word-left";
4787
+ if (input === "\x1B[1;3C" || input === "\x1Bf" || input === "\x1B[1;5C") return "word-right";
4788
+ return null;
4789
+ }
4790
+ function detectWordDeletion(input, key) {
4791
+ const isAltBackspace = key.meta && key.backspace || input === "\x1B\x7F";
4792
+ const isCtrlW = key.ctrl && input === "w";
4793
+ return isAltBackspace || isCtrlW;
4794
+ }
4795
+ function isPasteInput(input, key) {
4796
+ if (key.ctrl || key.meta) return false;
4797
+ if (input.startsWith("[200~")) return true;
4798
+ return input.length > 1 && /[\n\r]/.test(input);
4799
+ }
4800
+ function cleanPastedInput(input) {
4801
+ return input.replace(/^\[200~/, "").replace(new RegExp(String.fromCharCode(27) + "\\[201~$"), "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
4802
+ }
4803
+ function wordBoundaryLeft(value, pos) {
4804
+ const chars = [...value];
4805
+ let i = pos;
4806
+ while (i > 0 && chars[i - 1] === " ") i--;
4807
+ while (i > 0 && chars[i - 1] !== " ") i--;
4808
+ return i;
4809
+ }
4810
+ function wordBoundaryRight(value, pos) {
4811
+ const chars = [...value];
4812
+ let i = pos;
4813
+ while (i < chars.length && chars[i] === " ") i++;
4814
+ while (i < chars.length && chars[i] !== " ") i++;
4815
+ return i;
4816
+ }
4817
+
4818
+ // src/cli/ui/bordered-input.tsx
4819
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
4768
4820
  function supportsUnicode() {
4769
4821
  const term = process.env.TERM ?? "";
4770
4822
  const lang = process.env.LANG ?? "";
@@ -4785,16 +4837,18 @@ function BorderedInput({
4785
4837
  completionEngine,
4786
4838
  onSubmit,
4787
4839
  onHistoryAppend,
4788
- onSlashCommand
4840
+ onSlashCommand,
4841
+ activeSuggestion,
4842
+ injectedValue
4789
4843
  }) {
4790
4844
  const [value, setValue] = useState("");
4845
+ const [cursorPos, setCursorPos] = useState(0);
4791
4846
  const [multiLineBuffer, setMultiLineBuffer] = useState(null);
4792
- const [expanded, setExpanded] = useState(false);
4847
+ const [completionHint, setCompletionHint] = useState(null);
4793
4848
  const { stdout } = useStdout();
4794
4849
  const [columns, setColumns] = useState(stdout?.columns ?? 80);
4795
4850
  const historyIdx = useRef(-1);
4796
4851
  const savedInput = useRef("");
4797
- const [completionHint, setCompletionHint] = useState(null);
4798
4852
  useEffect(() => {
4799
4853
  if (!stdout) return;
4800
4854
  const onResize = () => setColumns(stdout.columns);
@@ -4803,15 +4857,79 @@ function BorderedInput({
4803
4857
  stdout.off("resize", onResize);
4804
4858
  };
4805
4859
  }, [stdout]);
4806
- useInput((_input, key) => {
4860
+ useEffect(() => {
4861
+ if (injectedValue != null) {
4862
+ setValue(injectedValue.value);
4863
+ setCursorPos([...injectedValue.value].length);
4864
+ }
4865
+ }, [injectedValue]);
4866
+ const processSubmit = useCallback((input) => {
4867
+ const trimmed = input.trim();
4868
+ if (!trimmed) return;
4869
+ historyIdx.current = -1;
4870
+ savedInput.current = "";
4871
+ setCompletionHint(null);
4872
+ if (trimmed === "/expand") {
4873
+ setValue("");
4874
+ setCursorPos(0);
4875
+ return;
4876
+ }
4877
+ if (trimmed === "/send" && multiLineBuffer) {
4878
+ onHistoryAppend?.(multiLineBuffer);
4879
+ onSubmit(multiLineBuffer);
4880
+ setMultiLineBuffer(null);
4881
+ setValue("");
4882
+ setCursorPos(0);
4883
+ return;
4884
+ }
4885
+ if (trimmed.startsWith("/") && onSlashCommand) {
4886
+ const spaceIdx = trimmed.indexOf(" ");
4887
+ const cmd = spaceIdx === -1 ? trimmed.slice(1) : trimmed.slice(1, spaceIdx);
4888
+ const args = spaceIdx === -1 ? void 0 : trimmed.slice(spaceIdx + 1);
4889
+ onHistoryAppend?.(trimmed);
4890
+ onSlashCommand(cmd, args);
4891
+ setValue("");
4892
+ setCursorPos(0);
4893
+ return;
4894
+ }
4895
+ onHistoryAppend?.(input);
4896
+ onSubmit(input);
4897
+ setValue("");
4898
+ setCursorPos(0);
4899
+ }, [multiLineBuffer, onSubmit, onSlashCommand, onHistoryAppend]);
4900
+ useInput((input, key) => {
4807
4901
  if (!isActive) return;
4808
- if (key.upArrow && history.length > 0) {
4809
- if (historyIdx.current === -1) {
4810
- savedInput.current = value;
4902
+ if (multiLineBuffer !== null) {
4903
+ if (key.return) {
4904
+ onHistoryAppend?.(multiLineBuffer);
4905
+ onSubmit(multiLineBuffer);
4906
+ setMultiLineBuffer(null);
4907
+ setValue("");
4908
+ setCursorPos(0);
4909
+ historyIdx.current = -1;
4910
+ savedInput.current = "";
4911
+ return;
4912
+ }
4913
+ if (key.escape) {
4914
+ setMultiLineBuffer(null);
4915
+ setValue("");
4916
+ setCursorPos(0);
4917
+ return;
4811
4918
  }
4919
+ }
4920
+ if (isPasteInput(input, key)) {
4921
+ setMultiLineBuffer(cleanPastedInput(input));
4922
+ setValue("");
4923
+ setCursorPos(0);
4924
+ return;
4925
+ }
4926
+ if (key.upArrow && history.length > 0) {
4927
+ if (historyIdx.current === -1) savedInput.current = value;
4812
4928
  const newIdx = Math.min(historyIdx.current + 1, history.length - 1);
4813
4929
  historyIdx.current = newIdx;
4814
- setValue(history[history.length - 1 - newIdx]);
4930
+ const newVal = history[history.length - 1 - newIdx];
4931
+ setValue(newVal);
4932
+ setCursorPos([...newVal].length);
4815
4933
  setCompletionHint(null);
4816
4934
  return;
4817
4935
  }
@@ -4819,110 +4937,175 @@ function BorderedInput({
4819
4937
  if (historyIdx.current <= 0) {
4820
4938
  historyIdx.current = -1;
4821
4939
  setValue(savedInput.current);
4940
+ setCursorPos([...savedInput.current].length);
4822
4941
  } else {
4823
4942
  historyIdx.current--;
4824
- setValue(history[history.length - 1 - historyIdx.current]);
4943
+ const newVal = history[history.length - 1 - historyIdx.current];
4944
+ setValue(newVal);
4945
+ setCursorPos([...newVal].length);
4825
4946
  }
4826
4947
  setCompletionHint(null);
4827
4948
  return;
4828
4949
  }
4829
- if (key.tab && completionEngine && value) {
4830
- const items = completionEngine.complete(value);
4831
- if (items.length === 1) {
4832
- setValue(items[0].value);
4833
- setCompletionHint(null);
4834
- } else if (items.length > 1) {
4835
- const common = completionEngine.commonPrefix(items);
4836
- if (common.length > value.length) {
4837
- setValue(common);
4838
- }
4839
- setCompletionHint(items.map((i) => i.label).join(" "));
4950
+ if (key.return) {
4951
+ processSubmit(value);
4952
+ return;
4953
+ }
4954
+ const isHome = input === "\x1B[H" || input === "\x1B[1~";
4955
+ const isEnd = input === "\x1B[F" || input === "\x1B[4~";
4956
+ if (key.ctrl && input === "a" || isHome) {
4957
+ setCursorPos(0);
4958
+ return;
4959
+ }
4960
+ if (key.ctrl && input === "e" || isEnd) {
4961
+ setCursorPos([...value].length);
4962
+ return;
4963
+ }
4964
+ if (key.ctrl && input === "u") {
4965
+ const chars2 = [...value];
4966
+ setValue(chars2.slice(cursorPos).join(""));
4967
+ setCursorPos(0);
4968
+ historyIdx.current = -1;
4969
+ return;
4970
+ }
4971
+ if (key.ctrl && input === "k") {
4972
+ const chars2 = [...value];
4973
+ setValue(chars2.slice(0, cursorPos).join(""));
4974
+ historyIdx.current = -1;
4975
+ return;
4976
+ }
4977
+ const wordNav = detectWordNav(input);
4978
+ if (wordNav === "word-left") {
4979
+ setCursorPos(wordBoundaryLeft(value, cursorPos));
4980
+ return;
4981
+ }
4982
+ if (wordNav === "word-right") {
4983
+ setCursorPos(wordBoundaryRight(value, cursorPos));
4984
+ return;
4985
+ }
4986
+ if (detectWordDeletion(input, key)) {
4987
+ const chars2 = [...value];
4988
+ const newPos = wordBoundaryLeft(value, cursorPos);
4989
+ setValue([...chars2.slice(0, newPos), ...chars2.slice(cursorPos)].join(""));
4990
+ setCursorPos(newPos);
4991
+ historyIdx.current = -1;
4992
+ return;
4993
+ }
4994
+ if (key.backspace) {
4995
+ if (cursorPos > 0) {
4996
+ const chars2 = [...value];
4997
+ chars2.splice(cursorPos - 1, 1);
4998
+ setValue(chars2.join(""));
4999
+ setCursorPos(cursorPos - 1);
5000
+ historyIdx.current = -1;
4840
5001
  }
4841
5002
  return;
4842
5003
  }
4843
- }, { isActive });
4844
- const handleChange = useCallback((newValue) => {
4845
- historyIdx.current = -1;
4846
- setCompletionHint(null);
4847
- if (newValue.includes("\n")) {
4848
- setMultiLineBuffer(newValue);
4849
- setExpanded(false);
4850
- const firstLine = newValue.split("\n")[0];
4851
- setValue(firstLine);
4852
- } else {
4853
- if (multiLineBuffer !== null && !newValue.startsWith(value)) {
4854
- setMultiLineBuffer(null);
5004
+ if (key.delete) {
5005
+ if (cursorPos > 0) {
5006
+ const chars2 = [...value];
5007
+ chars2.splice(cursorPos - 1, 1);
5008
+ setValue(chars2.join(""));
5009
+ setCursorPos(cursorPos - 1);
5010
+ historyIdx.current = -1;
4855
5011
  }
4856
- setValue(newValue);
5012
+ return;
4857
5013
  }
4858
- }, [multiLineBuffer, value]);
4859
- const handleSubmit = useCallback((input) => {
4860
- const trimmed = input.trim();
4861
- if (!trimmed) return;
4862
- historyIdx.current = -1;
4863
- savedInput.current = "";
4864
- setCompletionHint(null);
4865
- if (trimmed === "/expand" && multiLineBuffer) {
4866
- setExpanded(!expanded);
4867
- setValue("");
5014
+ if (key.leftArrow) {
5015
+ setCursorPos(Math.max(0, cursorPos - 1));
4868
5016
  return;
4869
5017
  }
4870
- if (trimmed === "/send" && multiLineBuffer) {
4871
- onHistoryAppend?.(multiLineBuffer);
4872
- onSubmit(multiLineBuffer);
4873
- setMultiLineBuffer(null);
4874
- setExpanded(false);
4875
- setValue("");
5018
+ if (key.rightArrow) {
5019
+ setCursorPos(Math.min([...value].length, cursorPos + 1));
4876
5020
  return;
4877
5021
  }
4878
- if (trimmed.startsWith("/") && onSlashCommand) {
4879
- const spaceIdx = trimmed.indexOf(" ");
4880
- const cmd = spaceIdx === -1 ? trimmed.slice(1) : trimmed.slice(1, spaceIdx);
4881
- const args = spaceIdx === -1 ? void 0 : trimmed.slice(spaceIdx + 1);
4882
- onHistoryAppend?.(trimmed);
4883
- onSlashCommand(cmd, args);
4884
- setValue("");
5022
+ if (key.tab) {
5023
+ if (!value && activeSuggestion) {
5024
+ onHistoryAppend?.(activeSuggestion.action);
5025
+ onSubmit(activeSuggestion.action);
5026
+ historyIdx.current = -1;
5027
+ savedInput.current = "";
5028
+ return;
5029
+ }
5030
+ if (completionEngine && value) {
5031
+ const items = completionEngine.complete(value);
5032
+ if (items.length === 1) {
5033
+ setValue(items[0].value);
5034
+ setCursorPos([...items[0].value].length);
5035
+ setCompletionHint(null);
5036
+ } else if (items.length > 1) {
5037
+ const common = completionEngine.commonPrefix(items);
5038
+ if (common.length > value.length) {
5039
+ setValue(common);
5040
+ setCursorPos([...common].length);
5041
+ }
5042
+ setCompletionHint(items.map((i) => i.label).join(" "));
5043
+ }
5044
+ }
4885
5045
  return;
4886
5046
  }
4887
- const toSubmit = multiLineBuffer ?? input;
4888
- onHistoryAppend?.(toSubmit);
4889
- onSubmit(toSubmit);
4890
- setMultiLineBuffer(null);
4891
- setExpanded(false);
4892
- setValue("");
4893
- }, [multiLineBuffer, expanded, onSubmit, onSlashCommand, onHistoryAppend]);
4894
- const lineCount = multiLineBuffer ? multiLineBuffer.split("\n").length : 0;
5047
+ if (key.ctrl && input === "r") {
5048
+ onSlashCommand?.("history-search");
5049
+ return;
5050
+ }
5051
+ const cp = input.codePointAt(0);
5052
+ if (cp === void 0 || cp < 32 || cp === 127) return;
5053
+ if (key.ctrl || key.meta) return;
5054
+ const chars = [...value];
5055
+ const inputChars = [...input];
5056
+ chars.splice(cursorPos, 0, ...inputChars);
5057
+ setValue(chars.join(""));
5058
+ setCursorPos(cursorPos + inputChars.length);
5059
+ historyIdx.current = -1;
5060
+ setCompletionHint(null);
5061
+ }, { isActive });
5062
+ function renderMultilinePreview() {
5063
+ if (!multiLineBuffer) return null;
5064
+ const lines = multiLineBuffer.split("\n");
5065
+ const totalLines = lines.length;
5066
+ const byteLen = Buffer.byteLength(multiLineBuffer, "utf8");
5067
+ const sizeStr = byteLen >= 1024 ? `${(byteLen / 1024).toFixed(1)} KB` : `${byteLen} B`;
5068
+ const firstNonEmpty = lines.find((l) => l.trim()) ?? "";
5069
+ const sanitized = firstNonEmpty.replace(/[^\x20-\x7E]/g, "").trim();
5070
+ const maxHint = Math.max(20, columns - 14);
5071
+ const hint = sanitized.length > maxHint ? sanitized.slice(0, maxHint - 1) + "\u2026" : sanitized;
5072
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", marginBottom: 1, children: [
5073
+ /* @__PURE__ */ jsxs2(Box, { gap: 1, children: [
5074
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "\u2398" }),
5075
+ /* @__PURE__ */ jsxs2(Text2, { bold: true, children: [
5076
+ totalLines,
5077
+ " line",
5078
+ totalLines !== 1 ? "s" : ""
5079
+ ] }),
5080
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\xB7" }),
5081
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: sizeStr }),
5082
+ hint ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
5083
+ '\xB7 "',
5084
+ hint,
5085
+ '"'
5086
+ ] }) : null
5087
+ ] }),
5088
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "[Enter to send \xB7 Esc to discard]" })
5089
+ ] });
5090
+ }
4895
5091
  if (!bordered || columns < 40 || hasInkGhostingIssue()) {
4896
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
4897
- /* @__PURE__ */ jsxs(Box, { children: [
4898
- /* @__PURE__ */ jsxs(Text, { color: "green", bold: true, children: [
5092
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", children: [
5093
+ /* @__PURE__ */ jsxs2(Box, { children: [
5094
+ /* @__PURE__ */ jsxs2(Text2, { color: "green", bold: true, children: [
4899
5095
  ">",
4900
5096
  " "
4901
5097
  ] }),
4902
- /* @__PURE__ */ jsx(
4903
- TextInput,
4904
- {
4905
- value,
4906
- onChange: handleChange,
4907
- onSubmit: handleSubmit,
4908
- focus: isActive
4909
- }
4910
- ),
4911
- multiLineBuffer && !expanded && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
4912
- " [",
4913
- lineCount,
4914
- " lines - /expand to view, /send to submit]"
4915
- ] })
5098
+ /* @__PURE__ */ jsx2(CursorText, { value, cursorPos, active: isActive })
4916
5099
  ] }),
4917
- completionHint && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
5100
+ completionHint && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
4918
5101
  " ",
4919
5102
  completionHint
4920
5103
  ] }),
4921
- expanded && multiLineBuffer && /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginLeft: 2, children: multiLineBuffer.split("\n").map((line, i) => /* @__PURE__ */ jsx(Text, { dimColor: true, children: line }, i)) })
5104
+ renderMultilinePreview()
4922
5105
  ] });
4923
5106
  }
4924
5107
  const borderStyle = supportsUnicode() ? "round" : "classic";
4925
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(
5108
+ return /* @__PURE__ */ jsx2(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs2(
4926
5109
  Box,
4927
5110
  {
4928
5111
  flexDirection: "column",
@@ -4932,34 +5115,18 @@ function BorderedInput({
4932
5115
  paddingLeft: 1,
4933
5116
  paddingRight: 1,
4934
5117
  children: [
4935
- /* @__PURE__ */ jsxs(Box, { children: [
4936
- /* @__PURE__ */ jsxs(Text, { color: "green", bold: true, children: [
5118
+ /* @__PURE__ */ jsxs2(Box, { children: [
5119
+ /* @__PURE__ */ jsxs2(Text2, { color: "green", bold: true, children: [
4937
5120
  ">",
4938
5121
  " "
4939
5122
  ] }),
4940
- /* @__PURE__ */ jsx(
4941
- TextInput,
4942
- {
4943
- value,
4944
- onChange: handleChange,
4945
- onSubmit: handleSubmit,
4946
- focus: isActive
4947
- }
4948
- )
5123
+ /* @__PURE__ */ jsx2(CursorText, { value, cursorPos, active: isActive })
4949
5124
  ] }),
4950
- completionHint && /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
5125
+ completionHint && /* @__PURE__ */ jsx2(Box, { children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
4951
5126
  " ",
4952
5127
  completionHint
4953
5128
  ] }) }),
4954
- multiLineBuffer && !expanded && /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
4955
- "[",
4956
- lineCount,
4957
- " lines pasted - /expand to view, /send to submit]"
4958
- ] }) }),
4959
- expanded && multiLineBuffer && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
4960
- multiLineBuffer.split("\n").map((line, i) => /* @__PURE__ */ jsx(Text, { dimColor: true, children: line }, i)),
4961
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[/send to submit, /expand to collapse]" })
4962
- ] })
5129
+ renderMultilinePreview()
4963
5130
  ]
4964
5131
  }
4965
5132
  ) });
@@ -4967,11 +5134,11 @@ function BorderedInput({
4967
5134
 
4968
5135
  // src/cli/ui/status-bar.tsx
4969
5136
  import { useState as useState2, useEffect as useEffect2 } from "react";
4970
- import { Box as Box2, Text as Text3, useStdout as useStdout2 } from "ink";
5137
+ import { Box as Box2, Text as Text4, useStdout as useStdout2 } from "ink";
4971
5138
 
4972
5139
  // src/cli/ui/context-bar.tsx
4973
- import { Text as Text2 } from "ink";
4974
- import { jsxs as jsxs2 } from "react/jsx-runtime";
5140
+ import { Text as Text3 } from "ink";
5141
+ import { jsxs as jsxs3 } from "react/jsx-runtime";
4975
5142
  function ContextBar({ percent, segments = 10 }) {
4976
5143
  const clamped = Math.max(0, Math.min(100, percent));
4977
5144
  const filled = Math.round(clamped / 100 * segments);
@@ -4985,7 +5152,7 @@ function ContextBar({ percent, segments = 10 }) {
4985
5152
  } else {
4986
5153
  color2 = "green";
4987
5154
  }
4988
- return /* @__PURE__ */ jsxs2(Text2, { color: color2, children: [
5155
+ return /* @__PURE__ */ jsxs3(Text3, { color: color2, children: [
4989
5156
  "[",
4990
5157
  bar,
4991
5158
  "] ",
@@ -4995,7 +5162,7 @@ function ContextBar({ percent, segments = 10 }) {
4995
5162
  }
4996
5163
 
4997
5164
  // src/cli/ui/status-bar.tsx
4998
- import { Fragment, jsx as jsx2, jsxs as jsxs3 } from "react/jsx-runtime";
5165
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs4 } from "react/jsx-runtime";
4999
5166
  function StatusBar({ bridge, model, sessionIdentifier, visible = true }) {
5000
5167
  const { stdout } = useStdout2();
5001
5168
  const [usage, setUsage] = useState2({
@@ -5018,19 +5185,19 @@ function StatusBar({ bridge, model, sessionIdentifier, visible = true }) {
5018
5185
  if (!stdout?.isTTY) return null;
5019
5186
  const tokens = `${usage.sessionInputTokens.toLocaleString()} in / ${usage.sessionOutputTokens.toLocaleString()} out`;
5020
5187
  const cost = `$${usage.sessionCost.toFixed(2)}`;
5021
- return /* @__PURE__ */ jsxs3(Box2, { width: "100%", justifyContent: "space-between", children: [
5022
- /* @__PURE__ */ jsxs3(Box2, { children: [
5023
- /* @__PURE__ */ jsx2(Text3, { color: "cyan", bold: true, children: model }),
5024
- /* @__PURE__ */ jsx2(Text3, { dimColor: true, children: " | " }),
5025
- /* @__PURE__ */ jsx2(Text3, { children: tokens }),
5026
- /* @__PURE__ */ jsx2(Text3, { dimColor: true, children: " | " }),
5027
- /* @__PURE__ */ jsx2(Text3, { color: "yellow", children: cost })
5188
+ return /* @__PURE__ */ jsxs4(Box2, { width: "100%", justifyContent: "space-between", children: [
5189
+ /* @__PURE__ */ jsxs4(Box2, { children: [
5190
+ /* @__PURE__ */ jsx3(Text4, { color: "cyan", bold: true, children: model }),
5191
+ /* @__PURE__ */ jsx3(Text4, { dimColor: true, children: " | " }),
5192
+ /* @__PURE__ */ jsx3(Text4, { children: tokens }),
5193
+ /* @__PURE__ */ jsx3(Text4, { dimColor: true, children: " | " }),
5194
+ /* @__PURE__ */ jsx3(Text4, { color: "yellow", children: cost })
5028
5195
  ] }),
5029
- /* @__PURE__ */ jsxs3(Box2, { children: [
5030
- /* @__PURE__ */ jsx2(ContextBar, { percent: contextPercent }),
5031
- sessionIdentifier && /* @__PURE__ */ jsxs3(Fragment, { children: [
5032
- /* @__PURE__ */ jsx2(Text3, { dimColor: true, children: " | " }),
5033
- /* @__PURE__ */ jsx2(Text3, { dimColor: true, children: sessionIdentifier })
5196
+ /* @__PURE__ */ jsxs4(Box2, { children: [
5197
+ /* @__PURE__ */ jsx3(ContextBar, { percent: contextPercent }),
5198
+ sessionIdentifier && /* @__PURE__ */ jsxs4(Fragment2, { children: [
5199
+ /* @__PURE__ */ jsx3(Text4, { dimColor: true, children: " | " }),
5200
+ /* @__PURE__ */ jsx3(Text4, { dimColor: true, children: sessionIdentifier })
5034
5201
  ] })
5035
5202
  ] })
5036
5203
  ] });
@@ -5041,11 +5208,11 @@ import React3, { useState as useState3, useCallback as useCallback2 } from "reac
5041
5208
  import { Box as Box5, useInput as useInput2 } from "ink";
5042
5209
 
5043
5210
  // src/cli/ui/approval-prompt.tsx
5044
- import { Box as Box4, Text as Text5, useStdout as useStdout3 } from "ink";
5211
+ import { Box as Box4, Text as Text6, useStdout as useStdout3 } from "ink";
5045
5212
 
5046
5213
  // src/cli/ui/diff-view.tsx
5047
- import { Box as Box3, Text as Text4 } from "ink";
5048
- import { jsx as jsx3, jsxs as jsxs4 } from "react/jsx-runtime";
5214
+ import { Box as Box3, Text as Text5 } from "ink";
5215
+ import { jsx as jsx4, jsxs as jsxs5 } from "react/jsx-runtime";
5049
5216
  function DiffView({ diff, maxLines = 30 }) {
5050
5217
  let lineCount = 0;
5051
5218
  let truncated = false;
@@ -5059,19 +5226,19 @@ function DiffView({ diff, maxLines = 30 }) {
5059
5226
  lineCount++;
5060
5227
  if (line.startsWith("+")) {
5061
5228
  lines.push(
5062
- /* @__PURE__ */ jsx3(Text4, { backgroundColor: "green", color: "black", children: line }, `${hunkIndex}-${lineCount}`)
5229
+ /* @__PURE__ */ jsx4(Text5, { backgroundColor: "green", color: "black", children: line }, `${hunkIndex}-${lineCount}`)
5063
5230
  );
5064
5231
  } else if (line.startsWith("-")) {
5065
5232
  lines.push(
5066
- /* @__PURE__ */ jsx3(Text4, { backgroundColor: "red", color: "black", children: line }, `${hunkIndex}-${lineCount}`)
5233
+ /* @__PURE__ */ jsx4(Text5, { backgroundColor: "red", color: "black", children: line }, `${hunkIndex}-${lineCount}`)
5067
5234
  );
5068
5235
  } else if (line.startsWith("@@")) {
5069
5236
  lines.push(
5070
- /* @__PURE__ */ jsx3(Text4, { color: "cyan", children: line }, `${hunkIndex}-${lineCount}`)
5237
+ /* @__PURE__ */ jsx4(Text5, { color: "cyan", children: line }, `${hunkIndex}-${lineCount}`)
5071
5238
  );
5072
5239
  } else {
5073
5240
  lines.push(
5074
- /* @__PURE__ */ jsx3(Text4, { dimColor: true, children: line }, `${hunkIndex}-${lineCount}`)
5241
+ /* @__PURE__ */ jsx4(Text5, { dimColor: true, children: line }, `${hunkIndex}-${lineCount}`)
5075
5242
  );
5076
5243
  }
5077
5244
  }
@@ -5079,14 +5246,14 @@ function DiffView({ diff, maxLines = 30 }) {
5079
5246
  };
5080
5247
  const allLines = diff.hunks.flatMap((hunk, i) => renderHunk(hunk, i));
5081
5248
  const totalLines = diff.hunks.reduce((sum, h) => sum + h.lines.length, 0);
5082
- return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", children: [
5083
- /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
5249
+ return /* @__PURE__ */ jsxs5(Box3, { flexDirection: "column", children: [
5250
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
5084
5251
  " -- ",
5085
5252
  diff.filePath,
5086
5253
  " --"
5087
5254
  ] }),
5088
5255
  allLines,
5089
- truncated && /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
5256
+ truncated && /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
5090
5257
  " ...",
5091
5258
  totalLines - maxLines,
5092
5259
  " more lines"
@@ -5095,12 +5262,12 @@ function DiffView({ diff, maxLines = 30 }) {
5095
5262
  }
5096
5263
 
5097
5264
  // src/cli/ui/approval-prompt.tsx
5098
- import { jsx as jsx4, jsxs as jsxs5 } from "react/jsx-runtime";
5265
+ import { jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
5099
5266
  function ApprovalPrompt({ request, onRespond: _onRespond }) {
5100
5267
  const { stdout } = useStdout3();
5101
5268
  const columns = stdout?.columns ?? 80;
5102
5269
  const boxWidth = Math.min(columns - 4, 120);
5103
- return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsxs5(
5270
+ return /* @__PURE__ */ jsx5(Box4, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsxs6(
5104
5271
  Box4,
5105
5272
  {
5106
5273
  flexDirection: "column",
@@ -5110,12 +5277,12 @@ function ApprovalPrompt({ request, onRespond: _onRespond }) {
5110
5277
  paddingLeft: 1,
5111
5278
  paddingRight: 1,
5112
5279
  children: [
5113
- /* @__PURE__ */ jsxs5(Box4, { children: [
5114
- /* @__PURE__ */ jsxs5(Text5, { color: "yellow", bold: true, children: [
5280
+ /* @__PURE__ */ jsxs6(Box4, { children: [
5281
+ /* @__PURE__ */ jsxs6(Text6, { color: "yellow", bold: true, children: [
5115
5282
  "\u26A0",
5116
5283
  " Approval required"
5117
5284
  ] }),
5118
- request.total > 1 && /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
5285
+ request.total > 1 && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
5119
5286
  " [",
5120
5287
  request.index + 1,
5121
5288
  "/",
@@ -5123,36 +5290,36 @@ function ApprovalPrompt({ request, onRespond: _onRespond }) {
5123
5290
  "]"
5124
5291
  ] })
5125
5292
  ] }),
5126
- request.warning && /* @__PURE__ */ jsxs5(Box4, { marginTop: 1, children: [
5127
- /* @__PURE__ */ jsxs5(Text5, { color: "red", bold: true, children: [
5293
+ request.warning && /* @__PURE__ */ jsxs6(Box4, { marginTop: 1, children: [
5294
+ /* @__PURE__ */ jsxs6(Text6, { color: "red", bold: true, children: [
5128
5295
  "\u26A0",
5129
5296
  " WARNING: "
5130
5297
  ] }),
5131
- /* @__PURE__ */ jsxs5(Text5, { wrap: "wrap", children: [
5298
+ /* @__PURE__ */ jsxs6(Text6, { wrap: "wrap", children: [
5132
5299
  "This command accesses a sensitive system path outside the project root (",
5133
5300
  request.warning,
5134
5301
  ")"
5135
5302
  ] })
5136
5303
  ] }),
5137
- /* @__PURE__ */ jsxs5(Box4, { marginTop: 1, children: [
5138
- /* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
5304
+ /* @__PURE__ */ jsxs6(Box4, { marginTop: 1, children: [
5305
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
5139
5306
  request.toolName,
5140
5307
  ": "
5141
5308
  ] }),
5142
- /* @__PURE__ */ jsx4(Text5, { wrap: "wrap", children: request.summary })
5309
+ /* @__PURE__ */ jsx5(Text6, { wrap: "wrap", children: request.summary })
5143
5310
  ] }),
5144
- request.diff && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(DiffView, { diff: request.diff, maxLines: 20 }) }),
5145
- /* @__PURE__ */ jsxs5(Box4, { marginTop: 1, children: [
5146
- /* @__PURE__ */ jsx4(Text5, { color: "green", children: "[y] " }),
5147
- /* @__PURE__ */ jsx4(Text5, { children: "allow " }),
5148
- /* @__PURE__ */ jsx4(Text5, { color: "cyan", children: "[a] " }),
5149
- /* @__PURE__ */ jsx4(Text5, { children: "always " }),
5150
- /* @__PURE__ */ jsx4(Text5, { color: "red", children: "[n] " }),
5151
- /* @__PURE__ */ jsx4(Text5, { children: "deny " }),
5152
- /* @__PURE__ */ jsx4(Text5, { color: "yellow", children: "[A] " }),
5153
- /* @__PURE__ */ jsx4(Text5, { children: "all " }),
5154
- /* @__PURE__ */ jsx4(Text5, { color: "magenta", children: "[s] " }),
5155
- /* @__PURE__ */ jsx4(Text5, { children: "similar" })
5311
+ request.diff && /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx5(DiffView, { diff: request.diff, maxLines: 20 }) }),
5312
+ /* @__PURE__ */ jsxs6(Box4, { marginTop: 1, children: [
5313
+ /* @__PURE__ */ jsx5(Text6, { color: "green", children: "[y] " }),
5314
+ /* @__PURE__ */ jsx5(Text6, { children: "allow " }),
5315
+ /* @__PURE__ */ jsx5(Text6, { color: "cyan", children: "[a] " }),
5316
+ /* @__PURE__ */ jsx5(Text6, { children: "always " }),
5317
+ /* @__PURE__ */ jsx5(Text6, { color: "red", children: "[n] " }),
5318
+ /* @__PURE__ */ jsx5(Text6, { children: "deny " }),
5319
+ /* @__PURE__ */ jsx5(Text6, { color: "yellow", children: "[A] " }),
5320
+ /* @__PURE__ */ jsx5(Text6, { children: "all " }),
5321
+ /* @__PURE__ */ jsx5(Text6, { color: "magenta", children: "[s] " }),
5322
+ /* @__PURE__ */ jsx5(Text6, { children: "similar" })
5156
5323
  ] })
5157
5324
  ]
5158
5325
  }
@@ -5160,7 +5327,7 @@ function ApprovalPrompt({ request, onRespond: _onRespond }) {
5160
5327
  }
5161
5328
 
5162
5329
  // src/cli/ui/approval-handler.tsx
5163
- import { jsx as jsx5 } from "react/jsx-runtime";
5330
+ import { jsx as jsx6 } from "react/jsx-runtime";
5164
5331
  function ApprovalHandler({ bridge }) {
5165
5332
  const [pending, setPending] = useState3(null);
5166
5333
  React3.useEffect(() => {
@@ -5204,7 +5371,7 @@ function ApprovalHandler({ bridge }) {
5204
5371
  { isActive: pending !== null }
5205
5372
  );
5206
5373
  if (!pending) return null;
5207
- return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
5374
+ return /* @__PURE__ */ jsx6(Box5, { children: /* @__PURE__ */ jsx6(
5208
5375
  ApprovalPrompt,
5209
5376
  {
5210
5377
  request: pending.request,
@@ -5213,8 +5380,189 @@ function ApprovalHandler({ bridge }) {
5213
5380
  ) });
5214
5381
  }
5215
5382
 
5383
+ // src/cli/ui/activity-bar.tsx
5384
+ import { Text as Text7 } from "ink";
5385
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
5386
+ function ActivityBar({ phase, spinnerFrame, spinnerElapsed, liveTool }) {
5387
+ if (liveTool !== null) {
5388
+ return /* @__PURE__ */ jsxs7(Text7, { color: "green", children: [
5389
+ " ",
5390
+ "\u25CF",
5391
+ " ",
5392
+ liveTool
5393
+ ] });
5394
+ }
5395
+ if (phase === "thinking") {
5396
+ return /* @__PURE__ */ jsxs7(Text7, { children: [
5397
+ " ",
5398
+ /* @__PURE__ */ jsx7(Text7, { color: "magenta", children: spinnerFrame }),
5399
+ " ",
5400
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
5401
+ "thinking... ",
5402
+ /* @__PURE__ */ jsx7(Text7, { color: "gray", children: spinnerElapsed })
5403
+ ] })
5404
+ ] });
5405
+ }
5406
+ if (phase === "streaming") {
5407
+ return /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
5408
+ " ",
5409
+ spinnerFrame,
5410
+ " ..."
5411
+ ] });
5412
+ }
5413
+ return /* @__PURE__ */ jsx7(Text7, { children: " " });
5414
+ }
5415
+
5416
+ // src/cli/ui/suggestion-hint.tsx
5417
+ import { useState as useState4, useEffect as useEffect3 } from "react";
5418
+ import { Box as Box6, Text as Text8 } from "ink";
5419
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
5420
+ var DEFAULT_RULES = [
5421
+ {
5422
+ id: "run-tests",
5423
+ condition: (ctx) => ctx.editCount > 0 && ctx.hasTestFramework && ctx.lastToolNames.includes("edit"),
5424
+ suggestion: "Run tests to verify changes?",
5425
+ action: "run the tests for the files I just changed"
5426
+ },
5427
+ {
5428
+ id: "commit-changes",
5429
+ condition: (ctx) => ctx.editCount >= 3,
5430
+ suggestion: "Commit these changes?",
5431
+ action: "commit the changes with a descriptive message"
5432
+ },
5433
+ {
5434
+ id: "resume-session",
5435
+ condition: (ctx) => ctx.sessionCount > 0 && ctx.editCount === 0,
5436
+ suggestion: "Resume previous session?",
5437
+ action: "/session resume"
5438
+ }
5439
+ ];
5440
+ function SuggestionHint({
5441
+ bridge,
5442
+ enabled = true,
5443
+ rules = DEFAULT_RULES,
5444
+ initialContext,
5445
+ onSuggestionChange
5446
+ }) {
5447
+ const [context, setContext] = useState4({
5448
+ lastToolNames: [],
5449
+ editCount: 0,
5450
+ hasTestFramework: false,
5451
+ sessionCount: 0,
5452
+ ...initialContext
5453
+ });
5454
+ useEffect3(() => {
5455
+ const onToolComplete = (tool) => {
5456
+ setContext((prev) => ({
5457
+ ...prev,
5458
+ lastToolNames: [...prev.lastToolNames.slice(-5), tool.name],
5459
+ editCount: tool.name === "edit" || tool.name === "write" ? prev.editCount + 1 : prev.editCount
5460
+ }));
5461
+ };
5462
+ const onTurnComplete = () => {
5463
+ setContext((prev) => ({ ...prev, lastToolNames: [] }));
5464
+ };
5465
+ bridge.on("tool-complete", onToolComplete);
5466
+ bridge.on("turn-complete", onTurnComplete);
5467
+ return () => {
5468
+ bridge.off("tool-complete", onToolComplete);
5469
+ bridge.off("turn-complete", onTurnComplete);
5470
+ };
5471
+ }, [bridge]);
5472
+ const activeSuggestion = enabled ? rules.find((rule) => rule.condition(context)) ?? null : null;
5473
+ useEffect3(() => {
5474
+ onSuggestionChange?.(activeSuggestion);
5475
+ }, [activeSuggestion, onSuggestionChange]);
5476
+ if (!enabled || activeSuggestion === null) return null;
5477
+ return /* @__PURE__ */ jsx8(Box6, { marginLeft: 2, children: /* @__PURE__ */ jsxs8(Text8, { dimColor: true, italic: true, children: [
5478
+ activeSuggestion.suggestion,
5479
+ " [Tab to accept]"
5480
+ ] }) });
5481
+ }
5482
+
5483
+ // src/cli/ui/history-search.tsx
5484
+ import { useState as useState5, useMemo } from "react";
5485
+ import { Box as Box7, Text as Text9, useInput as useInput3 } from "ink";
5486
+ import TextInput from "ink-text-input";
5487
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
5488
+ function HistorySearch({ history, visible, onSelect, onDismiss }) {
5489
+ const [query, setQuery] = useState5("");
5490
+ const [selectedIndex, setSelectedIndex] = useState5(0);
5491
+ const filtered = useMemo(() => {
5492
+ if (!query) return history.slice(0, 20);
5493
+ const lowerQuery = query.toLowerCase();
5494
+ return history.filter((entry) => {
5495
+ const lower = entry.toLowerCase();
5496
+ let qi = 0;
5497
+ for (let i = 0; i < lower.length && qi < lowerQuery.length; i++) {
5498
+ if (lower[i] === lowerQuery[qi]) qi++;
5499
+ }
5500
+ return qi === lowerQuery.length;
5501
+ }).slice(0, 20);
5502
+ }, [history, query]);
5503
+ useInput3(
5504
+ (_input, key) => {
5505
+ if (!visible) return;
5506
+ if (key.escape) {
5507
+ setQuery("");
5508
+ setSelectedIndex(0);
5509
+ onDismiss();
5510
+ return;
5511
+ }
5512
+ if (key.return) {
5513
+ if (filtered.length > 0) {
5514
+ onSelect(filtered[selectedIndex]);
5515
+ }
5516
+ setQuery("");
5517
+ setSelectedIndex(0);
5518
+ return;
5519
+ }
5520
+ if (key.upArrow) {
5521
+ setSelectedIndex((prev) => Math.max(prev - 1, 0));
5522
+ return;
5523
+ }
5524
+ if (key.downArrow) {
5525
+ setSelectedIndex((prev) => Math.min(prev + 1, filtered.length - 1));
5526
+ return;
5527
+ }
5528
+ },
5529
+ { isActive: visible }
5530
+ );
5531
+ if (!visible) return null;
5532
+ const maxVisible = 10;
5533
+ const displayItems = filtered.slice(0, maxVisible);
5534
+ return /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", borderStyle: "single", borderColor: "yellow", paddingLeft: 1, paddingRight: 1, children: [
5535
+ /* @__PURE__ */ jsxs9(Box7, { children: [
5536
+ /* @__PURE__ */ jsx9(Text9, { color: "yellow", bold: true, children: "reverse-i-search: " }),
5537
+ /* @__PURE__ */ jsx9(TextInput, { value: query, onChange: (v) => {
5538
+ setQuery(v);
5539
+ setSelectedIndex(0);
5540
+ }, focus: visible })
5541
+ ] }),
5542
+ displayItems.length > 0 ? /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", marginTop: 1, children: [
5543
+ displayItems.map((entry, i) => /* @__PURE__ */ jsxs9(
5544
+ Text9,
5545
+ {
5546
+ color: i === selectedIndex ? "cyan" : void 0,
5547
+ bold: i === selectedIndex,
5548
+ children: [
5549
+ i === selectedIndex ? "> " : " ",
5550
+ entry
5551
+ ]
5552
+ },
5553
+ i
5554
+ )),
5555
+ filtered.length > maxVisible && /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
5556
+ " ...",
5557
+ filtered.length - maxVisible,
5558
+ " more matches"
5559
+ ] })
5560
+ ] }) : /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " No matches" })
5561
+ ] });
5562
+ }
5563
+
5216
5564
  // src/cli/ui/app.tsx
5217
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
5565
+ import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
5218
5566
  var DEFAULT_UI_CONFIG = {
5219
5567
  bordered_input: true,
5220
5568
  status_bar: true,
@@ -5227,10 +5575,10 @@ var DEFAULT_UI_CONFIG = {
5227
5575
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
5228
5576
  var SPINNER_INTERVAL = 80;
5229
5577
  function useSpinner(active) {
5230
- const [frameIdx, setFrameIdx] = useState4(0);
5231
- const [elapsed, setElapsed] = useState4(0);
5578
+ const [frameIdx, setFrameIdx] = useState6(0);
5579
+ const [elapsed, setElapsed] = useState6(0);
5232
5580
  const startTime = useRef2(0);
5233
- useEffect3(() => {
5581
+ useEffect4(() => {
5234
5582
  if (!active) {
5235
5583
  setFrameIdx(0);
5236
5584
  setElapsed(0);
@@ -5254,19 +5602,19 @@ function renderInline(text) {
5254
5602
  while (remaining.length > 0) {
5255
5603
  const boldMatch = remaining.match(/^\*\*(.+?)\*\*/);
5256
5604
  if (boldMatch) {
5257
- parts.push(/* @__PURE__ */ jsx6(Text6, { bold: true, children: boldMatch[1] }, key++));
5605
+ parts.push(/* @__PURE__ */ jsx10(Text10, { bold: true, children: boldMatch[1] }, key++));
5258
5606
  remaining = remaining.slice(boldMatch[0].length);
5259
5607
  continue;
5260
5608
  }
5261
5609
  const italicMatch = remaining.match(/^\*(.+?)\*/);
5262
5610
  if (italicMatch) {
5263
- parts.push(/* @__PURE__ */ jsx6(Text6, { italic: true, children: italicMatch[1] }, key++));
5611
+ parts.push(/* @__PURE__ */ jsx10(Text10, { italic: true, children: italicMatch[1] }, key++));
5264
5612
  remaining = remaining.slice(italicMatch[0].length);
5265
5613
  continue;
5266
5614
  }
5267
5615
  const codeMatch = remaining.match(/^`([^`]+)`/);
5268
5616
  if (codeMatch) {
5269
- parts.push(/* @__PURE__ */ jsx6(Text6, { color: "cyan", bold: true, children: codeMatch[1] }, key++));
5617
+ parts.push(/* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: codeMatch[1] }, key++));
5270
5618
  remaining = remaining.slice(codeMatch[0].length);
5271
5619
  continue;
5272
5620
  }
@@ -5283,7 +5631,7 @@ function renderInline(text) {
5283
5631
  remaining = remaining.slice(nextSpecial);
5284
5632
  }
5285
5633
  }
5286
- return parts.length === 1 ? parts[0] : /* @__PURE__ */ jsx6(Fragment2, { children: parts });
5634
+ return parts.length === 1 ? parts[0] : /* @__PURE__ */ jsx10(Fragment3, { children: parts });
5287
5635
  }
5288
5636
  function renderMarkdownBlocks(text) {
5289
5637
  const lines = text.split("\n");
@@ -5303,9 +5651,9 @@ function renderMarkdownBlocks(text) {
5303
5651
  }
5304
5652
  if (i < lines.length) i++;
5305
5653
  elements.push(
5306
- /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginY: 1, children: [
5307
- lang && /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: lang }),
5308
- /* @__PURE__ */ jsx6(Box6, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "column", children: codeLines.map((cl, ci) => /* @__PURE__ */ jsx6(Text6, { color: "white", children: cl }, ci)) })
5654
+ /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", marginY: 1, children: [
5655
+ lang && /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: lang }),
5656
+ /* @__PURE__ */ jsx10(Box8, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "column", children: codeLines.map((cl, ci) => /* @__PURE__ */ jsx10(Text10, { color: "white", children: cl }, ci)) })
5309
5657
  ] }, key++)
5310
5658
  );
5311
5659
  continue;
@@ -5315,7 +5663,7 @@ function renderMarkdownBlocks(text) {
5315
5663
  const level = headerMatch[1].length;
5316
5664
  const content = headerMatch[2];
5317
5665
  elements.push(
5318
- /* @__PURE__ */ jsxs6(Text6, { bold: true, color: level <= 2 ? "white" : void 0, children: [
5666
+ /* @__PURE__ */ jsxs10(Text10, { bold: true, color: level <= 2 ? "white" : void 0, children: [
5319
5667
  level <= 2 ? "\n" : "",
5320
5668
  content
5321
5669
  ] }, key++)
@@ -5325,7 +5673,7 @@ function renderMarkdownBlocks(text) {
5325
5673
  }
5326
5674
  if (/^[-*_]{3,}$/.test(trimmed)) {
5327
5675
  elements.push(
5328
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\u2500".repeat(40) }, key++)
5676
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2500".repeat(40) }, key++)
5329
5677
  );
5330
5678
  i++;
5331
5679
  continue;
@@ -5333,7 +5681,7 @@ function renderMarkdownBlocks(text) {
5333
5681
  const ulMatch = trimmed.match(/^[-*+]\s+(.*)/);
5334
5682
  if (ulMatch) {
5335
5683
  elements.push(
5336
- /* @__PURE__ */ jsxs6(Text6, { wrap: "wrap", children: [
5684
+ /* @__PURE__ */ jsxs10(Text10, { wrap: "wrap", children: [
5337
5685
  " ",
5338
5686
  "\u2022",
5339
5687
  " ",
@@ -5346,7 +5694,7 @@ function renderMarkdownBlocks(text) {
5346
5694
  const olMatch = trimmed.match(/^(\d+)[.)]\s+(.*)/);
5347
5695
  if (olMatch) {
5348
5696
  elements.push(
5349
- /* @__PURE__ */ jsxs6(Text6, { wrap: "wrap", children: [
5697
+ /* @__PURE__ */ jsxs10(Text10, { wrap: "wrap", children: [
5350
5698
  " ",
5351
5699
  olMatch[1],
5352
5700
  ". ",
@@ -5359,7 +5707,7 @@ function renderMarkdownBlocks(text) {
5359
5707
  if (trimmed.startsWith(">")) {
5360
5708
  const content = trimmed.replace(/^>\s?/, "");
5361
5709
  elements.push(
5362
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, wrap: "wrap", children: [
5710
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, wrap: "wrap", children: [
5363
5711
  " ",
5364
5712
  "\u2502",
5365
5713
  " ",
@@ -5374,7 +5722,7 @@ function renderMarkdownBlocks(text) {
5374
5722
  continue;
5375
5723
  }
5376
5724
  elements.push(
5377
- /* @__PURE__ */ jsx6(Text6, { wrap: "wrap", children: renderInline(line) }, key++)
5725
+ /* @__PURE__ */ jsx10(Text10, { wrap: "wrap", children: renderInline(line) }, key++)
5378
5726
  );
5379
5727
  i++;
5380
5728
  }
@@ -5390,17 +5738,18 @@ var CopairApp = forwardRef(function CopairApp2({
5390
5738
  onMessage,
5391
5739
  onHistoryAppend,
5392
5740
  onSlashCommand,
5393
- onExit: _onExit
5741
+ onExit: _onExit,
5742
+ initialContext
5394
5743
  }, ref) {
5395
5744
  const config = { ...DEFAULT_UI_CONFIG, ...uiOverrides };
5396
5745
  const { exit } = useApp();
5397
5746
  const ctrlCCount = useRef2(0);
5398
5747
  const ctrlCTimer = useRef2(null);
5399
5748
  const nextId = useRef2(0);
5400
- const [staticItems, setStaticItems] = useState4([]);
5401
- const [liveText, setLiveText] = useState4("");
5402
- const [liveTool, setLiveTool] = useState4(null);
5403
- const [state, setState] = useState4({
5749
+ const [staticItems, setStaticItems] = useState6([]);
5750
+ const [liveText, setLiveText] = useState6("");
5751
+ const [liveTool, setLiveTool] = useState6(null);
5752
+ const [state, setState] = useState6({
5404
5753
  phase: "input",
5405
5754
  model,
5406
5755
  sessionIdentifier: sessionIdentifier ?? "",
@@ -5415,7 +5764,11 @@ var CopairApp = forwardRef(function CopairApp2({
5415
5764
  contextWindowPercent: 0,
5416
5765
  notification: null
5417
5766
  });
5418
- const spinner = useSpinner(state.phase === "thinking");
5767
+ const spinner = useSpinner(state.phase === "thinking" || state.phase === "streaming");
5768
+ const [activeSuggestion, setActiveSuggestion] = useState6(null);
5769
+ const [historySearchVisible, setHistorySearchVisible] = useState6(false);
5770
+ const [injectedInput, setInjectedInput] = useState6(void 0);
5771
+ const injectedNonce = useRef2(0);
5419
5772
  useImperativeHandle(ref, () => ({
5420
5773
  updateModel: (newModel) => {
5421
5774
  setState((prev) => ({ ...prev, model: newModel }));
@@ -5424,7 +5777,7 @@ var CopairApp = forwardRef(function CopairApp2({
5424
5777
  setState((prev) => ({ ...prev, sessionIdentifier: id }));
5425
5778
  }
5426
5779
  }));
5427
- useInput3((_input, key) => {
5780
+ useInput4((_input, key) => {
5428
5781
  if (key.ctrl && _input === "c") {
5429
5782
  ctrlCCount.current++;
5430
5783
  if (ctrlCCount.current >= 2) {
@@ -5440,7 +5793,7 @@ var CopairApp = forwardRef(function CopairApp2({
5440
5793
  }, 2e3);
5441
5794
  }
5442
5795
  });
5443
- useEffect3(() => {
5796
+ useEffect4(() => {
5444
5797
  const onStreamText = (text) => {
5445
5798
  setState((prev) => prev.phase === "thinking" ? { ...prev, phase: "streaming" } : prev);
5446
5799
  setLiveText((prev) => prev + text);
@@ -5543,48 +5896,71 @@ var CopairApp = forwardRef(function CopairApp2({
5543
5896
  setState((prev) => ({ ...prev, phase: "input" }));
5544
5897
  });
5545
5898
  }, [onMessage, bridge]);
5546
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
5547
- /* @__PURE__ */ jsx6(Static, { items: staticItems, children: (item) => {
5899
+ const handleSlashCommand = useCallback3(async (command, args) => {
5900
+ if (command === "history-search") {
5901
+ setHistorySearchVisible(true);
5902
+ return;
5903
+ }
5904
+ await onSlashCommand?.(command, args);
5905
+ }, [onSlashCommand]);
5906
+ return /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", children: [
5907
+ /* @__PURE__ */ jsx10(Static, { items: staticItems, children: (item) => {
5548
5908
  switch (item.type) {
5549
5909
  case "user":
5550
- return /* @__PURE__ */ jsxs6(Text6, { color: "cyan", bold: true, children: [
5910
+ return /* @__PURE__ */ jsxs10(Text10, { color: "cyan", bold: true, children: [
5551
5911
  "\u276F",
5552
5912
  " ",
5553
5913
  item.content
5554
5914
  ] }, item.id);
5555
5915
  case "error":
5556
- return /* @__PURE__ */ jsx6(Text6, { color: "red", children: item.content }, item.id);
5916
+ return /* @__PURE__ */ jsx10(Text10, { color: "red", children: item.content }, item.id);
5557
5917
  case "tool":
5558
- return /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
5918
+ return /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
5559
5919
  " ",
5560
5920
  item.content
5561
5921
  ] }, item.id);
5562
5922
  case "diff":
5563
- return item.diff ? /* @__PURE__ */ jsx6(DiffView, { diff: item.diff }, item.id) : null;
5923
+ return item.diff ? /* @__PURE__ */ jsx10(DiffView, { diff: item.diff }, item.id) : null;
5564
5924
  case "text":
5565
5925
  default:
5566
- return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", children: renderMarkdownBlocks(item.content) }, item.id);
5926
+ return /* @__PURE__ */ jsx10(Box8, { flexDirection: "column", children: renderMarkdownBlocks(item.content) }, item.id);
5567
5927
  }
5568
5928
  } }),
5569
- state.phase === "thinking" && /* @__PURE__ */ jsxs6(Text6, { children: [
5570
- " ",
5571
- /* @__PURE__ */ jsx6(Text6, { color: "magenta", children: spinner.frame }),
5572
- " ",
5573
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
5574
- "thinking... ",
5575
- /* @__PURE__ */ jsx6(Text6, { color: "gray", children: spinner.elapsed })
5576
- ] })
5577
- ] }),
5578
- liveText && /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", children: renderMarkdownBlocks(liveText) }),
5579
- liveTool && /* @__PURE__ */ jsxs6(Text6, { color: "green", children: [
5580
- " ",
5581
- "\u25CF",
5582
- " ",
5583
- liveTool
5584
- ] }),
5585
- /* @__PURE__ */ jsx6(ApprovalHandler, { bridge }),
5586
- state.notification && /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: state.notification }),
5587
- state.phase === "input" ? /* @__PURE__ */ jsx6(
5929
+ liveText && /* @__PURE__ */ jsx10(Box8, { flexDirection: "column", children: renderMarkdownBlocks(liveText) }),
5930
+ /* @__PURE__ */ jsx10(
5931
+ ActivityBar,
5932
+ {
5933
+ phase: state.phase,
5934
+ spinnerFrame: spinner.frame,
5935
+ spinnerElapsed: spinner.elapsed,
5936
+ liveTool
5937
+ }
5938
+ ),
5939
+ config.suggestions && /* @__PURE__ */ jsx10(
5940
+ SuggestionHint,
5941
+ {
5942
+ bridge,
5943
+ enabled: config.suggestions,
5944
+ onSuggestionChange: setActiveSuggestion,
5945
+ initialContext
5946
+ }
5947
+ ),
5948
+ /* @__PURE__ */ jsx10(
5949
+ HistorySearch,
5950
+ {
5951
+ history: history ?? [],
5952
+ visible: historySearchVisible,
5953
+ onSelect: (selected) => {
5954
+ setHistorySearchVisible(false);
5955
+ injectedNonce.current += 1;
5956
+ setInjectedInput({ value: selected, nonce: injectedNonce.current });
5957
+ },
5958
+ onDismiss: () => setHistorySearchVisible(false)
5959
+ }
5960
+ ),
5961
+ /* @__PURE__ */ jsx10(ApprovalHandler, { bridge }),
5962
+ state.notification && /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: state.notification }),
5963
+ state.phase === "input" && !historySearchVisible ? /* @__PURE__ */ jsx10(
5588
5964
  BorderedInput,
5589
5965
  {
5590
5966
  sessionIdentifier: state.sessionIdentifier,
@@ -5594,10 +5970,12 @@ var CopairApp = forwardRef(function CopairApp2({
5594
5970
  completionEngine,
5595
5971
  onSubmit: handleSubmit,
5596
5972
  onHistoryAppend,
5597
- onSlashCommand
5973
+ onSlashCommand: handleSlashCommand,
5974
+ activeSuggestion,
5975
+ injectedValue: injectedInput
5598
5976
  }
5599
5977
  ) : null,
5600
- /* @__PURE__ */ jsx6(
5978
+ /* @__PURE__ */ jsx10(
5601
5979
  StatusBar,
5602
5980
  {
5603
5981
  bridge,
@@ -5614,7 +5992,7 @@ function renderApp(bridge, model, options) {
5614
5992
  imperativeHandle = handle;
5615
5993
  };
5616
5994
  const instance = render(
5617
- /* @__PURE__ */ jsx6(
5995
+ /* @__PURE__ */ jsx10(
5618
5996
  CopairApp,
5619
5997
  {
5620
5998
  ref: appRef,
@@ -5627,7 +6005,8 @@ function renderApp(bridge, model, options) {
5627
6005
  onMessage: options?.onMessage,
5628
6006
  onHistoryAppend: options?.onHistoryAppend,
5629
6007
  onSlashCommand: options?.onSlashCommand,
5630
- onExit: options?.onExit
6008
+ onExit: options?.onExit,
6009
+ initialContext: options?.initialContext
5631
6010
  }
5632
6011
  ),
5633
6012
  { exitOnCtrlC: false }
@@ -5975,7 +6354,7 @@ import chalk6 from "chalk";
5975
6354
  // package.json
5976
6355
  var package_default = {
5977
6356
  name: "@dugleelabs/copair",
5978
- version: "1.2.0",
6357
+ version: "1.3.0",
5979
6358
  description: "Model-agnostic AI coding agent for the terminal",
5980
6359
  type: "module",
5981
6360
  main: "dist/index.js",
@@ -6815,6 +7194,23 @@ async function runAuditCommand(argv) {
6815
7194
  }
6816
7195
 
6817
7196
  // src/index.ts
7197
+ function detectTestFramework(cwd) {
7198
+ const patterns = [
7199
+ "vitest.config.ts",
7200
+ "vitest.config.js",
7201
+ "vitest.config.mjs",
7202
+ "jest.config.ts",
7203
+ "jest.config.js",
7204
+ "jest.config.mjs"
7205
+ ];
7206
+ if (patterns.some((f) => existsSync18(join15(cwd, f)))) return true;
7207
+ try {
7208
+ const pkg3 = JSON.parse(readFileSync10(join15(cwd, "package.json"), "utf8"));
7209
+ return Boolean(pkg3.scripts?.test);
7210
+ } catch {
7211
+ return false;
7212
+ }
7213
+ }
6818
7214
  function resolveModel(config, modelOverride) {
6819
7215
  const modelAlias = modelOverride ?? config.default_model;
6820
7216
  if (!modelAlias) {
@@ -6906,19 +7302,6 @@ async function main() {
6906
7302
  const agentBridge = new AgentBridge();
6907
7303
  gate.setBridge(agentBridge);
6908
7304
  const mcpManager = new McpClientManager();
6909
- if (config.mcp_servers.length > 0) {
6910
- setImmediate(async () => {
6911
- try {
6912
- await mcpManager.initialize(config.mcp_servers);
6913
- const bridge = new McpBridge(mcpManager, toolRegistry);
6914
- await bridge.registerAll();
6915
- } catch (err) {
6916
- const msg = err instanceof Error ? err.message : String(err);
6917
- process.stderr.write(`[mcp] Failed to initialize MCP servers: ${msg}
6918
- `);
6919
- }
6920
- });
6921
- }
6922
7305
  gate.addTrustedPath(join15(cwd, ".copair"));
6923
7306
  const gitCtx = detectGitContext(cwd);
6924
7307
  const knowledgeManager = new KnowledgeManager({
@@ -7060,6 +7443,12 @@ Environment:
7060
7443
  uiConfig: config.ui,
7061
7444
  history: inputHistory,
7062
7445
  completionEngine,
7446
+ initialContext: {
7447
+ hasTestFramework: detectTestFramework(cwd),
7448
+ // Session picker already ran before ink — user chose resume or fresh.
7449
+ // No need to re-suggest resuming.
7450
+ sessionCount: 0
7451
+ },
7063
7452
  onHistoryAppend: (entry) => {
7064
7453
  inputHistory.push(entry);
7065
7454
  appendHistory(historyPath, entry);
@@ -7146,6 +7535,18 @@ Environment:
7146
7535
  agentBridge.emit("turn-complete");
7147
7536
  }
7148
7537
  });
7538
+ if (config.mcp_servers.length > 0) {
7539
+ setImmediate(async () => {
7540
+ try {
7541
+ await mcpManager.initialize(config.mcp_servers);
7542
+ const bridge = new McpBridge(mcpManager, toolRegistry);
7543
+ await bridge.registerAll();
7544
+ } catch (err) {
7545
+ const msg = err instanceof Error ? err.message : String(err);
7546
+ agentBridge.emit("error", `[mcp] Failed to initialize MCP servers: ${msg}`);
7547
+ }
7548
+ });
7549
+ }
7149
7550
  await appHandle.waitForExit().then(doExit);
7150
7551
  }
7151
7552
  if (process.argv[2] === "audit") {