fluxflow-cli 1.5.3 → 1.6.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/TOOLS.md CHANGED
@@ -1,52 +1,52 @@
1
- # 🧰 Agent Tools & Capabilities
2
-
3
- Flux Flow provides a robust set of tools that allow the AI to interact with the file system, execute code, and search the web. The availability of these tools depends on the active operating mode.
4
-
5
- ## Tool Availability by Mode
6
-
7
- | Tool | Flux Mode (Dev) | Flow Mode (Chat) |
8
- | :--- | :---: | :---: |
9
- | **Web Search** | ✅ | ✅ |
10
- | **Web Scrape** | ✅ | ✅ |
11
- | **Write PDF** | ✅ | ✅ |
12
- | **View/Read Files** | ✅ | ❌ |
13
- | **Write/Update Files** | ✅ | ❌ |
14
- | **Execute Commands** | ✅ | ❌ |
15
-
16
- ---
17
-
18
- ## Core Tools
19
-
20
- ### 🌐 Web & Research
21
- - **`web_search`**: Uses DuckDuckGo to find up-to-date information on the internet. Crucial for answering questions about recent events or unlearned documentation.
22
- - **`web_scrape`**: Extracts the detailed text content from a specific URL, allowing the agent to read documentation or articles.
23
- - **`write_pdf`**: Generates high-fidelity, branded PDF documents from HTML/CSS. Features automatic watermarking and page-aware layout management.
24
-
25
- ### 📁 File System Operations
26
- - **`list_files`**: Lists the contents of a directory to help the agent understand the project structure.
27
- - **`read_folder`**: Provides detailed statistics and metadata about a directory's contents.
28
- - **`view_file`**: Reads the content of a file.
29
- - **Native Multimodality**: Supports analyzing images (JPG, PNG, WEBP) and PDF documents. The tool automatically detects binary formats and encodes them for AI analysis.
30
- - **Text Reading**: Supports specific line ranges (`start_line`, `end_line`) to manage context size efficiently.
31
-
32
- ### ✍️ Code Editing
33
- - **`write_file`**: Creates a new file or completely overwrites an existing one with new content.
34
- - **`update_file` (Smart Patching)**: Surgically replaces a specific block of text within a file.
35
- - *Diff Generation*: It returns a high-fidelity visual diff (Red/Green changes with context lines) to the UI, allowing the user to see exactly what the agent modified.
36
-
37
- ### 💻 Terminal Execution
38
- - **`exec_command`**: Runs a shell command directly in the terminal using Node's `child_process.spawn`.
39
- - *Context Aware*: Runs in the current working directory.
40
- - *Cross-Platform*: Uses `shell: true` to handle Windows `.cmd`/`.bat` files natively.
41
-
42
- ---
43
-
44
- ## Memory Management
45
-
46
- The memory tool (`memory.js`) is primarily used by the background **Janitor** model, but can be accessed by the main agent if necessary.
47
-
48
- - **Temporary Context (`action='temp'`)**: Saves a rolling summary of the current session to maintain conversational context without bloating the main prompt history.
49
- - **Persistent User Memory (`action='user'`)**:
50
- - The Janitor analyzes conversations to detect user preferences, hobbies, or instructions.
51
- - It uses `add`, `update`, or `delete` methods to manage facts in the encrypted `memories.json` vault.
1
+ # 🧰 Agent Tools & Capabilities
2
+
3
+ Flux Flow provides a robust set of tools that allow the AI to interact with the file system, execute code, and search the web. The availability of these tools depends on the active operating mode.
4
+
5
+ ## Tool Availability by Mode
6
+
7
+ | Tool | Flux Mode (Dev) | Flow Mode (Chat) |
8
+ | :--- | :---: | :---: |
9
+ | **Web Search** | ✅ | ✅ |
10
+ | **Web Scrape** | ✅ | ✅ |
11
+ | **Write PDF** | ✅ | ✅ |
12
+ | **View/Read Files** | ✅ | ❌ |
13
+ | **Write/Update Files** | ✅ | ❌ |
14
+ | **Execute Commands** | ✅ | ❌ |
15
+
16
+ ---
17
+
18
+ ## Core Tools
19
+
20
+ ### 🌐 Web & Research
21
+ - **`web_search`**: Uses DuckDuckGo to find up-to-date information on the internet. Crucial for answering questions about recent events or unlearned documentation.
22
+ - **`web_scrape`**: Extracts the detailed text content from a specific URL, allowing the agent to read documentation or articles.
23
+ - **`write_pdf`**: Generates high-fidelity, branded PDF documents from HTML/CSS. Features automatic watermarking and page-aware layout management.
24
+
25
+ ### 📁 File System Operations
26
+ - **`list_files`**: Lists the contents of a directory to help the agent understand the project structure.
27
+ - **`read_folder`**: Provides detailed statistics and metadata about a directory's contents.
28
+ - **`view_file`**: Reads the content of a file.
29
+ - **Native Multimodality**: Supports analyzing images (JPG, PNG, WEBP) and PDF documents. The tool automatically detects binary formats and encodes them for AI analysis.
30
+ - **Text Reading**: Supports specific line ranges (`start_line`, `end_line`) to manage context size efficiently.
31
+
32
+ ### ✍️ Code Editing
33
+ - **`write_file`**: Creates a new file or completely overwrites an existing one with new content.
34
+ - **`update_file` (Smart Patching)**: Surgically replaces a specific block of text within a file.
35
+ - *Diff Generation*: It returns a high-fidelity visual diff (Red/Green changes with context lines) to the UI, allowing the user to see exactly what the agent modified.
36
+
37
+ ### 💻 Terminal Execution
38
+ - **`exec_command`**: Runs a shell command directly in the terminal using Node's `child_process.spawn`.
39
+ - *Context Aware*: Runs in the current working directory.
40
+ - *Cross-Platform*: Uses `shell: true` to handle Windows `.cmd`/`.bat` files natively.
41
+
42
+ ---
43
+
44
+ ## Memory Management
45
+
46
+ The memory tool (`memory.js`) is primarily used by the background **Janitor** model, but can be accessed by the main agent if necessary.
47
+
48
+ - **Temporary Context (`action='temp'`)**: Saves a rolling summary of the current session to maintain conversational context without bloating the main prompt history.
49
+ - **Persistent User Memory (`action='user'`)**:
50
+ - The Janitor analyzes conversations to detect user preferences, hobbies, or instructions.
51
+ - It uses `add`, `update`, or `delete` methods to manage facts in the encrypted `memories.json` vault.
52
52
  - These memories are injected into the system prompt of *future* sessions, allowing Flux Flow to learn and adapt to the user over time.
