@lazyingart/agintiflow 0.19.2 → 0.20.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.
@@ -34,6 +34,8 @@ Interactive commands:
34
34
  ```text
35
35
  /models
36
36
  /venice
37
+ /route
38
+ /model
37
39
  /route deepseek/deepseek-v4-flash
38
40
  /model deepseek/deepseek-v4-pro
39
41
  /spare openai/gpt-5.4 medium
@@ -41,6 +43,8 @@ Interactive commands:
41
43
  /auxiliary model grsai/nano-banana-2
42
44
  ```
43
45
 
46
+ In the interactive CLI, `/route` and `/model` without arguments open a selector. Use Up/Down/Left/Right to move through choices, Enter to confirm, and Esc to cancel. Slash-command hints use the same arrow selection behavior: type a prefix such as `/mo`, use arrows to choose `/model` or `/models`, then press Enter or Tab.
47
+
44
48
  `/venice` is a shortcut for:
45
49
 
46
50
  ```text
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.19.2",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a web-first coding agent and CLI with DeepSeek routing, sandboxed tools, model providers, canvas artifacts, and optional wrappers.",
6
6
  "license": "Apache-2.0",
@@ -137,9 +137,28 @@ try {
137
137
  const queuedText = queuedPromptLayout.renderedRows
138
138
  .map((line) => line.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, ""))
139
139
  .join("\n");
140
- if (!queuedText.includes("run running · tool: apply_patch") || !queuedText.includes("→ apply this") || !queuedText.includes("↳ run this") || !queuedText.includes("cwd /tmp/aginti-project")) {
140
+ if (
141
+ !queuedText.includes("run running · tool: apply_patch") ||
142
+ !queuedText.includes("→ apply this") ||
143
+ !queuedText.includes("↳ run this") ||
144
+ !queuedText.includes("cwd /tmp/aginti-project")
145
+ ) {
141
146
  throw new Error("terminal prompt layout did not render live input queue and cwd footer");
142
147
  }
148
+ const hintPromptLayout = buildPromptLayout("/mo", 3, 90, 24, { suggestions: ["/models", "/model"], suggestionIndex: 1 });
149
+ const hintText = hintPromptLayout.renderedRows
150
+ .map((line) => line.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, ""))
151
+ .join("\n");
152
+ if (!hintText.includes("user> /mo") || !hintText.includes("hint /models >/model")) {
153
+ throw new Error("terminal prompt layout did not align user and hint text columns");
154
+ }
155
+ const exactHintLayout = buildPromptLayout("/model", 6, 90, 24);
156
+ const exactHintText = exactHintLayout.renderedRows
157
+ .map((line) => line.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, ""))
158
+ .join("\n");
159
+ if (!exactHintText.includes("hint >/model") || exactHintText.includes("/models")) {
160
+ throw new Error("exact slash commands should not show broader prefix matches");
161
+ }
143
162
  if (classifyEscapeAction({ active: false }) !== "noop") {
144
163
  throw new Error("idle Esc should not redraw or clear the prompt");
145
164
  }
@@ -171,9 +171,20 @@ function promptGutter() {
171
171
  function commandSuggestions(line = "") {
172
172
  const trimmed = String(line || "");
173
173
  if (!trimmed.startsWith("/") || /\s/.test(trimmed)) return [];
174
+ if (SLASH_COMMANDS.includes(trimmed)) return [trimmed];
174
175
  return SLASH_COMMANDS.filter((command) => command.startsWith(trimmed)).slice(0, 8);
175
176
  }
176
177
 
178
+ function renderSuggestionList(suggestions = [], selectedIndex = 0) {
179
+ return suggestions
180
+ .map((suggestion, index) => {
181
+ const active = index === selectedIndex;
182
+ if (!active) return suggestion;
183
+ return useColor ? color(suggestion, ansi.userBg, ansi.bold) : `>${suggestion}`;
184
+ })
185
+ .join(" ");
186
+ }
187
+
177
188
  function resolveSlashCommand(command = "") {
178
189
  const raw = String(command || "").trim();
179
190
  if (!raw) return raw;
@@ -574,9 +585,9 @@ function printHelp() {
574
585
  " /memory Alias for /instructions.",
575
586
  " /models Show route/main/spare/wrapper/auxiliary model roles.",
576
587
  " /venice Shortcut: use Venice Uncensored 1.2 for route and main roles.",
577
- " /route <mode|provider/model>",
578
- " Set routing mode or fast route model, e.g. /route deepseek/deepseek-v4-flash.",
579
- " /model <provider/model> Set the active/main model, e.g. /model deepseek/deepseek-v4-pro.",
588
+ " /route [mode|provider/model]",
589
+ " Open route selector, or set routing/fast route model.",
590
+ " /model [provider/model] Open main-model selector, or set the active/main model.",
580
591
  " /spare <provider/model> [reasoning]",
581
592
  " Set spare model, e.g. /spare openai/gpt-5.4 medium.",
582
593
  " /wrapper [on|off|codex model reasoning]",
@@ -730,14 +741,17 @@ export function buildPromptLayout(buffer = "", cursor = 0, width = terminalWidth
730
741
  }
731
742
  }
732
743
 
733
- const suggestions = commandSuggestions(safeBuffer.split("\n")[0] || "");
744
+ const suggestions = Array.isArray(options.suggestions)
745
+ ? options.suggestions
746
+ : commandSuggestions(safeBuffer.split("\n")[0] || "");
747
+ const suggestionIndex = clamp(Number(options.suggestionIndex) || 0, 0, Math.max(suggestions.length - 1, 0));
734
748
  const emptyHint = "type a request, /help, Enter to send, Ctrl+J for newline";
735
749
  const visible = promptVisibleWindow(rows, cursorRow, height);
736
750
  const renderedRows = [];
737
751
  let renderedCursorRow = cursorRow - visible.start;
738
752
 
739
753
  if (options.statusLine) {
740
- renderedRows.push(panelLine(` run ${compactLine(options.statusLine, lineWidth - 8)}`, ansi.statusBg, lineWidth));
754
+ renderedRows.push(panelLine(`${labelText("run", { prompt: true })}${compactLine(options.statusLine, lineWidth - 10)}`, ansi.statusBg, lineWidth));
741
755
  renderedCursorRow += 1;
742
756
  }
743
757
 
@@ -767,10 +781,10 @@ export function buildPromptLayout(buffer = "", cursor = 0, width = terminalWidth
767
781
  }
768
782
 
769
783
  if (suggestions.length > 0) {
770
- renderedRows.push(panelLine(` hint ${suggestions.join(" ")}`, ansi.systemBg, lineWidth));
784
+ renderedRows.push(panelLine(`${labelText("hint", { prompt: true })}${renderSuggestionList(suggestions, suggestionIndex)}`, ansi.systemBg, lineWidth));
771
785
  }
772
786
  if (options.commandCwd) {
773
- renderedRows.push(panelLine(` cwd ${options.commandCwd}`, ansi.systemBg, lineWidth));
787
+ renderedRows.push(panelLine(`${labelText("cwd", { prompt: true })}${options.commandCwd}`, ansi.systemBg, lineWidth));
774
788
  }
775
789
 
776
790
  return {
@@ -884,6 +898,8 @@ function readTtyPrompt(options = {}) {
884
898
  let historyIndex = promptHistory.length;
885
899
  let draft = "";
886
900
  let redrawHandle = null;
901
+ let suggestionAnchor = "";
902
+ let suggestionIndex = 0;
887
903
 
888
904
  const cleanup = () => {
889
905
  if (redrawHandle) {
@@ -901,14 +917,19 @@ function readTtyPrompt(options = {}) {
901
917
  clearImmediate(redrawHandle);
902
918
  redrawHandle = null;
903
919
  }
904
- rendered = renderPromptBuffer(buffer, cursor, rendered, options);
920
+ const suggestions = commandSuggestions(suggestionAnchor || buffer.split("\n")[0] || "");
921
+ rendered = renderPromptBuffer(buffer, cursor, rendered, {
922
+ ...options,
923
+ suggestions,
924
+ suggestionIndex,
925
+ });
905
926
  };
906
927
 
907
928
  const redraw = () => {
908
929
  if (redrawHandle) return;
909
930
  redrawHandle = setImmediate(() => {
910
931
  redrawHandle = null;
911
- rendered = renderPromptBuffer(buffer, cursor, rendered, options);
932
+ renderNow();
912
933
  });
913
934
  };
914
935
 
@@ -922,13 +943,33 @@ function readTtyPrompt(options = {}) {
922
943
  resolve(buffer);
923
944
  };
924
945
 
925
- const setBuffer = (nextBuffer, nextCursor = nextBuffer.length) => {
946
+ const setBuffer = (nextBuffer, nextCursor = nextBuffer.length, { keepSuggestionAnchor = false } = {}) => {
926
947
  buffer = nextBuffer;
927
948
  cursor = clamp(nextCursor, 0, buffer.length);
928
949
  preferredColumn = null;
950
+ if (!keepSuggestionAnchor) {
951
+ suggestionAnchor = "";
952
+ suggestionIndex = 0;
953
+ }
929
954
  redraw();
930
955
  };
931
956
 
957
+ const cycleSuggestion = (delta) => {
958
+ const firstLine = buffer.split("\n")[0] || "";
959
+ if (!suggestionAnchor) suggestionAnchor = firstLine;
960
+ const suggestions = commandSuggestions(suggestionAnchor);
961
+ if (suggestions.length === 0) {
962
+ suggestionAnchor = "";
963
+ suggestionIndex = 0;
964
+ return false;
965
+ }
966
+ suggestionIndex = (suggestionIndex + delta + suggestions.length) % suggestions.length;
967
+ setBuffer(suggestions[suggestionIndex], suggestions[suggestionIndex].length, { keepSuggestionAnchor: true });
968
+ return true;
969
+ };
970
+
971
+ const suggestionModeActive = () => commandSuggestions(suggestionAnchor || buffer.split("\n")[0] || "").length > 1;
972
+
932
973
  const moveVertical = (direction) => {
933
974
  const layout = buildPromptLayout(buffer, cursor);
934
975
  const location = cursorLocation(layout, cursor);
@@ -979,32 +1020,40 @@ function readTtyPrompt(options = {}) {
979
1020
  if (key.name === "backspace") {
980
1021
  ({ buffer, cursor } = removeBefore(buffer, cursor));
981
1022
  preferredColumn = null;
1023
+ suggestionAnchor = "";
1024
+ suggestionIndex = 0;
982
1025
  redraw();
983
1026
  return;
984
1027
  }
985
1028
  if (key.name === "delete") {
986
1029
  ({ buffer, cursor } = removeAt(buffer, cursor));
987
1030
  preferredColumn = null;
1031
+ suggestionAnchor = "";
1032
+ suggestionIndex = 0;
988
1033
  redraw();
989
1034
  return;
990
1035
  }
991
1036
  if (key.name === "left") {
1037
+ if (suggestionModeActive() && cycleSuggestion(-1)) return;
992
1038
  cursor = Math.max(cursor - 1, 0);
993
1039
  preferredColumn = null;
994
1040
  redraw();
995
1041
  return;
996
1042
  }
997
1043
  if (key.name === "right") {
1044
+ if (suggestionModeActive() && cycleSuggestion(1)) return;
998
1045
  cursor = Math.min(cursor + 1, buffer.length);
999
1046
  preferredColumn = null;
1000
1047
  redraw();
1001
1048
  return;
1002
1049
  }
1003
1050
  if (key.name === "up") {
1051
+ if (suggestionModeActive() && cycleSuggestion(-1)) return;
1004
1052
  moveVertical(-1);
1005
1053
  return;
1006
1054
  }
1007
1055
  if (key.name === "down") {
1056
+ if (suggestionModeActive() && cycleSuggestion(1)) return;
1008
1057
  moveVertical(1);
1009
1058
  return;
1010
1059
  }
@@ -1036,10 +1085,12 @@ function readTtyPrompt(options = {}) {
1036
1085
  return;
1037
1086
  }
1038
1087
  if (key.name === "tab") {
1039
- const suggestions = commandSuggestions(buffer.split("\n")[0] || "");
1040
- if (suggestions.length === 1) {
1041
- buffer = suggestions[0];
1088
+ const suggestions = commandSuggestions(suggestionAnchor || buffer.split("\n")[0] || "");
1089
+ if (suggestions.length > 0) {
1090
+ buffer = suggestions[clamp(suggestionIndex, 0, suggestions.length - 1)];
1042
1091
  cursor = buffer.length;
1092
+ suggestionAnchor = "";
1093
+ suggestionIndex = 0;
1043
1094
  }
1044
1095
  preferredColumn = null;
1045
1096
  redraw();
@@ -1054,6 +1105,8 @@ function readTtyPrompt(options = {}) {
1054
1105
  const text = str.replace(/\r/g, "");
1055
1106
  ({ buffer, cursor } = insertAt(buffer, cursor, text));
1056
1107
  preferredColumn = null;
1108
+ suggestionAnchor = "";
1109
+ suggestionIndex = 0;
1057
1110
  redraw();
1058
1111
  }
1059
1112
  };
@@ -1601,6 +1654,193 @@ function printModelRoles(state) {
1601
1654
  );
1602
1655
  }
1603
1656
 
1657
+ function modelRoleChoices(role = "main") {
1658
+ const common = [
1659
+ {
1660
+ provider: "deepseek",
1661
+ model: "deepseek-v4-flash",
1662
+ label: "DeepSeek V4 Flash",
1663
+ description: "fast planner, short shell/browser/code work",
1664
+ route: true,
1665
+ },
1666
+ {
1667
+ provider: "deepseek",
1668
+ model: "deepseek-v4-pro",
1669
+ label: "DeepSeek V4 Pro",
1670
+ description: "complex coding, debugging, writing, and long tasks",
1671
+ main: true,
1672
+ },
1673
+ {
1674
+ provider: "venice",
1675
+ model: "venice-uncensored-1-2",
1676
+ label: "Venice Uncensored 1.2",
1677
+ description: "Venice text route; use /auth venice if missing",
1678
+ route: true,
1679
+ main: true,
1680
+ },
1681
+ {
1682
+ provider: "openai",
1683
+ model: "gpt-5.5",
1684
+ label: "OpenAI GPT-5.5",
1685
+ description: "frontier spare/manual model",
1686
+ main: true,
1687
+ },
1688
+ {
1689
+ provider: "openai",
1690
+ model: "gpt-5.4",
1691
+ label: "OpenAI GPT-5.4",
1692
+ description: "strong everyday coding spare",
1693
+ main: true,
1694
+ },
1695
+ {
1696
+ provider: "openai",
1697
+ model: "gpt-5.4-mini",
1698
+ label: "OpenAI GPT-5.4 Mini",
1699
+ description: "small fast spare route",
1700
+ route: true,
1701
+ },
1702
+ {
1703
+ provider: "qwen",
1704
+ model: "qwen-plus",
1705
+ label: "Qwen Plus",
1706
+ description: "general Qwen route",
1707
+ route: true,
1708
+ },
1709
+ {
1710
+ provider: "qwen",
1711
+ model: "qwen-max",
1712
+ label: "Qwen Max",
1713
+ description: "larger Qwen route",
1714
+ main: true,
1715
+ },
1716
+ {
1717
+ provider: "mock",
1718
+ model: "mock-agent",
1719
+ label: "Mock Agent",
1720
+ description: "local deterministic test route",
1721
+ route: true,
1722
+ main: true,
1723
+ },
1724
+ ];
1725
+ const filtered = common.filter((item) => (role === "route" ? item.route : item.main));
1726
+ return role === "route"
1727
+ ? filtered.sort((a, b) => Number(b.model === "deepseek-v4-flash") - Number(a.model === "deepseek-v4-flash"))
1728
+ : filtered.sort((a, b) => Number(b.model === "deepseek-v4-pro") - Number(a.model === "deepseek-v4-pro"));
1729
+ }
1730
+
1731
+ function modelChoiceLine(option) {
1732
+ return `${option.label} ${option.provider}/${option.model} ${option.description}`;
1733
+ }
1734
+
1735
+ function clearSelector(lineCount) {
1736
+ for (let index = 0; index < lineCount; index += 1) {
1737
+ output.write("\x1b[1A\r\x1b[2K");
1738
+ }
1739
+ }
1740
+
1741
+ function renderSelector({ title, subtitle, options, selectedIndex, lineCount = 0 }) {
1742
+ if (lineCount > 0) clearSelector(lineCount);
1743
+ const width = Math.min(Math.max(terminalWidth() - 2, 60), 110);
1744
+ const bodyWidth = width - 4;
1745
+ const rows = [
1746
+ `╭${"─".repeat(width - 2)}╮`,
1747
+ `│ ${padVisible(title, bodyWidth)} │`,
1748
+ `│ ${padVisible(subtitle, bodyWidth)} │`,
1749
+ `├${"─".repeat(width - 2)}┤`,
1750
+ ...options.map((option, index) => {
1751
+ const marker = index === selectedIndex ? ">" : " ";
1752
+ const rendered = `${marker} ${compactLine(modelChoiceLine(option), bodyWidth - 2)}`;
1753
+ return `│ ${padVisible(index === selectedIndex ? color(rendered, ansi.userBg, ansi.bold) : rendered, bodyWidth)} │`;
1754
+ }),
1755
+ `╰${"─".repeat(width - 2)}╯`,
1756
+ ];
1757
+ output.write(`${rows.join("\n")}\n`);
1758
+ return rows.length;
1759
+ }
1760
+
1761
+ function selectModelChoice({ title, subtitle, options, initialIndex = 0 }) {
1762
+ if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") return Promise.resolve(null);
1763
+ return new Promise((resolve) => {
1764
+ emitKeypressEvents(input);
1765
+ const wasRaw = Boolean(input.isRaw);
1766
+ let selectedIndex = clamp(initialIndex, 0, Math.max(options.length - 1, 0));
1767
+ let lineCount = 0;
1768
+ const cleanup = () => {
1769
+ input.off("keypress", handler);
1770
+ if (typeof input.setRawMode === "function") input.setRawMode(wasRaw);
1771
+ input.pause();
1772
+ output.write(ansi.cursorShow);
1773
+ };
1774
+ const redraw = () => {
1775
+ lineCount = renderSelector({ title, subtitle, options, selectedIndex, lineCount });
1776
+ };
1777
+ const finish = (value) => {
1778
+ if (lineCount > 0) clearSelector(lineCount);
1779
+ cleanup();
1780
+ resolve(value);
1781
+ };
1782
+ const handler = (_str = "", key = {}) => {
1783
+ if (key.ctrl && key.name === "c") {
1784
+ finish(null);
1785
+ return;
1786
+ }
1787
+ if (key.name === "escape") {
1788
+ finish(null);
1789
+ return;
1790
+ }
1791
+ if (key.name === "up" || key.name === "left") {
1792
+ selectedIndex = (selectedIndex - 1 + options.length) % options.length;
1793
+ redraw();
1794
+ return;
1795
+ }
1796
+ if (key.name === "down" || key.name === "right" || key.name === "tab") {
1797
+ selectedIndex = (selectedIndex + 1) % options.length;
1798
+ redraw();
1799
+ return;
1800
+ }
1801
+ if (key.name === "return" || key.name === "enter" || key.sequence === "\r") {
1802
+ finish(options[selectedIndex]);
1803
+ }
1804
+ };
1805
+ input.resume();
1806
+ input.setRawMode(true);
1807
+ input.on("keypress", handler);
1808
+ output.write(ansi.cursorHide);
1809
+ redraw();
1810
+ });
1811
+ }
1812
+
1813
+ async function pickModelRole(role, state) {
1814
+ const options = modelRoleChoices(role);
1815
+ const currentProvider = role === "route" ? state.routeProvider || "deepseek" : state.mainProvider || state.provider || "deepseek";
1816
+ const currentModel = role === "route" ? state.routeModel || "deepseek-v4-flash" : state.mainModel || "deepseek-v4-pro";
1817
+ const initialIndex = Math.max(
1818
+ options.findIndex((item) => item.provider === currentProvider && item.model === currentModel),
1819
+ 0
1820
+ );
1821
+ const selected = await selectModelChoice({
1822
+ title: role === "route" ? "Select route model" : "Select main model",
1823
+ subtitle: "Up/Down/Left/Right selects, Enter confirms, Esc cancels.",
1824
+ options,
1825
+ initialIndex,
1826
+ });
1827
+ if (!selected) return false;
1828
+
1829
+ state.routingMode = "smart";
1830
+ state.provider = "deepseek";
1831
+ state.model = "";
1832
+ if (role === "route") {
1833
+ state.routeProvider = selected.provider;
1834
+ state.routeModel = selected.model;
1835
+ printSystemLine(`route=${state.routeProvider}/${state.routeModel}`);
1836
+ } else {
1837
+ state.mainProvider = selected.provider;
1838
+ state.mainModel = selected.model;
1839
+ printSystemLine(`main=${state.mainProvider}/${state.mainModel}`);
1840
+ }
1841
+ return true;
1842
+ }
1843
+
1604
1844
  async function maybeOnboardDeepSeekKey(state) {
1605
1845
  if (!shouldPromptForDeepSeek(state, process.cwd())) return;
1606
1846
 
@@ -1843,6 +2083,11 @@ async function handleCommand(line, state, packageDir) {
1843
2083
  }
1844
2084
  if (command === "route") {
1845
2085
  if (!value) {
2086
+ if (input.isTTY && output.isTTY && typeof input.setRawMode === "function") {
2087
+ const changed = await pickModelRole("route", state);
2088
+ if (!changed) printSystemLine(`route=${state.routeProvider || "deepseek"}/${state.routeModel || "deepseek-v4-flash"}`);
2089
+ return true;
2090
+ }
1846
2091
  printAgentMessage(
1847
2092
  `Route model: ${state.routeProvider || "deepseek"}/${state.routeModel || "deepseek-v4-flash"}\nUse /route deepseek/deepseek-v4-flash or /route fast|smart|complex.`
1848
2093
  );
@@ -1860,7 +2105,18 @@ async function handleCommand(line, state, packageDir) {
1860
2105
  return true;
1861
2106
  }
1862
2107
  if (command === "main" || command === "model") {
1863
- if (!value || value === "auto") {
2108
+ if (!value) {
2109
+ if (input.isTTY && output.isTTY && typeof input.setRawMode === "function") {
2110
+ const changed = await pickModelRole("main", state);
2111
+ if (!changed) printSystemLine(`main=${state.mainProvider || state.provider || "deepseek"}/${state.mainModel || state.model || "deepseek-v4-pro"}`);
2112
+ return true;
2113
+ }
2114
+ printAgentMessage(
2115
+ `Main model: ${state.mainProvider || state.provider || "deepseek"}/${state.mainModel || state.model || "deepseek-v4-pro"}\nUse /model deepseek/deepseek-v4-pro or /model auto.`
2116
+ );
2117
+ return true;
2118
+ }
2119
+ if (value === "auto") {
1864
2120
  if (command === "main") {
1865
2121
  state.mainProvider = "";
1866
2122
  state.mainModel = "";