Skip to content

Commit 4a7546f

Browse files
committed
feat(agent): add git context to system prompt, improve sidebar resizing and chat input UI
- Add get_git_context() to provide agent with branch, staged files, and recent commits - Increase agent sidebar max width from 500px to 70% of window - Improve chat input status bar with larger elements and better styling - Clean up stop/send button layout
1 parent 2c6420b commit 4a7546f

6 files changed

Lines changed: 238 additions & 238 deletions

File tree

clif-pad-ide/src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

clif-pad-ide/src-tauri/src/commands/agent.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use std::sync::{Arc, Mutex};
55
use tauri::{Emitter, Manager};
66
use uuid::Uuid;
77

8+
use crate::commands::git::get_git_context;
9+
810
static AGENT_SESSIONS: std::sync::LazyLock<Arc<Mutex<HashMap<String, tokio::sync::oneshot::Sender<()>>>>> =
911
std::sync::LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
1012

@@ -238,6 +240,11 @@ fn build_system_prompt(workspace_dir: &str, context: Option<&str>) -> String {
238240
prompt.push_str("\n\n---\n");
239241
}
240242

243+
// Add git context (branch, modified files, recent commits)
244+
let git_context = get_git_context(workspace_dir);
245+
prompt.push_str("\n## Current Git State\n\n");
246+
prompt.push_str(&git_context);
247+
241248
if let Some(ctx) = context {
242249
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(ctx) {
243250
if let Some(active_file) = parsed.get("activeFile").and_then(|v| v.as_str()) {

clif-pad-ide/src-tauri/src/commands/git.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,3 +589,82 @@ pub fn git_stage(path: String, files: Vec<String>) -> Result<(), String> {
589589

590590
Ok(())
591591
}
592+
593+
/// Gather git context for AI agent system prompt
594+
/// Returns: current branch, modified/untracked files, and recent commits
595+
pub fn get_git_context(workspace_dir: &str) -> String {
596+
let mut context = String::new();
597+
598+
// Get current branch
599+
let branch_output = Command::new("git")
600+
.args(["branch", "--show-current"])
601+
.current_dir(workspace_dir)
602+
.output();
603+
604+
if let Ok(output) = branch_output {
605+
if output.status.success() {
606+
let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
607+
if !branch.is_empty() {
608+
context.push_str(&format!("Current branch: {}\n", branch));
609+
}
610+
}
611+
}
612+
613+
// Get modified/untracked files (last 10)
614+
let status_output = Command::new("git")
615+
.args(["status", "--porcelain"])
616+
.current_dir(workspace_dir)
617+
.output();
618+
619+
if let Ok(output) = status_output {
620+
if output.status.success() {
621+
let stdout = String::from_utf8_lossy(&output.stdout);
622+
let files: Vec<&str> = stdout.lines().filter(|l| !l.is_empty()).take(10).collect();
623+
if !files.is_empty() {
624+
context.push_str("\nModified/untracked files:\n");
625+
for file in files {
626+
// Parse porcelain format: "XY filename"
627+
let file_path = if file.len() > 3 { &file[3..] } else { file };
628+
let status_char = file.chars().next().unwrap_or(' ');
629+
let status_label = match status_char {
630+
'M' => "modified",
631+
'A' => "added",
632+
'D' => "deleted",
633+
'?' => "untracked",
634+
'R' => "renamed",
635+
_ => "changed",
636+
};
637+
context.push_str(&format!(" [{}] {}\n", status_label, file_path));
638+
}
639+
if stdout.lines().count() > 10 {
640+
context.push_str(&format!(" ... and {} more files\n", stdout.lines().count() - 10));
641+
}
642+
}
643+
}
644+
}
645+
646+
// Get recent commits (last 5)
647+
let log_output = Command::new("git")
648+
.args(["log", "--oneline", "-5"])
649+
.current_dir(workspace_dir)
650+
.output();
651+
652+
if let Ok(output) = log_output {
653+
if output.status.success() {
654+
let stdout = String::from_utf8_lossy(&output.stdout);
655+
let commits: Vec<&str> = stdout.lines().filter(|l| !l.is_empty()).collect();
656+
if !commits.is_empty() {
657+
context.push_str("\nRecent commits:\n");
658+
for commit in commits {
659+
context.push_str(&format!(" {}\n", commit));
660+
}
661+
}
662+
}
663+
}
664+
665+
if context.is_empty() {
666+
"No git repository found or no changes.".to_string()
667+
} else {
668+
context
669+
}
670+
}

clif-pad-ide/src/components/agent/AgentChatPanel.tsx

Lines changed: 74 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -700,16 +700,17 @@ const AgentChatPanel: Component = () => {
700700
</div>
701701
</Show>
702702

703+
{/* Input row: attach | textarea | send - all on one line */}
703704
<div
704-
class="flex items-end gap-2 rounded-xl px-3 py-2"
705+
class="flex flex-row items-end gap-2 rounded-xl px-3 py-2"
705706
style={{
706707
background: "var(--bg-base)",
707708
border: "1px solid var(--border-default)",
708709
}}
709710
>
710-
{/* Attach file button */}
711+
{/* Attach file button - LEFT */}
711712
<button
712-
class="flex items-center justify-center shrink-0 rounded p-1 mb-0.5"
713+
class="flex items-center justify-center shrink-0 rounded p-1"
713714
style={{
714715
color: "var(--text-muted)",
715716
background: "transparent",
@@ -730,6 +731,7 @@ const AgentChatPanel: Component = () => {
730731
</svg>
731732
</button>
732733

734+
{/* Textarea - CENTER */}
733735
<textarea
734736
ref={inputRef}
735737
class="flex-1 resize-none outline-none"
@@ -746,10 +748,10 @@ const AgentChatPanel: Component = () => {
746748
}}
747749
placeholder={
748750
queuedMessages().length > 0
749-
? `Message queued (${queuedMessages().length} in line) — Shift+Enter to force push`
751+
? `${queuedMessages().length} queued — Shift+Enter to force`
750752
: (agentStreaming() || clifInitializing())
751-
? "Type to queue next message..."
752-
: "Ask the agent... (paste images with ⌘V)"
753+
? "Type to queue..."
754+
: "Ask the agent... (@ for files)"
753755
}
754756
rows={1}
755757
value={inputValue()}
@@ -778,166 +780,119 @@ const AgentChatPanel: Component = () => {
778780
onPaste={handlePaste}
779781
/>
780782

781-
{/* Queued messages badge */}
782-
<Show when={queuedMessages().length > 0}>
783-
<button
784-
class="absolute top-[-8px] right-[-8px] z-10 flex items-center justify-center rounded-full"
785-
style={{
786-
background: "var(--accent-primary)",
787-
color: "#fff",
788-
"font-size": "11px",
789-
"min-width": "20px",
790-
height: "20px",
791-
padding: "0 6px",
792-
"font-weight": "600",
793-
border: "2px solid var(--bg-base)",
794-
cursor: "pointer",
795-
}}
796-
onClick={() => handleSend(true)}
797-
title={`Force push: cancel current agent and send next message (${queuedMessages().length} queued)`}
798-
>
799-
{queuedMessages().length}
800-
</button>
801-
</Show>
802-
</div>
803-
804-
<div class="flex items-center justify-between mt-1 px-1">
805-
{/* Send / Stop streaming button */}
783+
{/* Send / Stop / Queue button - RIGHT */}
806784
<Show
807-
when={!agentStreaming()}
785+
when={agentStreaming()}
808786
fallback={
809787
<Show
810-
when={inputValue().trim()}
788+
when={queuedMessages().length > 0}
811789
fallback={
812790
<button
813-
class="flex items-center justify-center shrink-0 rounded-lg p-1.5 mb-0.5 transition-colors"
791+
class="flex items-center justify-center shrink-0 rounded-lg p-1.5 transition-colors"
814792
style={{
815-
background: "color-mix(in srgb, var(--accent-red) 12%, transparent)",
816-
color: "var(--accent-red)",
817-
border: "1px solid color-mix(in srgb, var(--accent-red) 25%, transparent)",
818-
cursor: "pointer",
793+
background: (inputValue().trim() || pastedImages().length > 0)
794+
? "var(--accent-primary)"
795+
: "var(--bg-hover)",
796+
color: (inputValue().trim() || pastedImages().length > 0) ? "#fff" : "var(--text-muted)",
797+
border: "none",
798+
cursor: (inputValue().trim() || pastedImages().length > 0) ? "pointer" : "default",
819799
}}
820-
onClick={stopAgent}
821-
title="Stop streaming response"
800+
onClick={() => handleSend(false)}
801+
disabled={!inputValue().trim() && pastedImages().length === 0}
802+
title="Send message"
822803
>
823-
<StopIcon />
804+
<SendIcon />
824805
</button>
825806
}
826807
>
827-
{/* Queue button — send after agent finishes */}
828808
<button
829-
class="flex items-center justify-center shrink-0 rounded-lg p-1.5 mb-0.5 transition-colors"
809+
class="flex items-center justify-center shrink-0 rounded-lg px-2 py-1 transition-colors"
830810
style={{
831-
background: "color-mix(in srgb, var(--accent-primary) 15%, transparent)",
832-
color: "var(--accent-primary)",
833-
border: "1px solid color-mix(in srgb, var(--accent-primary) 30%, transparent)",
811+
background: "var(--accent-primary)",
812+
color: "#fff",
813+
border: "none",
834814
cursor: "pointer",
815+
"font-size": "11px",
816+
"font-weight": "500",
835817
}}
836-
onClick={() => handleSend(false)}
837-
title="Queue message — sends when agent finishes"
818+
onClick={() => handleSend(true)}
819+
title="Force send now"
838820
>
839-
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
840-
<line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/>
841-
</svg>
821+
{queuedMessages().length}
822+
<span style={{ opacity: 0.8, "margin-left": "4px" }}>Force</span>
842823
</button>
843824
</Show>
844825
}
845826
>
846827
<button
847-
class="flex items-center justify-center shrink-0 rounded-lg p-1.5 mb-0.5 transition-colors"
828+
class="flex items-center justify-center shrink-0 rounded-lg p-1.5 transition-colors"
848829
style={{
849-
background: (inputValue().trim() || pastedImages().length > 0)
850-
? "var(--accent-primary)"
851-
: "var(--bg-hover)",
852-
color: (inputValue().trim() || pastedImages().length > 0) ? "#fff" : "var(--text-muted)",
830+
background: "var(--accent-red)",
831+
color: "#fff",
853832
border: "none",
854-
cursor: (inputValue().trim() || pastedImages().length > 0) ? "pointer" : "default",
833+
cursor: "pointer",
855834
}}
856-
onClick={() => handleSend(false)}
857-
disabled={!inputValue().trim() && pastedImages().length === 0}
858-
title="Send message"
835+
onClick={stopAgent}
836+
title="Stop agent"
859837
>
860-
<SendIcon />
838+
<StopIcon />
861839
</button>
862840
</Show>
863841
</div>
842+
843+
{/* Status bar: streaming | web search + tokens - ONE LINE */}
864844
<div
865-
class="flex items-center justify-between mt-1 px-1"
866-
style={{ "font-size": `${fontSize() - 4}px`, color: "var(--text-muted)" }}
845+
class="flex flex-row items-center justify-between mt-2 px-1"
846+
style={{ height: "20px", "font-size": "12px", color: "var(--text-muted)" }}
867847
>
868-
{/* Left: status or hint */}
869-
<Show when={agentStreaming()}
870-
fallback={<span>Enter to send, Shift+Enter for newline</span>}
871-
>
872-
<div class="flex items-center gap-1.5">
873-
<span
874-
class="inline-block animate-pulse"
875-
style={{ width: "5px", height: "5px", "border-radius": "50%", background: "var(--accent-yellow)", "flex-shrink": "0" }}
876-
/>
877-
<span>Agent running</span>
878-
<button
879-
class="flex items-center gap-1 rounded px-1.5 py-0.5 transition-colors"
880-
style={{
881-
background: "transparent", color: "var(--text-muted)",
882-
border: "1px solid var(--border-default)", cursor: "pointer",
883-
"font-size": `${fontSize() - 4}px`, "font-weight": "600",
884-
}}
885-
onMouseEnter={(e) => {
886-
(e.currentTarget as HTMLElement).style.background = "color-mix(in srgb, var(--accent-red) 10%, transparent)";
887-
(e.currentTarget as HTMLElement).style.color = "var(--accent-red)";
888-
(e.currentTarget as HTMLElement).style.borderColor = "color-mix(in srgb, var(--accent-red) 25%, transparent)";
889-
}}
890-
onMouseLeave={(e) => {
891-
(e.currentTarget as HTMLElement).style.background = "transparent";
892-
(e.currentTarget as HTMLElement).style.color = "var(--text-muted)";
893-
(e.currentTarget as HTMLElement).style.borderColor = "var(--border-default)";
894-
}}
895-
onClick={stopAgent}
896-
title="Stop all agent tasks"
897-
>
898-
<StopIcon />
899-
Stop
900-
</button>
901-
</div>
902-
</Show>
903-
<div class="flex items-center gap-2">
904-
{/* Web search toggle */}
848+
{/* Left: streaming indicator */}
849+
<div class="flex items-center" style={{ "min-width": "0" }}>
850+
<Show when={agentStreaming()}>
851+
<div class="flex items-center gap-2">
852+
<span
853+
class="inline-block animate-pulse"
854+
style={{ width: "8px", height: "8px", "border-radius": "50%", background: "var(--accent-yellow)", "flex-shrink": "0" }}
855+
/>
856+
<span style={{ "font-weight": "500" }}>Running...</span>
857+
</div>
858+
</Show>
859+
</div>
860+
861+
{/* Right: web search + tokens */}
862+
<div class="flex items-center gap-4" style={{ "flex-shrink": "0" }}>
905863
<Show when={settings().aiProvider === "openrouter"}>
906864
<button
907-
class="flex items-center gap-1 rounded px-1.5 py-0.5 transition-all"
865+
class="flex items-center gap-1.5 px-2 py-1 rounded-md transition-colors"
908866
style={{
909-
background: webSearchEnabled()
910-
? "color-mix(in srgb, var(--accent-blue) 15%, transparent)"
911-
: "transparent",
912867
color: webSearchEnabled() ? "var(--accent-blue)" : "var(--text-muted)",
913-
border: webSearchEnabled()
914-
? "1px solid color-mix(in srgb, var(--accent-blue) 30%, transparent)"
915-
: "1px solid transparent",
868+
background: webSearchEnabled() ? "rgba(59, 130, 246, 0.1)" : "transparent",
869+
border: "none",
916870
cursor: "pointer",
917-
"font-size": `${fontSize() - 4}px`,
918-
"font-family": "var(--font-sans)",
871+
"font-size": "12px",
872+
"font-weight": "500",
919873
}}
920-
onMouseEnter={(e) => { if (!webSearchEnabled()) (e.currentTarget as HTMLElement).style.background = "var(--bg-hover)"; }}
921-
onMouseLeave={(e) => { if (!webSearchEnabled()) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
922874
onClick={() => setWebSearchEnabled(!webSearchEnabled())}
923-
title={webSearchEnabled()
924-
? "Web search ON — model will fetch live results ($0.004/search)"
925-
: "Enable web search — appends :online to model via OpenRouter"}
875+
title={webSearchEnabled() ? "Web search enabled" : "Enable web search"}
926876
>
927-
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
928-
<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
877+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
878+
<circle cx="12" cy="12" r="10"/>
879+
<line x1="2" y1="12" x2="22" y2="12"/>
880+
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
929881
</svg>
930-
{webSearchEnabled() ? "Search on" : "Search"}
882+
<span>
883+
{webSearchEnabled() ? "Web on" : "Web"}
884+
</span>
931885
</button>
932886
</Show>
887+
933888
<Show when={agentTokens().prompt > 0}>
934-
<span style={{ "font-family": "var(--font-mono, monospace)" }}>
889+
<span style={{ "font-family": "var(--font-mono, monospace)", opacity: 0.8, "font-size": "12px" }}>
935890
{(() => {
936891
const t = agentTokens();
937892
const total = t.prompt + t.completion;
938893
const cost = (t.prompt * 3 + t.completion * 15) / 1_000_000;
939894
const totalStr = total >= 1000 ? `${(total / 1000).toFixed(1)}k` : `${total}`;
940-
return `${totalStr} tokens · ~$${cost.toFixed(4)}`;
895+
return `${totalStr} · $${cost.toFixed(4)}`;
941896
})()}
942897
</span>
943898
</Show>

0 commit comments

Comments
 (0)