package/UI_FEATURES.md CHANGED
@@ -1,101 +1,101 @@
1
- # 🎮 User Interface & Interaction Features
2
-
3
- Flux Flow is designed to be a high-performance terminal application. Beyond basic chat, it includes a variety of advanced UI features and human-in-the-loop controls to keep you in command of the agent.
4
-
5
- ## ⌨️ Command System
6
-
7
- You can control the application using `/` commands directly in the chat input:
8
-
9
- - **/mode [flux|flow]**: Quickly switch between **Flux** (Dev) and **Flow** (Chat) modes. Using it without arguments opens the selection menu.
10
- - **/thinking [low|medium|high|max|show|hide]**: Adjust reasoning depth or toggle visibility of the thinking process. Using it without arguments opens the selection menu.
11
- - **/model [name]**: Choose which AI model to use for the main interaction.
12
- - **/key**: Open the API Key management view to update or remove your credentials.
13
- - **/settings**: Access the system configuration menu.
14
- - **/profile**: Update your name, nickname, and custom instructions.
15
- - **/update**: Update Flux Flow to the latest version.
16
- - **/memory**: View and manage the persistent memories extracted by the Janitor.
17
- - **/resume <chat-id>**: Switch back to a previous conversation.
18
- - **/changelog**: Open the project's changelog in your default browser.
19
- - **/help**: List all available commands.
20
-
21
- ## 📁 External Data Sanctuary (Redirection)
22
-
23
- Flux Flow allows you to "Anchor" your data outside of the default user directory. This is perfect for users who want to keep their logs, chat history, and encrypted memories on an external drive, a VeraCrypt volume, or a specific project folder.
24
-
25
- - **How to Enable**: Open `/settings` and toggle **Use External Data**.
26
- - **Portability**: Once set, the application will synchronously "pivot" all data operations to your specified `externalDataPath` upon startup.
27
- - **Privacy**: Keeps sensitive data off your primary system drive.
28
-
29
-
30
- ### Command Shortcuts
31
-
32
- For power users, several commands support direct arguments to skip the menus:
33
- - ` /mode flux ` or ` /mode flow `
34
- - ` /thinking low ` / ` /thinking medium ` / ` /thinking high ` / ` /thinking max `
35
- - ` /thinking show ` / ` /thinking hide ` (Toggles thinking process visibility)
36
- - ` /model gemini-3.1-pro-preview ` (Switches model directly)
37
- - ` /update check ` (Checks for updates)
38
-
39
- ## 🧠 Thinking Levels & Visualization
40
-
41
- Flux Flow separates the model's "internal monologue" (reasoning) from its final response using `<think>` tags.
42
-
43
- - **Thinking Levels**: Depending on the mode, you can choose from **Low**, **Medium**, **High**, or **Max**. Higher levels allow the agent more "space" to reason through complex architecture or debugging problems.
44
- - **Show/Hide Thinking**: You can toggle the visibility of the thinking process using `/thinking show/hide`.
45
- - When **Hidden**, the agent doesn't just disappear; it provides a "minimalist" view showing only the core **Headings** and **Action Steps** (bolded lines) from its reasoning. This keeps you informed of its current "step" without cluttering the screen with detailed internal monologue.
46
-
47
- ## ⚡ Interactive Sub-Terminal
48
-
49
- Flux Flow features a high-fidelity, interactive sub-terminal that allows you to engage with commands spawned by the agent in real-time.
50
-
51
- - **Live Interaction**: When an agent executes a command (like `npm init`, `git commit`, or a custom script), the terminal output appears in a dedicated box.
52
- - **Focus Toggling (`TAB`)**: While a terminal is live, you can press **`TAB`** to shift your keyboard focus from the chat prompt directly into the terminal.
53
- - **Visual Feedback**:
54
- - **Yellow Glow**: When the terminal is focused, its border changes to a "Double Yellow" style to indicate it is capturing input.
55
- - **Status Indicator**: The footer will change from `● LIVE` to `▶ TERMINAL FOCUSED`.
56
- - **Cross-Platform Compatibility**: The input bridge automatically detects your OS and sends the correct line endings (`\r\n` for Windows, `\n` for Unix), ensuring that interactive prompts like `y/n` work seamlessly across environments.
57
- - **Input Mirroring**: Your keystrokes are mirrored in the terminal box in real-time, providing an IDE-grade responsive feel.
58
-
59
- > [!TIP]
60
- > Use the Interactive Sub-Terminal to handle authentication prompts, confirmation dialogs, or to manually steering a long-running process without leaving the Flux Flow interface.
61
-
62
- ## 🛡️ Human-in-the-Loop (HITL) Verification
63
-
64
- Security and safety are paramount when an AI has access to your file system and terminal. Flux Flow implements several layers of verification:
65
-
66
- ### Tool Approval
67
- By default, the agent **cannot** execute dangerous actions without your consent.
68
- - **Terminal Approval**: If the agent attempts to run a shell command (`exec_command`), a dedicated approval screen appears showing the exact command. You can **Allow** or **Deny** the execution.
69
- - **File Approval**: Similar to terminal commands, writing or updating files requires manual approval unless configured otherwise.
70
- - **Safe Commands**: Basic read-only commands (like `ls`, `git status`, `pwd`) are automatically allowed to minimize friction.
71
-
72
- ### Auto-Execution (Advanced)
73
- For power users, **Auto-Exec** can be enabled in `/settings`.
74
- - **⚠️ Warning**: This allows the agent to run any tool and execute any command autonomously.
75
- - **External Access**: You can also toggle whether the agent is allowed to access files outside of its current working directory.
76
-
77
- ## 🔄 Steering & Resolution
78
-
79
- ### Real-time Steering
80
- If you realize the agent is going down the wrong path *while* it is in an agentic loop, you can provide "Steering Hints." The system will inject your feedback into the next loop to course-correct the agent.
81
-
82
- ### Resolution Modal
83
- If the agent finishes its task just as you send a steering hint, a **Resolution Modal** appears. It asks if you want to:
84
- - **Send Anyway**: Start a new loop with your feedback.
85
- - **Edit Prompt**: Refine your feedback before sending.
86
-
87
- ## 📊 Status Bar & Feedback
88
- The bottom of the screen features a dynamic status bar showing:
89
- - **Active Mode** (Flux/Flow)
90
- - **Thinking Level**
91
- - **Token Usage**: Real-time tracking of tokens used in the current session.
92
- - **Agentic Loops**: Counters showing how many times the agent has "looped" to solve the current task.
93
- - **API Status**: Visual feedback when the model is thinking or executing a tool.
94
-
95
- ## 🚑 System Integrity & Self-Healing
96
-
97
- Flux Flow is a "Self-Healing" agent. It actively monitors its own environment to ensure all complex dependencies (like the Chromium engine for PDF generation) are ready for action.
98
-
99
- - **Startup Integrity Check**: On launch, Flux performs a "Heartbeat Check" on its internal engines.
100
- - **Automatic Recovery**: If a dependency is missing, you will see a `🔧 [SYSTEM] Initializing...` message. Flux will autonomously download and configure the required binaries using `pnpm` or `npx` fallbacks, keeping you informed every step of the way.
1
+ # 🎮 User Interface & Interaction Features
2
+
3
+ Flux Flow is designed to be a high-performance terminal application. Beyond basic chat, it includes a variety of advanced UI features and human-in-the-loop controls to keep you in command of the agent.
4
+
5
+ ## ⌨️ Command System
6
+
7
+ You can control the application using `/` commands directly in the chat input:
8
+
9
+ - **/mode [flux|flow]**: Quickly switch between **Flux** (Dev) and **Flow** (Chat) modes. Using it without arguments opens the selection menu.
10
+ - **/thinking [low|medium|high|max|show|hide]**: Adjust reasoning depth or toggle visibility of the thinking process. Using it without arguments opens the selection menu.
11
+ - **/model [name]**: Choose which AI model to use for the main interaction.
12
+ - **/key**: Open the API Key management view to update or remove your credentials.
13
+ - **/settings**: Access the system configuration menu.
14
+ - **/profile**: Update your name, nickname, and custom instructions.
15
+ - **/update**: Update Flux Flow to the latest version.
16
+ - **/memory**: View and manage the persistent memories extracted by the Janitor.
17
+ - **/resume <chat-id>**: Switch back to a previous conversation.
18
+ - **/changelog**: Open the project's changelog in your default browser.
19
+ - **/help**: List all available commands.
20
+
21
+ ## 📁 External Data Sanctuary (Redirection)
22
+
23
+ Flux Flow allows you to "Anchor" your data outside of the default user directory. This is perfect for users who want to keep their logs, chat history, and encrypted memories on an external drive, a VeraCrypt volume, or a specific project folder.
24
+
25
+ - **How to Enable**: Open `/settings` and toggle **Use External Data**.
26
+ - **Portability**: Once set, the application will synchronously "pivot" all data operations to your specified `externalDataPath` upon startup.
27
+ - **Privacy**: Keeps sensitive data off your primary system drive.
28
+
29
+
30
+ ### Command Shortcuts
31
+
32
+ For power users, several commands support direct arguments to skip the menus:
33
+ - ` /mode flux ` or ` /mode flow `
34
+ - ` /thinking low ` / ` /thinking medium ` / ` /thinking high ` / ` /thinking max `
35
+ - ` /thinking show ` / ` /thinking hide ` (Toggles thinking process visibility)
36
+ - ` /model gemini-3.1-pro-preview ` (Switches model directly)
37
+ - ` /update check ` (Checks for updates)
38
+
39
+ ## 🧠 Thinking Levels & Visualization
40
+
41
+ Flux Flow separates the model's "internal monologue" (reasoning) from its final response using `<think>` tags.
42
+
43
+ - **Thinking Levels**: Depending on the mode, you can choose from **Low**, **Medium**, **High**, or **Max**. Higher levels allow the agent more "space" to reason through complex architecture or debugging problems.
44
+ - **Show/Hide Thinking**: You can toggle the visibility of the thinking process using `/thinking show/hide`.
45
+ - When **Hidden**, the agent doesn't just disappear; it provides a "minimalist" view showing only the core **Headings** and **Action Steps** (bolded lines) from its reasoning. This keeps you informed of its current "step" without cluttering the screen with detailed internal monologue.
46
+
47
+ ## ⚡ Interactive Sub-Terminal
48
+
49
+ Flux Flow features a high-fidelity, interactive sub-terminal that allows you to engage with commands spawned by the agent in real-time.
50
+
51
+ - **Live Interaction**: When an agent executes a command (like `npm init`, `git commit`, or a custom script), the terminal output appears in a dedicated box.
52
+ - **Focus Toggling (`TAB`)**: While a terminal is live, you can press **`TAB`** to shift your keyboard focus from the chat prompt directly into the terminal.
53
+ - **Visual Feedback**:
54
+ - **Yellow Glow**: When the terminal is focused, its border changes to a "Double Yellow" style to indicate it is capturing input.
55
+ - **Status Indicator**: The footer will change from `● LIVE` to `▶ TERMINAL FOCUSED`.
56
+ - **Cross-Platform Compatibility**: The input bridge automatically detects your OS and sends the correct line endings (`\r\n` for Windows, `\n` for Unix), ensuring that interactive prompts like `y/n` work seamlessly across environments.
57
+ - **Input Mirroring**: Your keystrokes are mirrored in the terminal box in real-time, providing an IDE-grade responsive feel.
58
+
59
+ > [!TIP]
60
+ > Use the Interactive Sub-Terminal to handle authentication prompts, confirmation dialogs, or to manually steering a long-running process without leaving the Flux Flow interface.
61
+
62
+ ## 🛡️ Human-in-the-Loop (HITL) Verification
63
+
64
+ Security and safety are paramount when an AI has access to your file system and terminal. Flux Flow implements several layers of verification:
65
+
66
+ ### Tool Approval
67
+ By default, the agent **cannot** execute dangerous actions without your consent.
68
+ - **Terminal Approval**: If the agent attempts to run a shell command (`exec_command`), a dedicated approval screen appears showing the exact command. You can **Allow** or **Deny** the execution.
69
+ - **File Approval**: Similar to terminal commands, writing or updating files requires manual approval unless configured otherwise.
70
+ - **Safe Commands**: Basic read-only commands (like `ls`, `git status`, `pwd`) are automatically allowed to minimize friction.
71
+
72
+ ### Auto-Execution (Advanced)
73
+ For power users, **Auto-Exec** can be enabled in `/settings`.
74
+ - **⚠️ Warning**: This allows the agent to run any tool and execute any command autonomously.
75
+ - **External Access**: You can also toggle whether the agent is allowed to access files outside of its current working directory.
76
+
77
+ ## 🔄 Steering & Resolution
78
+
79
+ ### Real-time Steering
80
+ If you realize the agent is going down the wrong path *while* it is in an agentic loop, you can provide "Steering Hints." The system will inject your feedback into the next loop to course-correct the agent.
81
+
82
+ ### Resolution Modal
83
+ If the agent finishes its task just as you send a steering hint, a **Resolution Modal** appears. It asks if you want to:
84
+ - **Send Anyway**: Start a new loop with your feedback.
85
+ - **Edit Prompt**: Refine your feedback before sending.
86
+
87
+ ## 📊 Status Bar & Feedback
88
+ The bottom of the screen features a dynamic status bar showing:
89
+ - **Active Mode** (Flux/Flow)
90
+ - **Thinking Level**
91
+ - **Token Usage**: Real-time tracking of tokens used in the current session.
92
+ - **Agentic Loops**: Counters showing how many times the agent has "looped" to solve the current task.
93
+ - **API Status**: Visual feedback when the model is thinking or executing a tool.
94
+
95
+ ## 🚑 System Integrity & Self-Healing
96
+
97
+ Flux Flow is a "Self-Healing" agent. It actively monitors its own environment to ensure all complex dependencies (like the Chromium engine for PDF generation) are ready for action.
98
+
99
+ - **Startup Integrity Check**: On launch, Flux performs a "Heartbeat Check" on its internal engines.
100
+ - **Automatic Recovery**: If a dependency is missing, you will see a `🔧 [SYSTEM] Initializing...` message. Flux will autonomously download and configure the required binaries using `pnpm` or `npx` fallbacks, keeping you informed every step of the way.
101
101
  - **Silent Maintenance**: Once the engine is ready, you'll receive a `✅ [SYSTEM] All dependencies installed successfully.` confirmation.
