@nomad-e/bluma-cli 0.1.84 → 0.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.
@@ -0,0 +1,130 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Indicador de trabalho em curso — pulso minimalista + shimmer suave.
4
+ */
5
+ import { useState, useEffect, memo, useRef } from "react";
6
+ import { Box, Text } from "ink";
7
+ import { BLUMA_TERMINAL as T } from "./theme/blumaTerminal.js";
8
+ import { TOOL_DETAIL_PREFIX } from "./constants/toolUiSymbols.js";
9
+ import { formatTurnDurationMs } from "./utils/formatTurnDurationMs.js";
10
+ import { getToolActionLabel } from "./utils/toolActionLabels.js";
11
+ // ─── Pulse ────────────────────────────────────────────────────────────────────
12
+ // Frames com "peso visual" crescente/decrescente → sensação de respiração
13
+ const PULSE_FRAMES = ["·", "○", "◌", "◎", "●", "◎", "◌", "○"];
14
+ const PULSE_INTERVAL_MS = 200; // Increased from 110ms to reduce screen flicker
15
+ // ─── Shimmer ──────────────────────────────────────────────────────────────────
16
+ // Paleta de 5 níveis de brilho (sem mudar hue, apenas luminosidade percebida)
17
+ // Usamos gray scale para ficar neutro com qualquer tema
18
+ const SHIMMER_PALETTE = [
19
+ "#4a4a4a", // escuro
20
+ "#6e6e6e",
21
+ "#9a9a9a", // mid
22
+ "#c8c8c8",
23
+ "#f0f0f0", // quase branco
24
+ "#ffffff", // pico de brilho (branco puro)
25
+ "#f0f0f0",
26
+ "#c8c8c8",
27
+ "#9a9a9a",
28
+ "#6e6e6e",
29
+ ];
30
+ const SHIMMER_CYCLE_MS = 2000; // Increased from 1000ms to reduce flicker (slower shimmer)
31
+ /** Shimmer com uma única sombra que percorre o texto da esquerda para a direita.
32
+ * Cada passada (início → fim) demora SHIMMER_CYCLE_MS (1s).
33
+ * Quando chega ao fim, volta instantaneamente ao início e recomeça.
34
+ * Usa ref para animação fluída sem re-render quando o texto muda. */
35
+ const ShimmerText = memo(({ text, baseColor = "#9a9a9a" }) => {
36
+ const posRef = useRef(0);
37
+ const [, forceUpdate] = useState({});
38
+ const textRef = useRef(text);
39
+ const intervalMsRef = useRef(50); // valor padrão inicial
40
+ // Atualiza ref do texto sem afetar a animação
41
+ useEffect(() => {
42
+ textRef.current = text;
43
+ // Recalcula o intervalo para que a passada (início → fim) demore SHIMMER_CYCLE_MS
44
+ const cycleSteps = Math.max(1, text.length);
45
+ intervalMsRef.current = SHIMMER_CYCLE_MS / cycleSteps;
46
+ }, [text]);
47
+ useEffect(() => {
48
+ const rafId = setInterval(() => {
49
+ const len = textRef.current.length;
50
+ // Posição vai de 0 a len-1 (apenas ida, esquerda → direita)
51
+ // Quando chega ao fim, volta ao início (0)
52
+ posRef.current = (posRef.current + 1) % (len || 1);
53
+ forceUpdate({}); // Força re-render mínimo apenas para atualizar cores
54
+ }, intervalMsRef.current);
55
+ return () => clearInterval(rafId);
56
+ }, []);
57
+ const currentText = textRef.current;
58
+ const currentPos = posRef.current;
59
+ // Calcula a cor baseada na distância do ponto de brilho
60
+ // O caractere na currentPos tem o brilho máximo (índice 5 = branco)
61
+ // Os adjacentes têm brilho decrescente
62
+ const getColorForIndex = (i) => {
63
+ const dist = Math.abs(i - currentPos);
64
+ if (dist === 0)
65
+ return SHIMMER_PALETTE[5]; // branco puro no pico
66
+ if (dist === 1)
67
+ return SHIMMER_PALETTE[4]; // quase branco
68
+ if (dist === 2)
69
+ return SHIMMER_PALETTE[3]; // claro
70
+ if (dist === 3)
71
+ return SHIMMER_PALETTE[2]; // mid
72
+ return baseColor; // cor base para o resto
73
+ };
74
+ return (_jsx(_Fragment, { children: currentText.split("").map((char, i) => (_jsx(Text, { color: getColorForIndex(i), children: char }, i))) }));
75
+ });
76
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
77
+ function trimLine(s, max = 76) {
78
+ const t = String(s ?? "")
79
+ .replace(/\s+/g, " ")
80
+ .trim();
81
+ if (!t)
82
+ return "";
83
+ return t.length <= max ? t : `${t.slice(0, max - 1)}…`;
84
+ }
85
+ // ─── WorkingTimer ─────────────────────────────────────────────────────────────
86
+ const WorkingTimerComponent = ({ eventBus, taskName, taskStatus, liveToolName, liveToolArgs, isReasoning, startedAtMs, }) => {
87
+ const [currentAction, setCurrentAction] = useState("working");
88
+ const [pulseFrame, setPulseFrame] = useState(0);
89
+ const [nowTick, setNowTick] = useState(() => Date.now());
90
+ const dynamicActionLabel = liveToolName
91
+ ? getToolActionLabel(liveToolName, liveToolArgs)
92
+ : null;
93
+ useEffect(() => {
94
+ if (!eventBus)
95
+ return;
96
+ const handleActionStatus = (data) => {
97
+ if (data.action)
98
+ setCurrentAction(data.action);
99
+ };
100
+ eventBus.on("action_status", handleActionStatus);
101
+ return () => { eventBus.off("action_status", handleActionStatus); };
102
+ }, [eventBus]);
103
+ // Pulse — frame rate próprio, independente do shimmer
104
+ useEffect(() => {
105
+ const id = setInterval(() => {
106
+ setPulseFrame((prev) => (prev + 1) % PULSE_FRAMES.length);
107
+ }, PULSE_INTERVAL_MS);
108
+ return () => clearInterval(id);
109
+ }, []);
110
+ // Elapsed timer
111
+ useEffect(() => {
112
+ if (startedAtMs == null)
113
+ return;
114
+ setNowTick(Date.now());
115
+ const id = setInterval(() => setNowTick(Date.now()), 500);
116
+ return () => clearInterval(id);
117
+ }, [startedAtMs]);
118
+ const displayAction = dynamicActionLabel ||
119
+ (taskStatus || (isReasoning ? "thinking" : currentAction)).trim() ||
120
+ "working";
121
+ const actionLine = `${displayAction}…`;
122
+ const elapsedMs = startedAtMs != null ? Math.max(0, nowTick - startedAtMs) : 0;
123
+ const elapsedLabel = startedAtMs != null ? formatTurnDurationMs(elapsedMs) : null;
124
+ // Intensidade do pulse: frame central (●) = full, bordas = dim
125
+ const pulseSymbol = PULSE_FRAMES[pulseFrame];
126
+ const isPulsePeak = pulseFrame === 4; // "●"
127
+ const isPulseDim = pulseFrame === 0; // "·"
128
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 0.5, children: [_jsxs(Box, { flexDirection: "row", alignItems: "flex-start", children: [_jsx(Text, { color: T.onSurfaceVariant, dimColor: isPulseDim, bold: isPulsePeak, children: pulseSymbol }), _jsx(Text, { children: " " }), _jsx(ShimmerText, { text: actionLine, baseColor: T.onSurfaceVariant ?? "#6e6e6e" }), elapsedLabel ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: T.m3OnSurface, dimColor: true, children: " \u00B7 " }), _jsx(Text, { color: T.m3OnSurface, dimColor: true, children: elapsedLabel })] })) : null] }), taskName ? (_jsx(Box, { paddingLeft: TOOL_DETAIL_PREFIX.length, children: _jsx(Text, { color: T.m3OnSurface, dimColor: true, wrap: "truncate", children: trimLine(taskName) }) })) : null] }));
129
+ };
130
+ export const WorkingTimer = memo(WorkingTimerComponent);
@@ -0,0 +1,113 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState, useEffect, useRef, memo } from "react";
3
+ import { Box, Text } from "ink";
4
+ import { ChatBlock, MessageResponse } from "../theme/m3Layout.js";
5
+ import { BLUMA_TERMINAL as T } from "../theme/blumaTerminal.js";
6
+ import { applyStreamEndFlush } from "./streamingTextFlush.js";
7
+ import { collapseRepeatedReasoningLines } from "./reasoningText.js";
8
+ const THROTTLE_MS = 500; // Increased from 300ms to reduce Ink re-render flicker
9
+ const MAX_VISIBLE_LINES = 20;
10
+ const StreamingTextComponent = ({ eventBus, onReasoningComplete, onAssistantContentComplete, }) => {
11
+ const [reasoning, setReasoning] = useState("");
12
+ const [assistantContent, setAssistantContent] = useState("");
13
+ const [isStreaming, setIsStreaming] = useState(false);
14
+ const reasoningRef = useRef("");
15
+ const contentRef = useRef("");
16
+ const flushTimerRef = useRef(null);
17
+ const onReasoningCompleteRef = useRef(onReasoningComplete);
18
+ onReasoningCompleteRef.current = onReasoningComplete;
19
+ const onAssistantContentCompleteRef = useRef(onAssistantContentComplete);
20
+ onAssistantContentCompleteRef.current = onAssistantContentComplete;
21
+ const streamEndHandledRef = useRef(false);
22
+ useEffect(() => {
23
+ const syncVisible = () => {
24
+ setReasoning(reasoningRef.current);
25
+ setAssistantContent(contentRef.current);
26
+ };
27
+ const startFlushTimer = () => {
28
+ if (flushTimerRef.current)
29
+ return;
30
+ flushTimerRef.current = setInterval(() => {
31
+ setReasoning(reasoningRef.current);
32
+ setAssistantContent(contentRef.current);
33
+ }, THROTTLE_MS);
34
+ };
35
+ const handleStart = () => {
36
+ streamEndHandledRef.current = false;
37
+ reasoningRef.current = "";
38
+ contentRef.current = "";
39
+ setReasoning("");
40
+ setAssistantContent("");
41
+ setIsStreaming(true);
42
+ startFlushTimer();
43
+ };
44
+ const handleReasoningChunk = (data) => {
45
+ if (data.delta) {
46
+ reasoningRef.current += data.delta;
47
+ }
48
+ };
49
+ const handleContentChunk = (data) => {
50
+ if (data.delta) {
51
+ contentRef.current += data.delta;
52
+ }
53
+ };
54
+ const handleEnd = (payload) => {
55
+ if (streamEndHandledRef.current)
56
+ return;
57
+ streamEndHandledRef.current = true;
58
+ // Clear timers first
59
+ if (flushTimerRef.current) {
60
+ clearInterval(flushTimerRef.current);
61
+ flushTimerRef.current = null;
62
+ }
63
+ // IMMEDIATELY clear the streaming UI before adding to history
64
+ // This prevents the reasoning from appearing twice
65
+ const finalReasoning = reasoningRef.current;
66
+ const finalContent = contentRef.current;
67
+ setReasoning("");
68
+ setAssistantContent("");
69
+ reasoningRef.current = "";
70
+ contentRef.current = "";
71
+ setIsStreaming(false);
72
+ // NOW add to history (after UI is cleared)
73
+ applyStreamEndFlush({
74
+ finalReasoning,
75
+ finalContent,
76
+ payload,
77
+ onReasoningComplete: onReasoningCompleteRef.current,
78
+ onAssistantContentComplete: onAssistantContentCompleteRef.current,
79
+ });
80
+ };
81
+ eventBus.on("stream_start", handleStart);
82
+ eventBus.on("stream_reasoning_chunk", handleReasoningChunk);
83
+ eventBus.on("stream_chunk", handleContentChunk);
84
+ eventBus.on("stream_end", handleEnd);
85
+ return () => {
86
+ eventBus.off("stream_start", handleStart);
87
+ eventBus.off("stream_reasoning_chunk", handleReasoningChunk);
88
+ eventBus.off("stream_chunk", handleContentChunk);
89
+ eventBus.off("stream_end", handleEnd);
90
+ if (flushTimerRef.current) {
91
+ clearInterval(flushTimerRef.current);
92
+ flushTimerRef.current = null;
93
+ }
94
+ };
95
+ }, [eventBus]);
96
+ if (!isStreaming || (!reasoning && !assistantContent)) {
97
+ return null;
98
+ }
99
+ const renderLines = (text, dim) => {
100
+ const normalized = collapseRepeatedReasoningLines(text);
101
+ const lines = normalized.split("\n");
102
+ let displayLines = lines;
103
+ let truncatedCount = 0;
104
+ if (lines.length > MAX_VISIBLE_LINES) {
105
+ truncatedCount = lines.length - MAX_VISIBLE_LINES;
106
+ displayLines = lines.slice(-MAX_VISIBLE_LINES);
107
+ }
108
+ return (_jsxs(Box, { flexDirection: "column", children: [truncatedCount > 0 ? (_jsxs(Text, { dimColor: true, children: ["\u2026 ", truncatedCount, " lines above hidden"] })) : null, displayLines.map((line, i) => (_jsx(Text, { dimColor: dim, color: dim ? undefined : T.m3OnSurface, children: line }, i)))] }));
109
+ };
110
+ return (_jsxs(ChatBlock, { marginBottom: 0, children: [reasoning ? (_jsx(Box, { flexDirection: "column", paddingLeft: 2, children: renderLines(reasoning, true) })) : null, assistantContent ? (_jsx(MessageResponse, { children: renderLines(assistantContent, false) })) : null] }));
111
+ };
112
+ export const StreamingText = memo(StreamingTextComponent);
113
+ export default StreamingText;
@@ -0,0 +1,18 @@
1
+ export function collapseRepeatedReasoningLines(text) {
2
+ const raw = String(text ?? "").replace(/\r\n/g, "\n");
3
+ if (!raw.trim())
4
+ return "";
5
+ const lines = raw.split("\n");
6
+ const collapsed = [];
7
+ for (const line of lines) {
8
+ const current = line.trimEnd();
9
+ if (current === "" && collapsed[collapsed.length - 1] === "") {
10
+ continue;
11
+ }
12
+ if (collapsed.length > 0 && collapsed[collapsed.length - 1] === current) {
13
+ continue;
14
+ }
15
+ collapsed.push(current);
16
+ }
17
+ return collapsed.join("\n").trim();
18
+ }
@@ -0,0 +1,11 @@
1
+ export function applyStreamEndFlush(params) {
2
+ const { finalReasoning, finalContent, payload, onReasoningComplete, onAssistantContentComplete, } = params;
3
+ if (finalReasoning && onReasoningComplete) {
4
+ onReasoningComplete(finalReasoning);
5
+ }
6
+ const trimmed = finalContent.trim();
7
+ const skipAssistant = payload?.omitAssistantFlush === true;
8
+ if (trimmed && onAssistantContentComplete && !skipAssistant) {
9
+ onAssistantContentComplete(finalContent);
10
+ }
11
+ }
@@ -227,41 +227,7 @@
227
227
  }
