@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.
- package/README.md +252 -998
- package/dist/WorkingTimer.js +130 -0
- package/dist/components/StreamingText.js +113 -0
- package/dist/components/reasoningText.js +18 -0
- package/dist/components/streamingTextFlush.js +11 -0
- package/dist/config/native_tools.json +1 -59
- package/dist/config/plan_mode_instructions.md +218 -0
- package/dist/constants/toolUiSymbols.js +10 -0
- package/dist/main.js +22193 -8156
- package/dist/native/bluma-clipboard.linux-x64-gnu.node +0 -0
- package/dist/native/index.d.ts +38 -0
- package/dist/native/index.js +319 -0
- package/dist/native/package.json +38 -0
- package/dist/theme/blumaTerminal.js +79 -0
- package/dist/theme/m3Layout.js +68 -0
- package/dist/utils/formatTurnDurationMs.js +16 -0
- package/dist/utils/toolActionLabels.js +167 -0
- package/package.json +37 -12
|
@@ -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 ";
|