package/dist/fluxflow.js CHANGED
@@ -23,39 +23,81 @@ var init_TerminalBox = __esm({
23
23
  });
24
24
 
25
25
  // src/components/ChatLayout.jsx
26
- import React2 from "react";
26
+ import React2, { useState, useEffect, useRef } from "react";
27
27
  import { Box as Box2, Text as Text2 } from "ink";
28
- var cleanSignals, formatThinkText, InlineMarkdown, wrapText, TableRenderer, MarkdownText, DiffLine, DiffBlock, CodeRenderer, MessageItem, ChatLayout, ChatLayout_default;
28
+ var TypewriterText, cleanSignals, formatThinkText, InlineMarkdown, wrapText, TableRenderer, MarkdownText, DiffLine, DiffBlock, CodeRenderer, MessageItem, ChatLayout, ChatLayout_default;
29
29
  var init_ChatLayout = __esm({
30
30
  "src/components/ChatLayout.jsx"() {
31
31
  init_TerminalBox();
32
+ TypewriterText = ({ text, isStreaming, onComplete, columns = 80, color = "white", speed = 50, render }) => {
33
+ const [displayedText, setDisplayedText] = useState("");
34
+ const fullTextRef = useRef(text);
35
+ const displayedTextRef = useRef("");
36
+ useEffect(() => {
37
+ fullTextRef.current = text;
38
+ }, [text]);
39
+ useEffect(() => {
40
+ const timer = setInterval(() => {
41
+ const currentFull = fullTextRef.current;
42
+ const currentDisp = displayedTextRef.current;
43
+ if (currentDisp.length < currentFull.length) {
44
+ const remaining = currentFull.slice(currentDisp.length);
45
+ const match = remaining.match(/^(\S+\s*|\s+)/);
46
+ if (match) {
47
+ const chunk = match[0];
48
+ const gap = currentFull.length - currentDisp.length;
49
+ let revealedChunk = chunk;
50
+ if (gap > 100) {
51
+ const extraMatch = remaining.slice(chunk.length).match(/^(\S+\s*|\s+){0,2}/);
52
+ if (extraMatch) revealedChunk += extraMatch[0];
53
+ }
54
+ const nextText = currentDisp + revealedChunk;
55
+ setDisplayedText(nextText);
56
+ displayedTextRef.current = nextText;
57
+ }
58
+ } else if (!isStreaming) {
59
+ if (onComplete) onComplete();
60
+ clearInterval(timer);
61
+ }
62
+ }, speed);
63
+ return () => clearInterval(timer);
64
+ }, [isStreaming, speed]);
65
+ if (render) return render(displayedText);
66
+ return /* @__PURE__ */ React2.createElement(CodeRenderer, { text: displayedText, columns });
67
+ };
32
68
  cleanSignals = (text) => {
33
69
  if (!text) return text;
34
- const parts = [];
35
- let i = 0;
36
- while (i < text.length) {
37
- const trigger = "tool:functions.";
38
- if (text.substring(i, i + trigger.length).toLowerCase() === trigger) {
39
- let balance = 0;
40
- let foundStart = false;
41
- let j = i;
42
- while (j < text.length) {
43
- if (text[j] === "(") {
70
+ let result = text;
71
+ const trigger = "tool:functions.";
72
+ while (true) {
73
+ const lowerResult = result.toLowerCase();
74
+ const startIdx = lowerResult.indexOf(trigger);
75
+ if (startIdx === -1) break;
76
+ let balance = 0;
77
+ let foundStart = false;
78
+ let inString = null;
79
+ let j = startIdx;
80
+ while (j < result.length) {
81
+ const char = result[j];
82
+ if (!inString && (char === "'" || char === '"' || char === "`")) {
83
+ inString = char;
84
+ } else if (inString && char === inString && result[j - 1] !== "\\") {
85
+ inString = null;
86
+ }
87
+ if (!inString) {
88
+ if (char === "(") {
44
89
  balance++;
45
90
  foundStart = true;
46
- } else if (text[j] === ")") {
91
+ } else if (char === ")") {
47
92
  balance--;
48
93
  }
49
- j++;
50
- if (foundStart && balance === 0) break;
51
94
  }
52
- i = j;
53
- } else {
54
- parts.push(text[i]);
55
- i++;
95
+ j++;
96
+ if (foundStart && balance === 0 && !inString) break;
56
97
  }
98
+ result = result.substring(0, startIdx) + result.substring(j);
57
99
  }
58
- return parts.join("").replace(/^\[TOOL_RESULT\]:\s*/gi, "").split("\n").filter((line) => !line.trim().startsWith("SUCCESS:") && !line.trim().startsWith("ERROR:")).join("\n").replace(/\[\s*(turn\s*:)?\s*(continue|finish)\s*\]/gi, "").replace(/\n\s*turn\s*:\s*(continue|finish)\s*$/gi, "").replace(/\n\nResponded on .*/g, "").replace(/\n\n\[Prompted on: .*\]/g, "").replace(/(\$?\\?\/?\\rightarrow\$?|\$\\rightarrow\$)/gi, "\u2192").replace(/(\$?\\?\/?\\leftarrow\$?|\$\\leftarrow\$)/gi, "\u2190").replace(/(\$?\\?\/?\\uparrow\$?|\$\\uparrow\$)/gi, "\u2191").replace(/(\$?\\?\/?\\downarrow\$?|\$\\downarrow\$)/gi, "\u2193").replace(/(\$?\\?\/?\\leftrightarrow\$?|\$\\leftrightarrow\$)/gi, "\u2194").trim();
100
+ return result.replace(/^\[TOOL_RESULT\]:\s*/gi, "").split("\n").filter((line) => !line.trim().startsWith("SUCCESS:") && !line.trim().startsWith("ERROR:")).join("\n").replace(/\[\s*(turn\s*:)?\s*(continue|finish)\s*\]/gi, "").replace(/\n\s*turn\s*:\s*(continue|finish)\s*$/gi, "").replace(/\n\nResponded on .*/g, "").replace(/\n\n\[Prompted on: .*\]/g, "").replace(/(\$?\\?\/?\\rightarrow\$?|\$\\rightarrow\$)/gi, "\u2192").replace(/(\$?\\?\/?\\leftarrow\$?|\$\\leftarrow\$)/gi, "\u2190").replace(/(\$?\\?\/?\\uparrow\$?|\$\\uparrow\$)/gi, "\u2191").replace(/(\$?\\?\/?\\downarrow\$?|\$\\downarrow\$)/gi, "\u2193").replace(/(\$?\\?\/?\\leftrightarrow\$?|\$\\leftrightarrow\$)/gi, "\u2194").trim();
59
101
  };
60
102
  formatThinkText = (cleaned, columns = 80) => {
61
103
  if (!cleaned) return null;
@@ -257,6 +299,9 @@ var init_ChatLayout = __esm({
257
299
  MessageItem = React2.memo(({ msg, showFullThinking, columns = 80 }) => {
258
300
  const isDiffResult = msg.role === "system" && msg.text?.includes("[DIFF_START]");
259
301
  const isTerminalRecord = msg.isTerminalRecord;
302
+ if (msg.isVisualFeedback) {
303
+ return /* @__PURE__ */ React2.createElement(Box2, { marginBottom: 1, paddingX: 1, width: "100%" }, /* @__PURE__ */ React2.createElement(Text2, { color: "white" }, msg.text));
304
+ }
260
305
  if (msg.role === "system" && msg.text?.includes("[TOOL_RESULT]") && !isDiffResult && !isTerminalRecord) return null;
261
306
  if (msg.isAskRecord) {
262
307
  const selectionMatch = msg.text.match(/Selection: (.*)/);
@@ -290,7 +335,11 @@ var init_ChatLayout = __esm({
290
335
  const outputList = outputMatch ? outputMatch[1] : "";
291
336
  return /* @__PURE__ */ React2.createElement(Box2, { marginBottom: 1, paddingX: 1, width: "100%" }, /* @__PURE__ */ React2.createElement(TerminalBox, { command: cmd, output: outputList, completed: true }));
292
337
  }
338
+ const [animationDone, setAnimationDone] = React2.useState(!msg.isStreaming);
293
339
  const content = React2.useMemo(() => cleanSignals(msg.text), [msg.text]);
340
+ React2.useEffect(() => {
341
+ if (msg.isStreaming) setAnimationDone(false);
342
+ }, [msg.id]);
294
343
  const finalContent = React2.useMemo(() => {
295
344
  if (msg.role === "think" && !showFullThinking) {
296
345
  const lines = content.split("\n").filter((line) => {
@@ -317,7 +366,24 @@ var init_ChatLayout = __esm({
317
366
  finalContent.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\\\n/g, "\n").replace(/\\$/, ""),
318
367
  columns - 6
319
368
  ).split("\n").map((line, lineIdx) => /* @__PURE__ */ React2.createElement(Box2, { key: lineIdx, flexDirection: "row", width: "100%" }, /* @__PURE__ */ React2.createElement(Box2, { flexShrink: 0, width: 2 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "white" }, lineIdx === 0 ? "\u276F" : " ")), /* @__PURE__ */ React2.createElement(Box2, { flexGrow: 1, marginLeft: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: msg.color || "white", wrap: "anywhere" }, line))))
320
- ) : msg.role === "think" ? /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1, paddingX: 1, width: "100%" }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "white" }, "Thinking..."), /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "single", borderLeft: true, borderRight: false, borderTop: false, borderBottom: false, paddingLeft: 2, flexDirection: "column", width: "100%" }, formatThinkText(finalContent, columns))) : /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", paddingX: 1, marginTop: 1, width: "100%" }, /* @__PURE__ */ React2.createElement(CodeRenderer, { text: finalContent, columns }), msg.memoryUpdated && /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1, width: "100%" }, /* @__PURE__ */ React2.createElement(Text2, { color: "yellow", italic: true }, "\u2728 [Memory Updated]"))));
369
+ ) : msg.role === "think" ? /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1, paddingX: 1, width: "100%" }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "white" }, "Thinking..."), /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "single", borderLeft: true, borderRight: false, borderTop: false, borderBottom: false, paddingLeft: 2, flexDirection: "column", width: "100%" }, !animationDone ? /* @__PURE__ */ React2.createElement(
370
+ TypewriterText,
371
+ {
372
+ text: finalContent,
373
+ isStreaming: msg.isStreaming,
374
+ onComplete: () => setAnimationDone(true),
375
+ speed: 25,
376
+ render: (t) => formatThinkText(t, columns)
377
+ }
378
+ ) : formatThinkText(finalContent, columns))) : /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", paddingX: 1, marginTop: 1, width: "100%" }, !animationDone ? /* @__PURE__ */ React2.createElement(
379
+ TypewriterText,
380
+ {
381
+ text: finalContent,
382
+ isStreaming: msg.isStreaming,
383
+ onComplete: () => setAnimationDone(true),
384
+ columns
385
+ }
386
+ ) : /* @__PURE__ */ React2.createElement(CodeRenderer, { text: finalContent, columns }), msg.memoryUpdated && /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1, width: "100%" }, /* @__PURE__ */ React2.createElement(Text2, { color: "yellow", italic: true }, "\u2728 [Memory Updated]"))));
321
387
  });