228
228
  }
229
229
  },
230
- {
231
- "type": "function",
232
- "function": {
233
- "name": "message",
234
- "description": "Primary user-visible channel — use it generously. Call message_type \"info\" frequently during a turn (progress, findings, errors, next steps); multiple info calls per turn are required for good UX. The turn does not stop on \"info\". Use message_type \"result\" once when you finish this turn (final answer, deliverable, or question for the user); then the agent waits.",
235
- "parameters": {
236
- "type": "object",
237
- "properties": {
238
- "content": {
239
- "type": "string",
240
- "description": "Markdown body shown in the chat (user's language when appropriate)."
241
- },
242
- "message_type": {
243
- "type": "string",
244
- "enum": [
245
- "info",
246
- "result"
247
- ],
248
- "description": "Prefer \"info\" often while working (does not end turn). Use \"result\" only once to end the turn."
249
- },
250
- "attachments": {
251
- "type": "array",
252
- "items": {
253
- "type": "string"
254
- },
255
- "description": "Optional file paths (absolute) to deliver as attachments. Put generated files under ./.bluma/artifacts/ (or paths returned by task_boundary / create_artifact); never a top-level ./artifacts/ folder — tools remap that to .bluma/artifacts/."
256
- }
257
- },
258
- "required": [
259
- "content",
260
- "message_type"
261
- ]
262
- }
263
- }
264
- },
230
+
265
231
  {
266
232
  "type": "function",
267
233
  "function": {
@@ -720,30 +686,6 @@
720
686
  }
721
687
  }
