diff --git a/claude_cli.go b/claude_cli.go index 6a071bd..a45e517 100644 --- a/claude_cli.go +++ b/claude_cli.go @@ -108,7 +108,6 @@ func SpawnCLI(projectDir string) (*CLIProcess, error) { "--input-format", "stream-json", "--output-format", "stream-json", "--verbose", - "--include-partial-messages", } cmd := exec.Command("claude", args...) diff --git a/fragments.go b/fragments.go index c59f99d..7c8f5a6 100644 --- a/fragments.go +++ b/fragments.go @@ -8,9 +8,13 @@ import ( ) // FragmentUserMessage returns an HTML fragment for a user message. -func FragmentUserMessage(text string) string { +func FragmentUserMessage(text string, isPlan bool) string { escaped := html.EscapeString(text) - return fmt.Sprintf(`
%s
`, escaped) + modeClass := "message-user" + if isPlan { + modeClass = "message-user message-user-plan" + } + return fmt.Sprintf(`
%s
`, modeClass, escaped) } // FragmentAssistantStart returns the opening tag for an assistant message with streaming. diff --git a/fragments_test.go b/fragments_test.go index f6dfa41..e3b3bcd 100644 --- a/fragments_test.go +++ b/fragments_test.go @@ -6,7 +6,7 @@ import ( ) func TestFragmentUserMessage(t *testing.T) { - f := FragmentUserMessage("Hello ") + f := FragmentUserMessage("Hello ", false) if !strings.Contains(f, "message-user") { t.Error("missing message-user class") } @@ -16,6 +16,15 @@ func TestFragmentUserMessage(t *testing.T) { if !strings.Contains(f, `hx-swap-oob="beforeend"`) { t.Error("missing OOB swap") } + if strings.Contains(f, "message-user-plan") { + t.Error("should not have plan class in code mode") + } + + // Plan mode + fp := FragmentUserMessage("analyze this", true) + if !strings.Contains(fp, "message-user-plan") { + t.Error("missing plan class in plan mode") + } } func TestFragmentAssistantStart(t *testing.T) { diff --git a/static/style.css b/static/style.css index 5c0b6b2..8428146 100644 --- a/static/style.css +++ b/static/style.css @@ -107,6 +107,15 @@ a:hover { background: var(--accent-hover); } +.btn-secondary { + background: var(--bg-tertiary); + margin-right: 0.5rem; +} + +.btn-secondary:hover { + background: var(--border); +} + .btn-full { width: 100%; } @@ -121,6 +130,22 @@ a:hover { text-align: center; } +.success-msg { + background: rgba(76, 175, 80, 0.15); + color: var(--success); + padding: 0.6rem 1rem; + border-radius: 8px; + margin-bottom: 1rem; + font-size: 0.9rem; + text-align: center; +} + +.form-footer { + margin-top: 1rem; + text-align: center; + font-size: 0.85rem; +} + /* Projects grid */ .projects-container { max-width: 1000px; @@ -352,6 +377,59 @@ a:hover { 80%, 100% { opacity: 1; } } +/* Mode bar */ +.mode-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.3rem 0; + margin-bottom: 0.3rem; +} + +.mode-indicator { + display: flex; + gap: 0; + border: 1px solid var(--border); + border-radius: 4px; + overflow: hidden; +} + +.mode-label { + padding: 0.2rem 0.6rem; + font-size: 0.75rem; + font-family: 'JetBrains Mono', 'Fira Code', monospace; + color: var(--text-muted); + cursor: pointer; + transition: all 0.15s; + user-select: none; +} + +.mode-label:hover { + color: var(--text-secondary); + background: rgba(255, 255, 255, 0.05); +} + +.mode-label.mode-active { + background: var(--accent); + color: #fff; +} + +.mode-hint { + font-size: 0.65rem; + color: var(--text-muted); + font-family: 'JetBrains Mono', 'Fira Code', monospace; +} + +/* Plan mode user messages */ +.message-user-plan { + color: #64b5f6 !important; +} + +.message-user-plan::before { + content: "⊞ " !important; + color: #64b5f6 !important; +} + /* Input area — terminal prompt */ .chat-input-area { padding: 0.5rem 1rem; diff --git a/templates/chat.html b/templates/chat.html index caf8d48..156be10 100644 --- a/templates/chat.html +++ b/templates/chat.html @@ -42,7 +42,15 @@
+
+ + Code + Plan + + Shift+Tab za promenu moda +
+
@@ -61,6 +69,27 @@
diff --git a/ws.go b/ws.go index 9a9f1ea..8f0849a 100644 --- a/ws.go +++ b/ws.go @@ -20,6 +20,7 @@ var upgrader = websocket.Upgrader{ type wsMessage struct { Message string `json:"message"` + Mode string `json:"mode"` // "code" or "plan" } // WSHandler handles WebSocket connections for chat. @@ -134,8 +135,10 @@ func (wh *WSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { continue } + isPlan := msg.Mode == "plan" + // Add user message to buffer (broadcasts to all subscribers including us) - userFragment := FragmentUserMessage(text) + userFragment := FragmentUserMessage(text, isPlan) sess.AddMessage(ChatMessage{ Role: "user", Content: userFragment, @@ -145,8 +148,14 @@ func (wh *WSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Clear input and show typing send(FragmentCombine(FragmentClearInput(), FragmentTypingIndicator(true))) + // In plan mode, wrap the message with planning instructions + cliText := text + if isPlan { + cliText = "[PLAN MODE] Only analyze, plan, and explain. Do NOT use tools to write, edit, or create files. Do NOT execute commands. Just provide your analysis and step-by-step plan.\n\n" + text + } + // Send to claude CLI - if err := sess.Process.Send(text); err != nil { + if err := sess.Process.Send(cliText); err != nil { log.Printf("Send to CLI error: %v", err) send(FragmentSystemMessage("Greška pri slanju poruke")) } @@ -265,6 +274,10 @@ func (wh *WSHandler) listenEvents(sess *ChatSession) { func (wh *WSHandler) handleStreamEvent(sess *ChatSession, se *StreamEvent, currentMsgID *string, currentText *strings.Builder, msgCounter *int) { switch se.Type { case "content_block_start": + // Only create a message div for text blocks, skip tool_use blocks + if se.ContentBlock != nil && se.ContentBlock.Type != "text" { + return + } *msgCounter++ *currentMsgID = fmt.Sprintf("msg-%d-%d", time.Now().UnixMilli(), *msgCounter) currentText.Reset()