322
388
  ChatLayout = React2.memo(({ messages, showFullThinking, columns = 80 }) => {
323
389
  return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", width: "100%" }, messages.map((msg, idx) => /* @__PURE__ */ React2.createElement(
@@ -389,13 +455,13 @@ var init_CommandMenu = __esm({
389
455
  });
390
456
 
391
457
  // src/components/ProfileForm.jsx
392
- import React5, { useState } from "react";
458
+ import React5, { useState as useState2 } from "react";
393
459
  import { Box as Box5, Text as Text5 } from "ink";
394
460
  import TextInput from "ink-text-input";
395
461
  function ProfileForm({ onSave, onCancel }) {
396
- const [step, setStep] = useState(0);
397
- const [currentInput, setCurrentInput] = useState("");
398
- const [profile, setProfile] = useState({ name: "", nickname: "", instructions: "" });
462
+ const [step, setStep] = useState2(0);
463
+ const [currentInput, setCurrentInput] = useState2("");
464
+ const [profile, setProfile] = useState2({ name: "", nickname: "", instructions: "" });
399
465
  const steps = [
400
466
  { key: "name", label: "Enter your Name: " },
401
467
  { key: "nickname", label: "Enter a Nickname (Agent will use this): " },
@@ -427,16 +493,16 @@ var init_ProfileForm = __esm({
427
493
  });
428
494
 
429
495
  // src/components/AskUserModal.jsx
430
- import React6, { useState as useState2 } from "react";
496
+ import React6, { useState as useState3 } from "react";
431
497
  import { Box as Box6, Text as Text6, useInput } from "ink";
432
498
  import TextInput2 from "ink-text-input";
433
499
  var AskUserModal, AskUserModal_default;
434
500
  var init_AskUserModal = __esm({
435
501
  "src/components/AskUserModal.jsx"() {
436
502
  AskUserModal = ({ question, options, onResolve }) => {
437
- const [isSuggestingElse, setIsSuggestingElse] = useState2(false);
438
- const [customInput, setCustomInput] = useState2("");
439
- const [selectedIndex, setSelectedIndex] = useState2(0);
503
+ const [isSuggestingElse, setIsSuggestingElse] = useState3(false);
504
+ const [customInput, setCustomInput] = useState3("");
505
+ const [selectedIndex, setSelectedIndex] = useState3(0);
440
506
  const allOptions = [...options, { id: "CUSTOM", label: "Suggest something else...", description: "Provide a custom response" }];
441
507
  useInput((input, key) => {
442
508
  if (isSuggestingElse) return;
@@ -612,10 +678,10 @@ ${mode === "Flux" ? `
612
678
  3. Read Folder: tool:functions.read_folder(path="relative/path"). Detailed stats of a directory.
613
679
  4. Write File: tool:functions.write_file(path="path", content="content"). Creates/Overwrites. NO CODE BLOCKS. RETURNS: Disk verification + original content (if overwritten) for 100% reversibility.
614
680
  5. Update File: tool:functions.update_file(path="relative/path", content_to_replace="old", content_to_add="new"). Surgical patching. RETURNS: High-fidelity visual diff and old code block. You MUST verify that the change specifically matches your intent using the returned diff. PREFFER UPDATE FILE OVER WRITE FILE if file already exists for better reversal tracking (if a file has 500+ lines, try to stick with update_file over full-rewrite). DONT WRAP UPDATE FILE CALL CONTENT IN MARKDOWN CODE BLOCKS.
615
- 6. Write PDF: tool:functions.write_pdf(path="path", content="<html/css content>", orientation="portrait/landscape"). Generates a professional PDF document. Orientation are optional. A4 size page will be used. DO NOT ADD FOOTER MANUALLY, the system will handle it automatically. USE CSS TO VISUALLY BEAUTIFY THE DOCUMENT.
681
+ 6. Write PDF: tool:functions.write_pdf(path="path", content="<html/css content>", orientation="portrait/landscape"). Generates a professional PDF document. Orientation are optional. A4 size page will be used, so any multi-page PDFs calculate your alightment and page breaks to not mess up A4 page layout . DO NOT ADD FOOTER MANUALLY, the system will handle it automatically. USE CSS TO VISUALLY BEAUTIFY THE DOCUMENT.
616
682
  7. Execution: tool:functions.exec_command(command="terminal command"). Runs a shell command.
617
683
 
618
- **NOTE:** WHEN WRITING/UPDATING FILES, USE ACTUAL NEW LINE CHARACTER FOR LINE BREAKS RATHER THAN STRING '\\n'`.trim() : `
684
+ **NOTE:** WHEN WRITING/UPDATING FILES, USE ACTUAL NEW LINE CONTROL CHARACTER (LF) FOR LINE BREAKS RATHER THAN STRING '\\n'`.trim() : `
619
685
  - DEV & FILE TOOLS ARE NOT AVAILABLE IN FLOW MODE. If you need to access files, tell the user to switch to FLUX MODE (manually by user).`.trim()}
620
686
  -----------------
621
687
  Results will be provided in the next loop as: [TOOL_RESULT]: [content]
@@ -957,21 +1023,24 @@ var init_arg_parser = __esm({
957
1023
  "src/utils/arg_parser.js"() {
958
1024
  parseArgs = (argsString) => {
959
1025
  const args = {};
960
- const regex = /(\w+)\s*=\s*(?:(["'])((?:\\.|(?!\2)[\s\S])*)\2|([^,\s\)]+))/g;
1026
+ const regex = /(\w+)\s*=\s*(?:(["'`])((?:\\.|(?!\2)[\s\S])*)\2|([^,\s\)]+))/g;
961
1027
  let match;
962
1028
  while ((match = regex.exec(argsString)) !== null) {
963
1029
  const key = match[1];
964
1030
  let value = match[3] !== void 0 ? match[3] : match[4];
965
1031
  if (match[3] !== void 0) {
966
1032
  try {
967
- value = JSON.parse(`"${value.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`);
1033
+ value = JSON.parse(`"${value.replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`);
968
1034
  } catch (e) {
969
- value = value.replace(/\\"/g, '"').replace(/\\'/g, "'").replace(/\\\\/g, "\\");
1035
+ value = value.replace(/\\"/g, '"').replace(/\\'/g, "'").replace(/\\`/g, "`").replace(/\\\\/g, "\\");
970
1036
  }
971
1037
  }
972
1038
  if (value === "true") value = true;
973
1039
  else if (value === "false") value = false;
974
1040
  else if (typeof value === "string" && !isNaN(value) && value.trim() !== "") value = Number(value);
1041
+ if (typeof value === "string" && (key.toLowerCase().includes("path") || ["dest", "source", "to", "from"].includes(key.toLowerCase()))) {
1042
+ value = value.replace(/\x0C/g, "\\f").replace(/\x0D/g, "\\r").replace(/\x0B/g, "\\v").replace(/\x08/g, "\\b");
1043
+ }
975
1044
  args[key] = value;
976
1045
  }
977
1046
  return args;
@@ -1360,7 +1429,11 @@ var init_view_file = __esm({
1360
1429
  }
1361
1430
  };
1362
1431
  }
1363
- const content = fs9.readFileSync(absolutePath, "utf8");
1432
+ let content = fs9.readFileSync(absolutePath, "utf8");
1433
+ if (content.startsWith("\uFEFF")) {
1434
+ content = content.slice(1);
1435
+ }
1436
+ content = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1364
1437
  const lines = content.split("\n");
1365
1438
  const totalLines = lines.length;
1366
1439
  const start = Math.max(0, start_line - 1);
@@ -1389,7 +1462,7 @@ var init_write_file = __esm({
1389
1462
  let { path: targetPath, content } = parseArgs(args);
1390
1463
  if (!targetPath) return 'ERROR: Missing "path" argument for write_file.';
1391
1464
  if (content === void 0) return 'ERROR: Missing "content" argument for write_file.';
1392
- content = content.replace(/^```[\w]*\n?/, "").replace(/```\s*$/, "").trim();
1465
+ content = content.replace(/^```[\w]*\n?/, "").replace(/```\s*$/, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1393
1466
  const absolutePath = path10.resolve(process.cwd(), targetPath);
1394
1467
  const parentDir = path10.dirname(absolutePath);
1395
1468
  try {
@@ -1459,7 +1532,7 @@ var init_update_file = __esm({
1459
1532
  if (!targetPath) return 'ERROR: Missing "path" argument for update_file.';
1460
1533
  if (content_to_replace === void 0) return 'ERROR: Missing "content_to_replace" argument.';
1461
1534
  if (content_to_add === void 0) return 'ERROR: Missing "content_to_add" argument.';
1462
- const strip = (t) => t.replace(/^```[\w]*\n?/, "").replace(/```\s*$/, "").trim();
1535
+ const strip = (t) => t.replace(/^```[\w]*\n?/, "").replace(/```\s*$/, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1463
1536
  content_to_replace = strip(content_to_replace);
1464
1537
  content_to_add = strip(content_to_add);
1465
1538
  const absolutePath = path11.resolve(process.cwd(), targetPath);
@@ -1467,9 +1540,23 @@ var init_update_file = __esm({
1467
1540
  if (!fs11.existsSync(absolutePath)) {
1468
1541
  return `ERROR: File [${targetPath}] does not exist. Use write_file instead.`;
1469
1542
  }
1470
- const currentContent = fs11.readFileSync(absolutePath, "utf8");
1543
+ let diskContent = fs11.readFileSync(absolutePath, "utf8");
1544
+ if (diskContent.startsWith("\uFEFF")) {
1545
+ diskContent = diskContent.slice(1);
1546
+ }
1547
+ const normalizedDisk = diskContent.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1548
+ if (diskContent !== normalizedDisk) {
1549
+ fs11.writeFileSync(absolutePath, normalizedDisk, "utf8");
1550
+ diskContent = normalizedDisk;
1551
+ }
1552
+ const currentContent = diskContent;
1471
1553
  if (!currentContent.includes(content_to_replace)) {
1472
- return `ERROR: Could not find exact match for the specified "content_to_replace" in [${targetPath}]. Check indentation/whitespace/line breaks(LF or CRLF)/string. Try re-reading the file for latest changes.`;
1554
+ const diskLen = currentContent.length;
1555
+ const matchLen = content_to_replace.length;
1556
+ return `ERROR: Could not find exact match for the specified "content_to_replace" in [${targetPath}].
1557
+ - Disk Content Length (Normalized): ${diskLen}
1558
+ - Match String Length (Normalized): ${matchLen}
1559
+ - Check indentation/whitespace/line breaks. Try re-reading the file for latest changes.`;
1473
1560
  }
1474
1561
  const startPos = currentContent.indexOf(content_to_replace);
1475
1562
  const startLine = currentContent.substring(0, startPos).split(/\r?\n/).length;
@@ -1709,12 +1796,18 @@ var init_ask_user = __esm({
1709
1796
  import puppeteer3 from "puppeteer";
1710
1797
  import path13 from "path";
1711
1798
  import fs13 from "fs-extra";
1799
+ import { PDFDocument } from "pdf-lib";
1712
1800
  var write_pdf;
1713
1801
  var init_write_pdf = __esm({
1714
1802
  "src/tools/write_pdf.js"() {
1715
1803
  init_arg_parser();
1716
1804
  write_pdf = async (args) => {
1717
- const { path: targetPath, content, orientation = "portrait", margin = "10px" } = parseArgs(args);
1805
+ const {
1806
+ path: targetPath,
1807
+ content,
1808
+ orientation = "portrait",
1809
+ margin = "10px"
1810
+ } = parseArgs(args);
1718
1811
  if (!targetPath) return 'ERROR: Missing "path" argument for write_pdf.';
1719
1812
  if (!content) return 'ERROR: Missing "content" (HTML/CSS) for write_pdf.';
1720
1813
  const absolutePath = path13.resolve(process.cwd(), targetPath);
@@ -1733,39 +1826,57 @@ var init_write_pdf = __esm({
1733
1826
  const page = await browser.newPage();
1734
1827
  const styledContent = `
1735
1828
  <style>
1736
- @page {
1737
- margin: ${margin};
1829
+ @page {
1830
+ margin: ${margin};
1738
1831
  }
1739
- body {
1740
- margin: 0;
1832
+ body {
1833
+ margin: 0;
1741
1834
  padding: 0;
1742
1835
  font-family: system-ui, -apple-system, sans-serif;
1743
1836
  }
1744
1837
  * { box-sizing: border-box; }
1838
+ .watermark {
1839
+ position: fixed;
1840
+ top: 50%;
1841
+ left: 50%;
1842
+ transform: translate(-50%, -50%) rotate(-50deg);
1843
+ font-size: 52px;
1844
+ font-weight: bold;
1845
+ color: rgba(0, 0, 0, 0.005);
1846
+ pointer-events: none;
1847
+ z-index: -1000;
1848
+ text-align: center;
1849
+ width: 150%;
1850
+ white-space: nowrap;
1851
+ text-transform: uppercase;
1852
+ letter-spacing: 5px;
1853
+ }
1745
1854
  </style>
1855
+ <div class="watermark">Generated by FluxFlow CLI (AI)</div>
1746
1856
  ${content}
1747
1857
  `;
1748
1858
  await page.setContent(styledContent, { waitUntil: "networkidle0" });
1749
- await page.pdf({
1750
- path: absolutePath,
1859
+ const pdfBytes = await page.pdf({
1751
1860
  format: "A4",
1752
1861
  landscape: orientation.toLowerCase() === "landscape",
1753
1862
  margin: {
1754
1863
  top: margin,
1755
1864
  right: margin,
1756
- bottom: "15mm",
1757
- // Space for watermark
1865
+ bottom: margin,
1758
1866
  left: margin
1759
1867
  },
1760
- displayHeaderFooter: true,
1761
- headerTemplate: "<span></span>",
1762
- footerTemplate: `
1763
- <div style="font-size: 9px; color: rgba(0,0,0,0.2); width: 100%; text-align: right; padding-right: 15mm; font-family: system-ui, sans-serif; -webkit-print-color-adjust: exact;">
1764
- FluxFlow CLI
1765
- </div>
1766
- `,
1767
1868
  printBackground: true
1768
1869
  });
1870
+ const pdfDoc = await PDFDocument.load(pdfBytes);
1871
+ const fileName = path13.basename(targetPath);
1872
+ pdfDoc.setTitle(`FluxFlow ${fileName}`);
1873
+ pdfDoc.setAuthor("FluxFlow CLI");
1874
+ pdfDoc.setSubject("Generated with AI");
1875
+ pdfDoc.setKeywords(["FluxFlow", "AI", "Agentic", "Automated"]);
1876
+ pdfDoc.setCreator("FluxFlow PDF Engine");
1877
+ pdfDoc.setProducer("FluxFlow (Generative AI)");
1878
+ const finalPdfBytes = await pdfDoc.save();
1879
+ await fs13.writeFile(absolutePath, finalPdfBytes);
1769
1880
  const stats = await fs13.stat(absolutePath);
1770
1881
  return `SUCCESS: PDF generated successfully at [${targetPath}] (${(stats.size / 1024).toFixed(2)} KB).`;
1771
1882
  } catch (err) {
@@ -1865,39 +1976,47 @@ var init_ai = __esm({
1865
1976
  };
1866
1977
  detectToolCalls = (text) => {
1867
1978
  const results = [];
1868
- const toolRegex = /(?:\[?\s*tool:functions\.)([a-z0-9_]+)\s*\(([\s\S]*?)\)(?:\s*\]?)/gi;
1979
+ const toolRegex = /(?:\[?\s*tool:functions\.)([a-z0-9_]+)\s*\(/gi;
1869
1980
  let match;
1870
1981
  while ((match = toolRegex.exec(text)) !== null) {
1871
- const fullMatch = match[0];
1872
1982
  const toolName = match[1];
1873
- const args = match[2];
1874
- let openCount = (args.match(/\(/g) || []).length;
1875
- let closeCount = (args.match(/\)/g) || []).length;
1876
- let finalArgs = args;
1877
- let finalFullMatch = fullMatch;
1878
- if (openCount > closeCount) {
1879
- const startIdx = match.index + fullMatch.indexOf("(");
1880
- let balance = 0;
1881
- let endIdx = -1;
1882
- for (let i = startIdx; i < text.length; i++) {
1883
- if (text[i] === "(") balance++;
1884
- if (text[i] === ")") balance--;
1983
+ const startIdx = match.index + match[0].length - 1;
1984
+ let balance = 0;
1985
+ let inString = null;
1986
+ let isEscaped = false;
1987
+ let endIdx = -1;
1988
+ for (let i = startIdx; i < text.length; i++) {
1989
+ const char = text[i];
1990
+ if (!inString && (char === '"' || char === "'" || char === "`")) {
1991
+ inString = char;
1992
+ isEscaped = false;
1993
+ } else if (inString && char === inString && !isEscaped) {
1994
+ inString = null;
1995
+ }
1996
+ if (!inString) {
1997
+ if (char === "(") balance++;
1998
+ else if (char === ")") balance--;
1885
1999
  if (balance === 0) {
1886
2000
  endIdx = i;
1887
2001
  break;
1888
2002
  }
1889
2003
  }
1890
- if (endIdx !== -1) {
1891
- finalArgs = text.substring(startIdx + 1, endIdx);
1892
- finalFullMatch = text.substring(match.index, endIdx + 1);
1893
- toolRegex.lastIndex = endIdx + 1;
2004
+ if (char === "\\") {
2005
+ isEscaped = !isEscaped;
2006
+ } else {
2007
+ isEscaped = false;
1894
2008
  }
1895
2009
  }
1896
- results.push({
1897
- fullMatch: finalFullMatch,
1898
- toolName: toolName.trim(),
1899
- args: finalArgs.trim()
1900
- });
2010
+ if (endIdx !== -1) {
2011
+ const finalArgs = text.substring(startIdx + 1, endIdx);
2012
+ const finalFullMatch = text.substring(match.index, endIdx + 1);
2013
+ results.push({
2014
+ fullMatch: finalFullMatch,
2015
+ toolName: toolName.trim(),
2016
+ args: finalArgs.trim()
2017
+ });
2018
+ toolRegex.lastIndex = endIdx + 1;
2019
+ }
1901
2020
  }
1902
2021
  return results;
1903
2022
  };
@@ -1933,7 +2052,7 @@ USER_PROMPT: ${agentText}`.trim();
1933
2052
  let lastUsage = null;
1934
2053
  const MAX_LOOPS = mode === "Flux" ? 50 : 7;
1935
2054
  const MAX_RETRIES = 7;
1936
- yield { type: "status", content: "Working..." };
2055
+ yield { type: "status", content: "Connecting..." };
1937
2056
  TERMINATION_SIGNAL = false;
1938
2057
  let fullAgentResponseChunks = [];
1939
2058
  modifiedHistory.forEach((msg) => {
@@ -2036,6 +2155,7 @@ USER_PROMPT: ${agentText}`.trim();
2036
2155
  lastUsage = chunk.usageMetadata;
2037
2156
  if (lastUsage) {
2038
2157
  yield { type: "liveTokens", content: lastUsage.totalTokenCount };
2158
+ yield { type: "status", content: "Working..." };
2039
2159
  }
2040
2160
  }
2041
2161
  await incrementUsage("agent");
@@ -2043,7 +2163,11 @@ USER_PROMPT: ${agentText}`.trim();
2043
2163
  yield { type: "usage", content: lastUsage };
2044
2164
  }
2045
2165
  fullAgentResponseChunks.push(turnText);
2046
- const textToProcess = turnText.replace(/<think>[\s\S]*?<\/think>/g, "");
2166
+ let textToProcess = turnText;
2167
+ const thinkMatch = turnText.match(/<think>([\s\S]*?)<\/think>/i);
2168
+ if (thinkMatch) {
2169
+ textToProcess = turnText.replace(/<think>[\s\S]*?<\/think>/i, "");
2170
+ }
2047
2171
  const turnTextLower = textToProcess.toLowerCase();
2048
2172
  const hasFinish = /\[\s*(turn\s*:)?\s*finish\s*\]/i.test(turnTextLower);
2049
2173
  const toolCalls = detectToolCalls(textToProcess);
@@ -2102,7 +2226,7 @@ USER_PROMPT: ${agentText}`.trim();
2102
2226
  const boxTop = `\u256D${"\u2500".repeat(boxWidth)}\u256E`;
2103
2227
  const boxMid = `\u2502 ${label.padEnd(boxWidth - 2).substring(0, boxWidth - 2)} \u2502`;
2104
2228
  const boxBottom = `\u2570${"\u2500".repeat(boxWidth)}\u256F`;
2105
- yield { type: "text", content: `
2229
+ yield { type: "visual_feedback", content: `
2106
2230
 
2107
2231
  ${boxTop}
2108
2232
  ${boxMid}
@@ -2404,13 +2528,13 @@ var init_settings = __esm({
2404
2528
  });
2405
2529
 
2406
2530
  // src/components/ResumeModal.jsx
2407
- import React7, { useState as useState3, useEffect } from "react";
2531
+ import React7, { useState as useState4, useEffect as useEffect2 } from "react";
2408
2532
  import { Box as Box7, Text as Text7, useInput as useInput2 } from "ink";
2409
2533
  function ResumeModal({ onSelect, onDelete, onClose }) {
2410
- const [history, setHistory] = useState3({});
2411
- const [keys, setKeys] = useState3([]);
2412
- const [selectedIndex, setSelectedIndex] = useState3(0);
2413
- useEffect(() => {
2534
+ const [history, setHistory] = useState4({});
2535
+ const [keys, setKeys] = useState4([]);
2536
+ const [selectedIndex, setSelectedIndex] = useState4(0);
2537
+ useEffect2(() => {
2414
2538
  const fetchHistory = async () => {
2415
2539
  const h = await loadHistory();
2416
2540
  setHistory(h);
@@ -2447,16 +2571,16 @@ var init_ResumeModal = __esm({
2447
2571
  });
2448
2572
 
2449
2573
  // src/components/MemoryModal.jsx
2450
- import React8, { useState as useState4, useEffect as useEffect2 } from "react";
2574
+ import React8, { useState as useState5, useEffect as useEffect3 } from "react";
2451
2575
  import { Box as Box8, Text as Text8, useInput as useInput3 } from "ink";
2452
2576
  function MemoryModal({ onClose }) {
2453
- const [memories, setMemories] = useState4([]);
2454
- const [selectedIndex, setSelectedIndex] = useState4(0);
2577
+ const [memories, setMemories] = useState5([]);
2578
+ const [selectedIndex, setSelectedIndex] = useState5(0);
2455
2579
  const loadMemories = () => {
2456
2580
  const data = readEncryptedJson(MEMORIES_FILE, []);
2457
2581
  setMemories(data);
2458
2582
  };
2459
- useEffect2(() => {
2583
+ useEffect3(() => {
2460
2584
  loadMemories();
2461
2585
  }, []);
2462
2586
  useInput3((input, key) => {
@@ -2486,7 +2610,7 @@ var init_MemoryModal = __esm({
2486
2610
  });
2487
2611
 
2488
2612
  // src/components/UpdateProcessor.jsx
2489
- import React9, { useState as useState5, useEffect as useEffect3 } from "react";
2613
+ import React9, { useState as useState6, useEffect as useEffect4 } from "react";
2490
2614
  import { Box as Box9, Text as Text9 } from "ink";
2491
2615
  import Spinner from "ink-spinner";
2492
2616
  import { exec } from "child_process";
@@ -2494,10 +2618,10 @@ var UpdateProcessor, UpdateProcessor_default;
2494
2618
  var init_UpdateProcessor = __esm({
2495
2619
  "src/components/UpdateProcessor.jsx"() {
2496
2620
  UpdateProcessor = ({ latest, current, settings, onClose, onUpdateSettings, onSuccess }) => {
2497
- const [status, setStatus] = useState5("initializing");
2498
- const [log, setLog] = useState5("");
2499
- const [error, setError] = useState5(null);
2500
- useEffect3(() => {
2621
+ const [status, setStatus] = useState6("initializing");
2622
+ const [log, setLog] = useState6("");
2623
+ const [error, setError] = useState6(null);
2624
+ useEffect4(() => {
2501
2625
  const runUpdate = async () => {
2502
2626
  const manager = settings.updateManager || "npm";
2503
2627
  if (!settings.updateManager) {
@@ -2584,7 +2708,7 @@ var app_exports = {};
2584
2708
  __export(app_exports, {
2585
2709
  default: () => App
2586
2710
  });
2587
- import React10, { useState as useState6, useEffect as useEffect4, useRef, useMemo } from "react";
2711
+ import React10, { useState as useState7, useEffect as useEffect5, useRef as useRef2, useMemo } from "react";
2588
2712
  import { Box as Box10, Text as Text10, useInput as useInput4, useStdout } from "ink";
2589
2713
  import Spinner2 from "ink-spinner";
2590
2714
  import fs17 from "fs-extra";
@@ -2594,14 +2718,14 @@ import TextInput3 from "ink-text-input";
2594
2718
  import gradient from "gradient-string";
2595
2719
  function App() {
2596
2720
  const { stdout } = useStdout();
2597
- const [input, setInput] = useState6("");
2598
- const [isExpanded, setIsExpanded] = useState6(false);
2599
- const [mode, setMode] = useState6("Flux");
2600
- const [terminalSize, setTerminalSize] = useState6({
2721
+ const [input, setInput] = useState7("");
2722
+ const [isExpanded, setIsExpanded] = useState7(false);
2723
+ const [mode, setMode] = useState7("Flux");
2724
+ const [terminalSize, setTerminalSize] = useState7({
2601
2725
  columns: stdout?.columns || 80,
2602
2726
  rows: stdout?.rows || 24
2603
2727
  });
2604
- const [selectedIndex, setSelectedIndex] = useState6(0);
2728
+ const [selectedIndex, setSelectedIndex] = useState7(0);
2605
2729
  const performVersionCheck = async (manual = false, settingsOverride = null) => {
2606
2730
  const settingsToUse = settingsOverride || systemSettings;
2607
2731
  if (manual) {
@@ -2648,7 +2772,7 @@ Check what's new using \`/changelog\` command.`,
2648
2772
  }
2649
2773
  }
2650
2774
  };
2651
- useEffect4(() => {
2775
+ useEffect5(() => {
2652
2776
  const handleResize = () => {
2653
2777
  stdout.write("\x1Bc");
2654
2778
  setTerminalSize({
@@ -2661,29 +2785,29 @@ Check what's new using \`/changelog\` command.`,
2661
2785
  stdout.off("resize", handleResize);
2662
2786
  };
2663
2787
  }, [stdout]);
2664
- const [thinkingLevel, setThinkingLevel] = useState6("Medium");
2665
- const [latestVer, setLatestVer] = useState6(null);
2666
- const [showFullThinking, setShowFullThinking] = useState6(false);
2667
- const [activeModel, setActiveModel] = useState6("gemma-4-31b-it");
2668
- const [janitorModel, setJanitorModel] = useState6("gemma-4-26b-a4b-it");
2669
- const [isInitializing, setIsInitializing] = useState6(true);
2670
- const [apiKey, setApiKey] = useState6(null);
2671
- const [tempKey, setTempKey] = useState6("");
2672
- const [activeView, setActiveView] = useState6("chat");
2673
- const [apiTier, setApiTier] = useState6("Free");
2674
- const [quotas, setQuotas] = useState6({ agentLimit: 1500, backgroundLimit: 1500, searchLimit: 100, customModelId: "", customLimit: 0 });
2675
- const [inputConfig, setInputConfig] = useState6(null);
2676
- const [systemSettings, setSystemSettings] = useState6({ memory: true, compression: 0, autoExec: false, autoDeleteHistory: "7d", autoUpdate: false, updateManager: "npm", customUpdateCommand: "" });
2677
- const [profileData, setProfileData] = useState6({ name: null, nickname: null, instructions: null });
2678
- const [sessionStats, setSessionStats] = useState6({ tokens: 0 });
2679
- const [sessionAgentCalls, setSessionAgentCalls] = useState6(0);
2680
- const [sessionBackgroundCalls, setSessionBackgroundCalls] = useState6(0);
2681
- const [sessionTotalTokens, setSessionTotalTokens] = useState6(0);
2682
- const [dailyUsage, setDailyUsage] = useState6(null);
2683
- const [chatId, setChatId] = useState6(generateChatId());
2684
- const [activeCommand, setActiveCommand] = useState6(null);
2685
- const [execOutput, setExecOutput] = useState6("");
2686
- const [isTerminalFocused, setIsTerminalFocused] = useState6(false);
2788
+ const [thinkingLevel, setThinkingLevel] = useState7("Medium");
2789
+ const [latestVer, setLatestVer] = useState7(null);
2790
+ const [showFullThinking, setShowFullThinking] = useState7(false);
2791
+ const [activeModel, setActiveModel] = useState7("gemma-4-31b-it");
2792
+ const [janitorModel, setJanitorModel] = useState7("gemma-4-26b-a4b-it");
2793
+ const [isInitializing, setIsInitializing] = useState7(true);
2794
+ const [apiKey, setApiKey] = useState7(null);
2795
+ const [tempKey, setTempKey] = useState7("");
2796
+ const [activeView, setActiveView] = useState7("chat");
2797
+ const [apiTier, setApiTier] = useState7("Free");
2798
+ const [quotas, setQuotas] = useState7({ agentLimit: 1500, backgroundLimit: 1500, searchLimit: 100, customModelId: "", customLimit: 0 });
2799
+ const [inputConfig, setInputConfig] = useState7(null);
2800
+ const [systemSettings, setSystemSettings] = useState7({ memory: true, compression: 0, autoExec: false, autoDeleteHistory: "7d", autoUpdate: false, updateManager: "npm", customUpdateCommand: "" });
2801
+ const [profileData, setProfileData] = useState7({ name: null, nickname: null, instructions: null });
2802
+ const [sessionStats, setSessionStats] = useState7({ tokens: 0 });
2803
+ const [sessionAgentCalls, setSessionAgentCalls] = useState7(0);
2804
+ const [sessionBackgroundCalls, setSessionBackgroundCalls] = useState7(0);
2805
+ const [sessionTotalTokens, setSessionTotalTokens] = useState7(0);
2806
+ const [dailyUsage, setDailyUsage] = useState7(null);
2807
+ const [chatId, setChatId] = useState7(generateChatId());
2808
+ const [activeCommand, setActiveCommand] = useState7(null);
2809
+ const [execOutput, setExecOutput] = useState7("");
2810
+ const [isTerminalFocused, setIsTerminalFocused] = useState7(false);
2687
2811
  const terminalEnv = useMemo(() => {
2688
2812
  const isIDE = process.env.TERM_PROGRAM === "vscode" || !!process.env.VSC_TERMINAL_URL || !!process.env.INTELLIJ_TERMINAL_COMMAND_BLOCKS;
2689
2813
  return {
@@ -2691,17 +2815,17 @@ Check what's new using \`/changelog\` command.`,
2691
2815
  shortcut: isIDE ? "Shift + Enter" : "Ctrl + Enter"
2692
2816
  };
2693
2817
  }, []);
2694
- const activeCommandRef = useRef(null);
2695
- const execOutputRef = useRef("");
2696
- useEffect4(() => {
2818
+ const activeCommandRef = useRef2(null);
2819
+ const execOutputRef = useRef2("");
2820
+ useEffect5(() => {
2697
2821
  activeCommandRef.current = activeCommand;
2698
2822
  }, [activeCommand]);
2699
- useEffect4(() => {
2823
+ useEffect5(() => {
2700
2824
  execOutputRef.current = execOutput;
2701
2825
  }, [execOutput]);
2702
- const [autoAcceptWrites, setAutoAcceptWrites] = useState6(false);
2703
- const [pendingApproval, setPendingApproval] = useState6(null);
2704
- const [pendingAsk, setPendingAsk] = useState6(null);
2826
+ const [autoAcceptWrites, setAutoAcceptWrites] = useState7(false);
2827
+ const [pendingApproval, setPendingApproval] = useState7(null);
2828
+ const [pendingAsk, setPendingAsk] = useState7(null);
2705
2829
  const formatDuration = (totalSecs) => {
2706
2830
  const h = Math.floor(totalSecs / 3600);
2707
2831
  const m = Math.floor(totalSecs % 3600 / 60);
@@ -2712,18 +2836,18 @@ Check what's new using \`/changelog\` command.`,
2712
2836
  parts.push(`${s}s`);
2713
2837
  return parts.join(" ");
2714
2838
  };
2715
- const [statusText, setStatusText] = useState6(null);
2716
- const [isProcessing, setIsProcessing] = useState6(false);
2717
- const [escPressed, setEscPressed] = useState6(false);
2718
- const [escTimer, setEscTimer] = useState6(null);
2719
- const [queuedPrompt, setQueuedPrompt] = useState6(null);
2720
- const [resolutionData, setResolutionData] = useState6(null);
2721
- const [tempModelOverride, setTempModelOverride] = useState6(null);
2722
- const [messages, setMessages] = useState6([
2839
+ const [statusText, setStatusText] = useState7(null);
2840
+ const [isProcessing, setIsProcessing] = useState7(false);
2841
+ const [escPressed, setEscPressed] = useState7(false);
2842
+ const [escTimer, setEscTimer] = useState7(null);
2843
+ const [queuedPrompt, setQueuedPrompt] = useState7(null);
2844
+ const [resolutionData, setResolutionData] = useState7(null);
2845
+ const [tempModelOverride, setTempModelOverride] = useState7(null);
2846
+ const [messages, setMessages] = useState7([
2723
2847
  { id: "welcome", role: "system", text: FLUX_LOGO + "\n\n\u{1F30A}\u26A1 Welcome to Flux Flow! Type /help for commands.\n", isMeta: true }
2724
2848
  ]);
2725
- const queuedPromptRef = useRef(null);
2726
- const [completedIndex, setCompletedIndex] = useState6(1);
2849
+ const queuedPromptRef = useRef2(null);
2850
+ const [completedIndex, setCompletedIndex] = useState7(1);
2727
2851
  const windowedHistory = useMemo(() => {
2728
2852
  const MAX_LINES = 1e3;
2729
2853
  const width = stdout?.columns || 80;
@@ -2825,7 +2949,7 @@ Check what's new using \`/changelog\` command.`,
2825
2949
  setInput((prev) => prev.replace(/\\\r?$/, "").replace(/\r?$/, "") + "\n");
2826
2950
  }
2827
2951
  });
2828
- useEffect4(() => {
2952
+ useEffect5(() => {
2829
2953
  async function init() {
2830
2954
  if (!checkPuppeteerReady()) {
2831
2955
  setMessages((prev) => {
@@ -2870,7 +2994,7 @@ Check what's new using \`/changelog\` command.`,
2870
2994
  }
2871
2995
  init();
2872
2996
  }, []);
2873
- useEffect4(() => {
2997
+ useEffect5(() => {
2874
2998
  if (!isInitializing) {
2875
2999
  saveSettings({
2876
3000
  mode,
@@ -3214,7 +3338,7 @@ ${timestamp}` };
3214
3338
  setIsExpanded(false);
3215
3339
  try {
3216
3340
  const cleanHistoryForAI = [...messages, userMessage].filter(
3217
- (m) => m.role !== "think" && !String(m.id).startsWith("welcome")
3341
+ (m) => m.role !== "think" && !m.isVisualFeedback && !String(m.id).startsWith("welcome")
3218
3342
  ).map((m) => ({
3219
3343
  ...m,
3220
3344
  text: m.fullText || m.text
@@ -3315,6 +3439,12 @@ Selection: ${val}`,
3315
3439
  let inThinkMode = false;
3316
3440
  let currentThinkId = null;
3317
3441
  let currentAgentId = null;
3442
+ let inCodeBlock = false;
3443
+ let inToolCall = false;
3444
+ let thinkConsumedInTurn = false;
3445
+ let toolCallEncounteredInTurn = false;
3446
+ let toolCallBalance = 0;
3447
+ let inToolCallString = null;
3318
3448
  const signalRegex = /\[?_DISABLED_SIGNAL_REGEX_\]?/gi;
3319
3449
  for await (const packet of stream) {
3320
3450
  if (packet.type === "status") {
@@ -3329,6 +3459,9 @@ Selection: ${val}`,
3329
3459
  currentThinkId = null;
3330
3460
  currentAgentId = null;
3331
3461
  inThinkMode = false;
3462
+ inCodeBlock = false;
3463
+ inToolCall = false;
3464
+ toolCallEncounteredInTurn = false;
3332
3465
  continue;
3333
3466
  }
3334
3467
  if (packet.type === "memory_updated") {
@@ -3341,6 +3474,15 @@ Selection: ${val}`,
3341
3474
  });
3342
3475
  continue;
3343
3476
  }
3477
+ if (packet.type === "visual_feedback") {
3478
+ setMessages((prev) => [...prev, {
3479
+ id: "feedback-" + Date.now(),
3480
+ role: "system",
3481
+ text: packet.content,
3482
+ isVisualFeedback: true
3483
+ }]);
3484
+ continue;
3485
+ }
3344
3486
  if (packet.type === "exec_start") {
3345
3487
  continue;
3346
3488
  }
@@ -3372,23 +3514,50 @@ Selection: ${val}`,
3372
3514
  continue;
3373
3515
  }
3374
3516
  let chunkText = packet.content;
3375
- if ((chunkText.toLowerCase().includes("<think") || chunkText.toLowerCase().includes("<thought")) && !inThinkMode) {
3517
+ const chunkLower = chunkText.toLowerCase();
3518
+ if (chunkText.includes("```")) inCodeBlock = !inCodeBlock;
3519
+ if (chunkLower.includes("tool:functions.")) {
3520
+ inToolCall = true;
3521
+ toolCallBalance = 0;
3522
+ inToolCallString = null;
3523
+ }
3524
+ if (inToolCall) {
3525
+ for (let j = 0; j < chunkText.length; j++) {
3526
+ const char = chunkText[j];
3527
+ if (!inToolCallString && (char === "'" || char === '"' || char === "`")) {
3528
+ inToolCallString = char;
3529
+ } else if (inToolCallString && char === inToolCallString && chunkText[j - 1] !== "\\") {
3530
+ inToolCallString = null;
3531
+ }
3532
+ if (!inToolCallString) {
3533
+ if (char === "(") toolCallBalance++;
3534
+ else if (char === ")") toolCallBalance--;
3535
+ }
3536
+ }
3537
+ if (toolCallBalance <= 0 && !inToolCallString) {
3538
+ inToolCall = false;
3539
+ }
3540
+ }
3541
+ const hasThinkTag = chunkLower.includes("<think") || chunkLower.includes("<thought");
3542
+ const canThink = !inThinkMode && !inCodeBlock && !inToolCall && !thinkConsumedInTurn;
3543
+ if (hasThinkTag && canThink) {
3376
3544
  inThinkMode = true;
3377
- chunkText = chunkText.replace(/<(think|thought)>/gi, "");
3545
+ thinkConsumedInTurn = true;
3546
+ chunkText = chunkText.replace(/<(think|thought)>[\s\S]*?<\/(think|thought)>/gi, "").replace(/<(think|thought)>/gi, "");
3378
3547
  currentThinkId = "think-" + Date.now();
3379
- setMessages((prev) => [...prev, { id: currentThinkId, role: "think", text: "" }]);
3548
+ setMessages((prev) => [...prev, { id: currentThinkId, role: "think", text: "", isStreaming: true }]);
3380
3549
  }
3381
- if (chunkText.toLowerCase().includes("</think>") || chunkText.toLowerCase().includes("</thought>")) {
3550
+ if (chunkLower.includes("</think>") || chunkLower.includes("</thought>")) {
3382
3551
  const parts = chunkText.split(/<\/(think|thought)>/gi);
3383
3552
  const thinkPart = parts[0] || "";
3384
- const agentPart = parts.slice(2).join("") || "";
3553
+ const agentPart = parts.slice(2).join("").replace(/<\/?(think|thought)>/gi, "");
3385
3554
  setMessages((prev) => {
3386
3555
  const newMsgs = prev.map(
3387
- (m) => m.id === currentThinkId ? { ...m, text: m.text + thinkPart } : m
3556
+ (m) => m.id === currentThinkId ? { ...m, text: m.text + thinkPart, isStreaming: true } : m
3388
3557
  );
3389
3558
  inThinkMode = false;
3390
3559
  currentAgentId = "agent-" + Date.now();
3391
- return [...newMsgs, { id: currentAgentId, role: "agent", text: agentPart.replace(/<\/?(think|thought)>/gi, "") }];
3560
+ return [...newMsgs, { id: currentAgentId, role: "agent", text: agentPart, isStreaming: true }];
3392
3561
  });
3393
3562
  continue;
3394
3563
  }
@@ -3405,26 +3574,33 @@ Selection: ${val}`,
3405
3574
  transitionContent = parts.slice(1).join("</think>") || "";
3406
3575
  return { ...m, text: parts[0] };
3407
3576
  }
3408
- return { ...m, text: newText };
3577
+ return { ...m, text: newText, isStreaming: true };
3409
3578
  }
3410
3579
  return m;
3411
3580
  });
3412
3581
  if (transitioning) {
3413
3582
  inThinkMode = false;
3414
3583
  currentAgentId = "agent-" + Date.now();
3415
- return [...newMsgs, { id: currentAgentId, role: "agent", text: transitionContent.replace(/<\/?think>/gi, "") }];
3584
+ return [...newMsgs, { id: currentAgentId, role: "agent", text: transitionContent.replace(/<\/?(think|thought)>/gi, ""), isStreaming: true }];
3416
3585
  }
3417
3586
  return newMsgs;
3418
3587
  });
3419
- } else if (!inThinkMode) {
3420
- const cleanedText = chunkText.replace(/<\/?(think|thought)>/gi, "").replace(signalRegex, "");
3421
- if (!currentAgentId) {
3422
- currentAgentId = "agent-" + Date.now();
3423
- setMessages((prev) => [...prev, { id: currentAgentId, role: "agent", text: cleanedText }]);
3424
- } else {
3425
- setMessages((prev) => prev.map(
3426
- (m) => m.id === currentAgentId ? { ...m, text: m.text + cleanedText } : m
3427
- ));
3588
+ } else if (!inThinkMode && !toolCallEncounteredInTurn) {
3589
+ let cleanedText = chunkText.replace(/<(think|thought)>[\s\S]*?<\/(think|thought)>/gi, "").replace(/<\/?(think|thought)>/gi, "").replace(signalRegex, "");
3590
+ const toolIdx = cleanedText.toLowerCase().indexOf("tool:functions.");
3591
+ if (toolIdx !== -1) {
3592
+ cleanedText = cleanedText.substring(0, toolIdx);
3593
+ toolCallEncounteredInTurn = true;
3594
+ }
3595
+ if (cleanedText) {
3596
+ if (!currentAgentId) {
3597
+ currentAgentId = "agent-" + Date.now();
3598
+ setMessages((prev) => [...prev, { id: currentAgentId, role: "agent", text: cleanedText, isStreaming: true }]);
3599
+ } else {
3600
+ setMessages((prev) => prev.map(
3601
+ (m) => m.id === currentAgentId ? { ...m, text: m.text + cleanedText, isStreaming: true } : m
3602
+ ));
3603
+ }
3428
3604
  }
3429
3605
  }
3430
3606
  }
@@ -3452,10 +3628,11 @@ Selection: ${val}`,
3452
3628
  setActiveView("resolution");
3453
3629
  }
3454
3630
  setMessages((prev) => {
3455
- const historyToSave = prev.filter((m) => !String(m.id).startsWith("welcome"));
3631
+ const newMsgs = prev.map((m) => m.isStreaming ? { ...m, isStreaming: false } : m);
3632
+ const historyToSave = newMsgs.filter((m) => !String(m.id).startsWith("welcome") && !m.isVisualFeedback);
3456
3633
  saveChat(chatId, null, historyToSave);
3457
- setCompletedIndex(prev.length);
3458
- return prev;
3634
+ setCompletedIndex(newMsgs.length);
3635
+ return newMsgs;
3459
3636
  });
3460
3637
  }
3461
3638
  };
@@ -3483,7 +3660,7 @@ Selection: ${val}`,
3483
3660
  }
3484
3661
  return [];
3485
3662
  }, [input]);
3486
- useEffect4(() => {
3663
+ useEffect5(() => {
3487
3664
  setSelectedIndex(0);
3488
3665
  }, [suggestions]);
3489
3666
  const renderActiveView = () => {
@@ -4121,7 +4298,7 @@ var init_app = __esm({
4121
4298
  init_setup();
4122
4299
  SESSION_START_TIME = Date.now();
4123
4300
  CHANGELOG_URL = "https://fluxflow-cli.onrender.com/changelog.html";
4124
- versionFluxflow = "1.5.3";
4301
+ versionFluxflow = "1.6.0";
4125
4302
  updatedOn = "2026-05-02";
4126
4303
  ResolutionModal = ({ data, onResolve, onEdit }) => /* @__PURE__ */ React10.createElement(Box10, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 2, paddingY: 1, width: "100%" }, /* @__PURE__ */ React10.createElement(Text10, { color: "magenta", bold: true, underline: true }, "\u{1F7E3} STEERING HINT RESOLUTION"), /* @__PURE__ */ React10.createElement(Text10, { marginTop: 1 }, "The agent already finished the task (turn: finish) before your hint was consumed."), /* @__PURE__ */ React10.createElement(Box10, { marginTop: 1, backgroundColor: "#222", paddingX: 1, width: "100%" }, /* @__PURE__ */ React10.createElement(Text10, { italic: true, color: "gray" }, '"', data, '"')), /* @__PURE__ */ React10.createElement(Box10, { marginTop: 1 }, /* @__PURE__ */ React10.createElement(Text10, { color: "cyan" }, "How would you like to proceed?")), /* @__PURE__ */ React10.createElement(Box10, { marginTop: 1 }, /* @__PURE__ */ React10.createElement(
4127
4304
  CommandMenu,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluxflow-cli",
3
- "version": "1.5.3",
3
+ "version": "1.6.0",
4
4
  "description": "A high-fidelity agentic terminal assistant for the Flux Era.",
5
5
  "keywords": [
6
6
  "ai",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "scripts": {
36
36
  "start": "tsx ./src/cli.jsx",
37
- "build": "esbuild ./src/cli.jsx --bundle --platform=node --format=esm --outfile=./dist/fluxflow.js --external:react --external:ink --external:chalk --external:fs-extra --external:gradient-string --external:ink-text-input --external:ink-select-input --external:ink-spinner --external:ink-multiline-input --external:@google/genai --external:zod --external:nanoid --external:puppeteer --external:typescript"
37
+ "build": "esbuild ./src/cli.jsx --bundle --platform=node --format=esm --outfile=./dist/fluxflow.js --external:react --external:ink --external:chalk --external:fs-extra --external:gradient-string --external:ink-text-input --external:ink-select-input --external:ink-spinner --external:ink-multiline-input --external:@google/genai --external:zod --external:nanoid --external:puppeteer --external:pdf-lib --external:typescript"
38
38
  },
39
39
  "dependencies": {
40
40
  "@google/genai": "^1.50.1",
@@ -47,6 +47,7 @@
47
47
  "ink-spinner": "^5.0.0",
48
48
  "ink-text-input": "^6.0.0",
49
49
  "nanoid": "^5.1.9",
50
+ "pdf-lib": "^1.17.1",
50
51
  "puppeteer": "^24.42.0",
51
52
  "react": "^19.2.5",
52
53
  "zod": "^4.3.6"