722
688
  },
723
- {
724
- "type": "function",
725
- "function": {
726
- "name": "create_artifact",
727
- "description": "Create or update an artifact under the workspace .bluma/artifacts/<session>/ directory (see task_boundary artifacts_dir). Use for plans, walkthroughs, notes, or deliverables. Not the same as ~/.bluma.",
728
- "parameters": {
729
- "type": "object",
730
- "properties": {
731
- "filename": {
732
- "type": "string",
733
- "description": "Name of the artifact file to create. Examples: 'implementation_plan.md', 'walkthrough.md', 'notes.md'."
734
- },
735
- "content": {
736
- "type": "string",
737
- "description": "Content to write to the artifact file. Supports markdown formatting."
738
- }
739
- },
740
- "required": [
741
- "filename",
742
- "content"
743
- ]
744
- }
745
- }
746
- },
747
689
  {
748
690
  "type": "function",
749
691
  "function": {
@@ -0,0 +1,218 @@
1
+ # Plan Mode Instructions
2
+
3
+ ## Overview
4
+
5
+ Plan Mode is a structured approach to problem-solving that emphasizes **clarity, alignment, and decision-completeness** before implementation.
6
+
7
+ When in Plan Mode, you must follow the **3-phase workflow**:
8
+
9
+ ---
10
+
11
+ ## Phase 1: Grounding (Understanding)
12
+
13
+ **Goal:** Demonstrate deep understanding of the problem before proposing solutions.
14
+
15
+ **Actions:**
16
+ - Read relevant files to understand context
17
+ - Identify the root cause, not just symptoms
18
+ - Map out the codebase structure affected
19
+ - Ask clarifying questions if information is missing
20
+
21
+ **Output format:**
22
+ ```
23
+ <proposed_plan>
24
+ ## Understanding
25
+
26
+ ### What I Found
27
+ - [Key findings from investigation]
28
+ - [Root cause analysis]
29
+ - [Files and lines involved]
30
+
31
+ ### Context
32
+ - [Relevant code patterns]
33
+ - [Dependencies and side effects]
34
+ </proposed_plan>
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Phase 2: Intent (Proposal)
40
+
41
+ **Goal:** Present a clear, actionable plan for user approval.
42
+
43
+ **Actions:**
44
+ - Propose specific changes with file paths and line numbers
45
+ - Explain the "why" behind each decision
46
+ - Consider alternatives and trade-offs
47
+ - Make the plan "decision complete" (user can approve without asking follow-ups)
48
+
49
+ **Output format:**
50
+ ```
51
+ <proposed_plan>
52
+ ## Proposed Plan
53
+
54
+ ### Objective
55
+ [Clear statement of what we're fixing/building]
56
+
57
+ ### Approach
58
+ 1. [Step 1 with specific files/lines]
59
+ 2. [Step 2 with specific files/lines]
60
+ 3. [Step 3...]
61
+
62
+ ### Changes
63
+ - **File: `src/path/to/file.ts`**
64
+ - Line 42: Change X to Y because...
65
+ - Add function `helper()` at line 50 to...
66
+
67
+ ### Alternatives Considered
68
+ - Alternative A: [why not chosen]
69
+ - Alternative B: [why not chosen]
70
+
71
+ ### Risks & Mitigations
72
+ - [Potential issue] → [mitigation strategy]
73
+
74
+ ### Success Criteria
75
+ - [How we'll know this is working]
76
+ - [Tests to run]
77
+ </proposed_plan>
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Phase 3: Implementation (Execution)
83
+
84
+ **Goal:** Execute the approved plan with precision.
85
+
86
+ **Actions:**
87
+ - Follow the approved plan exactly
88
+ - Use `enter_plan_mode` before making changes
89
+ - Make surgical, targeted edits
90
+ - Run tests after each change
91
+ - Use `exit_plan_mode` when complete
92
+
93
+ **Important:**
94
+ - Do NOT deviate from the approved plan without user consent
95
+ - If you discover new issues, pause and propose an updated plan
96
+ - Commit changes with clear, descriptive messages
97
+
98
+ ---
99
+
100
+ ## Plan Mode Rules
101
+
102
+ ### DO:
103
+ ✅ Use `<proposed_plan>` tags for all plans
104
+ ✅ Make plans "decision complete" (no follow-up questions needed)
105
+ ✅ Include specific file paths and line numbers
106
+ ✅ Explain reasoning behind each decision
107
+ ✅ Consider alternatives and trade-offs
108
+ ✅ Use `enter_plan_mode` before making changes
109
+ ✅ Run tests and verify changes work
110
+
111
+ ### DON'T:
112
+ ❌ Make changes without a proposed plan
113
+ ❌ Propose vague or incomplete plans
114
+ ❌ Skip the grounding phase (understand first!)
115
+ ❌ Deviate from approved plan without consent
116
+ ❌ Make multiple unrelated changes in one plan
117
+ ❌ Forget to run tests after implementation
118
+
119
+ ---
120
+
121
+ ## When to Use Plan Mode
122
+
123
+ **Use Plan Mode when:**
124
+ - Fixing bugs (especially complex ones)
125
+ - Adding new features
126
+ - Refactoring code
127
+ - Making architectural changes
128
+ - Any change that affects multiple files
129
+
130
+ **Skip Plan Mode for:**
131
+ - Simple, one-line fixes
132
+ - Reading files to gather information
133
+ - Quick exploratory tasks
134
+ - User explicitly asks to "just fix it"
135
+
136
+ ---
137
+
138
+ ## Plan Mode Workflow Summary
139
+
140
+ 1. **Investigate** → Read files, understand the problem
141
+ 2. **Propose** → Create `<proposed_plan>` with clear steps
142
+ 3. **Wait** → Let user review and approve
143
+ 4. **Activate** → Call `enter_plan_mode` when approved
144
+ 5. **Implement** → Make the changes exactly as proposed
145
+ 6. **Verify** → Run tests, confirm success
146
+ 7. **Complete** → Call `exit_plan_mode` and report results
147
+
148
+ ---
149
+
150
+ ## Example Plan
151
+
152
+ ```
153
+ <proposed_plan>
154
+ ## Understanding
155
+
156
+ ### What I Found
157
+ - Null pointer in `src/auth/validate.ts:42` when `Session.expired` is true
158
+ - The `user` field is undefined but code tries to access `user.id`
159
+ - This happens because token cache isn't cleared on expiry
160
+
161
+ ### Context
162
+ - `validateToken()` returns cached token even when expired
163
+ - `getSession()` doesn't check if user exists before accessing properties
164
+
165
+ ## Proposed Plan
166
+
167
+ ### Objective
168
+ Fix null pointer exception when session expires but token is cached
169
+
170
+ ### Approach
171
+ 1. Add null check in `validate.ts:42` before accessing `user.id`
172
+ 2. Clear token cache when session expires
173
+ 3. Add test case for expired session with cached token
174
+
175
+ ### Changes
176
+ - **File: `src/auth/validate.ts`**
177
+ - Line 42: Add `if (!user) return { valid: false, error: 'Session expired' };`
178
+ - Line 58: Add cache invalidation when `Session.expired === true`
179
+
180
+ ### Alternatives Considered
181
+ - Alternative: Always refresh token on expiry → Too aggressive, impacts performance
182
+ - Alternative: Remove caching entirely → Loses benefit of caching valid tokens
183
+
184
+ ### Risks & Mitigations
185
+ - Risk: Breaking existing valid sessions → Mitigation: Add comprehensive tests
186
+ - Risk: Performance impact from cache invalidation → Mitigation: Only invalidate on confirmed expiry
187
+
188
+ ### Success Criteria
189
+ - No null pointer on expired session
190
+ - Valid sessions still cached and fast
191
+ - Test coverage for edge case added
192
+ </proposed_plan>
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Tool Usage in Plan Mode
198
+
199
+ When `enter_plan_mode` is active:
200
+ - All write/execute tools require explicit confirmation
201
+ - This ensures you review each change before it happens
202
+ - Use this as a safety check, not a barrier
203
+
204
+ Tools that require confirmation in Plan Mode:
205
+ - `edit_tool`
206
+ - `file_write`
207
+ - `shell_command`
208
+ - `spawn_agent`
209
+
210
+ ---
211
+
212
+ ## Final Notes
213
+
214
+ **Remember:** A good plan is specific, actionable, and decision-complete.
215
+ The user should be able to approve your plan without asking "what about X?" or "why this approach?"
216
+
217
+ If you're unsure, **ask questions first** before proposing the plan.
218
+ Better to spend 2 minutes clarifying than 20 minutes fixing the wrong thing.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Marcador de invocação de tool — bullet compacto (U+2022 •).
3
+ */
4
+ export const TOOL_INVOCATION_MARK = "\u2022 ";
5
+ /** Indentação do detalhe da tool. */
6
+ export const TOOL_DETAIL_PREFIX = " ";
7
+ /**
8
+ * Prefixo da linha de resultado sob a invocação (• List / path) — ramo `└` em dim, estilo BluMa.
9
+ */
10
+ export const RESULT_LINE_GUTTER = " \u2514 ";