Skip to content

Commit cca3827

Browse files
feat: virtual tabs for MCP and themes, fix dragging opacity, permission denials, add-dir support
1 parent df4e17f commit cca3827

16 files changed

Lines changed: 873 additions & 49 deletions

File tree

crates/session/src/claude.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,16 @@ impl ClaudeSession {
2424
permission_mode: Option<&str>,
2525
) -> Result<(Self, mpsc::UnboundedReceiver<AgentEvent>)> {
2626
let perm_mode = permission_mode.unwrap_or("bypassPermissions");
27+
let cwd_str = cwd.to_string_lossy().to_string();
2728
let mut args = vec![
2829
"-p",
2930
"--output-format", "stream-json",
3031
"--verbose",
3132
"--input-format", "stream-json",
3233
"--include-partial-messages",
3334
"--permission-mode", perm_mode,
35+
// Allow the working directory for file operations
36+
"--add-dir", &cwd_str,
3437
];
3538
if let Some(m) = model {
3639
args.push("--model");
@@ -554,6 +557,31 @@ fn handle_result(obj: &Value, current_model: &Option<String>) -> Vec<AgentEvent>
554557
});
555558
}
556559

560+
// Check for permission denials and surface them
561+
if let Some(denials) = obj.get("permission_denials").and_then(|d| d.as_array()) {
562+
if !denials.is_empty() {
563+
let denied_tools: Vec<String> = denials
564+
.iter()
565+
.map(|d| {
566+
let tool = d.get("tool_name").and_then(|t| t.as_str()).unwrap_or("unknown");
567+
let input = d.get("tool_input")
568+
.and_then(|i| i.get("command"))
569+
.and_then(|c| c.as_str())
570+
.or_else(|| d.get("tool_input").and_then(|i| i.get("file_path")).and_then(|f| f.as_str()))
571+
.unwrap_or("");
572+
if input.is_empty() {
573+
tool.to_string()
574+
} else {
575+
format!("{tool}: {input}")
576+
}
577+
})
578+
.collect();
579+
events.push(AgentEvent::SessionError {
580+
message: format!("Permission denied for: {}", denied_tools.join(", ")),
581+
});
582+
}
583+
}
584+
557585
let is_error = obj.get("is_error").and_then(|e| e.as_bool()).unwrap_or(false);
558586
if is_error {
559587
events.push(AgentEvent::SessionError {

crates/tauri-app/frontend/src/App.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ProviderPicker } from "./components/composer/ProviderPicker";
1212
import { CommandPalette } from "./components/shared/CommandPalette";
1313
import { SearchOverlay } from "./components/shared/SearchOverlay";
1414
import { UsageDashboard } from "./components/settings/UsageDashboard";
15+
import { ThemeSelector } from "./components/settings/ThemeSelector";
1516
import { SplitView } from "./components/shared/SplitView";
1617
import { BrowserPanel } from "./components/browser/BrowserPanel";
1718
import { DiffEditor } from "./components/diff/DiffEditor";
@@ -229,6 +230,10 @@ export function App() {
229230
<UsageDashboard />
230231
</Show>
231232

233+
<Show when={store.themeOpen}>
234+
<ThemeSelector />
235+
</Show>
236+
232237
<Show when={showWelcome()}>
233238
<WelcomeScreen onDismiss={() => setShowWelcome(false)} />
234239
</Show>
@@ -301,6 +306,14 @@ export function App() {
301306
min-width: 0;
302307
overflow: hidden;
303308
}
309+
.main-panel-side > * {
310+
flex: 1;
311+
min-height: 0;
312+
overflow: auto;
313+
}
314+
.main-panel-side > * + * {
315+
border-top: 1px solid var(--border);
316+
}
304317
`}</style>
305318
</>
306319
);

crates/tauri-app/frontend/src/components/chat/ChatArea.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Markdown } from "./Markdown";
55
import { ToolUseCard } from "./ToolUseCard";
66
import { ThinkingBlock } from "./ThinkingBlock";
77
import { PrDashboard } from "../github/PrDashboard";
8+
import { McpPanel } from "../sidebar/McpPanel";
89
import type { ContentBlock } from "../../types";
910

1011
export function ChatArea() {
@@ -122,8 +123,27 @@ export function ChatArea() {
122123
appStore.setStore("composerText", text);
123124
}
124125

126+
const isVirtualTab = () => store.activeTab?.startsWith("__") || false;
127+
125128
return (
126129
<div class="chat-area" ref={scrollRef} onScroll={handleScroll}>
130+
{/* Virtual tab content */}
131+
<Show when={store.activeTab === "__mcp__"}>
132+
<div class="virtual-tab-content">
133+
<McpPanel />
134+
</div>
135+
</Show>
136+
<Show when={store.activeTab === "__themes__"}>
137+
<div class="virtual-tab-content">
138+
<Show when={typeof (window as any).__THEME_SELECTOR__ !== "undefined"}
139+
fallback={<div class="virtual-tab-placeholder"><p>Theme selector loading...</p></div>}>
140+
{/* ThemeSelector will be rendered here once the agent finishes */}
141+
</Show>
142+
</div>
143+
</Show>
144+
145+
{/* Regular chat content */}
146+
<Show when={!isVirtualTab()}>
127147
<Show
128148
when={store.activeTab}
129149
fallback={
@@ -252,6 +272,7 @@ export function ChatArea() {
252272
</div>
253273
</Show>
254274
</Show>
275+
</Show>
255276
</div>
256277
);
257278
}
@@ -460,6 +481,21 @@ if (!document.getElementById("chat-styles")) {
460481
flex-direction: column;
461482
}
462483
484+
/* ── Virtual tab content ── */
485+
.virtual-tab-content {
486+
flex: 1;
487+
overflow-y: auto;
488+
padding: 16px;
489+
}
490+
.virtual-tab-placeholder {
491+
flex: 1;
492+
display: flex;
493+
align-items: center;
494+
justify-content: center;
495+
color: var(--text-tertiary);
496+
font-size: 14px;
497+
}
498+
463499
/* ── Empty states ── */
464500
.chat-empty {
465501
flex: 1;

crates/tauri-app/frontend/src/components/chat/Markdown.tsx

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ if (!document.getElementById("md-styles")) {
8282
border-radius: 4px;
8383
font-family: var(--font-mono);
8484
font-size: 0.88em;
85-
color: #dda0f7;
85+
color: var(--hljs-inline-code, #dda0f7);
8686
}
8787
8888
/* Code blocks */
@@ -105,7 +105,7 @@ if (!document.getElementById("md-styles")) {
105105
font-family: var(--font-mono);
106106
font-size: 13px;
107107
line-height: 1.5;
108-
color: #ebebf0;
108+
color: var(--hljs-code-text, #ebebf0);
109109
}
110110
.md-code-lang {
111111
position: absolute;
@@ -197,26 +197,26 @@ if (!document.getElementById("md-styles")) {
197197
/* Strong and emphasis */
198198
.md-render strong { color: var(--text, #ebebf0); font-weight: 600; }
199199
200-
/* highlight.js token colors (dark theme) */
201-
.hljs-keyword, .hljs-selector-tag, .hljs-type { color: #c678dd; }
202-
.hljs-string, .hljs-addition { color: #98c379; }
203-
.hljs-number, .hljs-literal { color: #d19a66; }
204-
.hljs-comment, .hljs-deletion { color: #5c6370; font-style: italic; }
205-
.hljs-built_in, .hljs-builtin-name { color: #e6c07b; }
206-
.hljs-function .hljs-title, .hljs-title.function_ { color: #61afef; }
207-
.hljs-attr, .hljs-attribute { color: #d19a66; }
208-
.hljs-variable, .hljs-template-variable { color: #e06c75; }
209-
.hljs-params { color: #abb2bf; }
210-
.hljs-meta { color: #61afef; }
211-
.hljs-regexp { color: #56b6c2; }
212-
.hljs-tag { color: #e06c75; }
213-
.hljs-name { color: #e06c75; }
214-
.hljs-selector-id, .hljs-selector-class { color: #e6c07b; }
215-
.hljs-symbol, .hljs-bullet { color: #56b6c2; }
216-
.hljs-link { color: #61afef; text-decoration: underline; }
217-
.hljs-punctuation { color: #abb2bf; }
218-
.hljs-property { color: #e06c75; }
219-
.hljs-title.class_ { color: #e6c07b; }
200+
/* highlight.js token colors (themed via CSS variables) */
201+
.hljs-keyword, .hljs-selector-tag, .hljs-type { color: var(--hljs-keyword, #c678dd); }
202+
.hljs-string, .hljs-addition { color: var(--hljs-string, #98c379); }
203+
.hljs-number, .hljs-literal { color: var(--hljs-number, #d19a66); }
204+
.hljs-comment, .hljs-deletion { color: var(--hljs-comment, #5c6370); font-style: italic; }
205+
.hljs-built_in, .hljs-builtin-name { color: var(--hljs-builtin, #e6c07b); }
206+
.hljs-function .hljs-title, .hljs-title.function_ { color: var(--hljs-function, #61afef); }
207+
.hljs-attr, .hljs-attribute { color: var(--hljs-attr, #d19a66); }
208+
.hljs-variable, .hljs-template-variable { color: var(--hljs-variable, #e06c75); }
209+
.hljs-params { color: var(--hljs-params, #abb2bf); }
210+
.hljs-meta { color: var(--hljs-meta, #61afef); }
211+
.hljs-regexp { color: var(--hljs-regexp, #56b6c2); }
212+
.hljs-tag { color: var(--hljs-tag, #e06c75); }
213+
.hljs-name { color: var(--hljs-tag, #e06c75); }
214+
.hljs-selector-id, .hljs-selector-class { color: var(--hljs-selector, #e6c07b); }
215+
.hljs-symbol, .hljs-bullet { color: var(--hljs-symbol, #56b6c2); }
216+
.hljs-link { color: var(--hljs-link, #61afef); text-decoration: underline; }
217+
.hljs-punctuation { color: var(--hljs-punctuation, #abb2bf); }
218+
.hljs-property { color: var(--hljs-property, #e06c75); }
219+
.hljs-title.class_ { color: var(--hljs-class, #e6c07b); }
220220
`;
221221
document.head.appendChild(style);
222222
}

crates/tauri-app/frontend/src/components/composer/Composer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { SlashCommand } from "../../ipc";
99
export function Composer() {
1010
const { store, setStore, sendUserMessage } = appStore;
1111

12-
const isActive = () => store.activeTab !== null;
12+
const isActive = () => store.activeTab !== null && !store.activeTab.startsWith("__");
1313
const isGenerating = () => {
1414
if (!store.activeTab) return false;
1515
const s = store.sessionStatuses[store.activeTab];

crates/tauri-app/frontend/src/components/diff/DiffEditor.tsx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export function DiffEditor(props: { cwd: string }) {
3333
const [loading, setLoading] = createSignal(true);
3434
const [error, setError] = createSignal<string | null>(null);
3535
const [collapsedHunks, setCollapsedHunks] = createSignal<Set<string>>(new Set());
36+
const [gitPanelOpen, setGitPanelOpen] = createSignal(true);
3637

3738
function close() {
3839
setStore("diffPanelOpen", false);
@@ -230,8 +231,18 @@ export function DiffEditor(props: { cwd: string }) {
230231
</div>
231232
</div>
232233

233-
{/* Git management panel */}
234-
<GitPanel cwd={props.cwd} />
234+
{/* Git management panel — collapsible */}
235+
<div class="de-git-section">
236+
<button class="de-git-toggle" onClick={() => setGitPanelOpen(!gitPanelOpen())}>
237+
<svg class="de-git-chevron" classList={{ "de-git-chevron--open": gitPanelOpen() }} width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
238+
<polyline points="9 18 15 12 9 6" />
239+
</svg>
240+
<span>Git</span>
241+
</button>
242+
<Show when={gitPanelOpen()}>
243+
<GitPanel cwd={props.cwd} />
244+
</Show>
245+
</div>
235246
</div>
236247
);
237248
}
@@ -529,4 +540,34 @@ const DIFF_STYLES = `
529540
color: var(--text-tertiary);
530541
opacity: 0.5;
531542
}
543+
544+
/* Collapsible git section */
545+
.de-git-section {
546+
flex-shrink: 0;
547+
border-top: 1px solid var(--border);
548+
}
549+
.de-git-toggle {
550+
display: flex;
551+
align-items: center;
552+
gap: 6px;
553+
width: 100%;
554+
padding: 6px 12px;
555+
font-size: 11px;
556+
font-weight: 600;
557+
color: var(--text-tertiary);
558+
text-transform: uppercase;
559+
letter-spacing: 0.04em;
560+
transition: color 0.1s, background 0.1s;
561+
}
562+
.de-git-toggle:hover {
563+
color: var(--text-secondary);
564+
background: var(--bg-hover);
565+
}
566+
.de-git-chevron {
567+
flex-shrink: 0;
568+
transition: transform 0.15s ease;
569+
}
570+
.de-git-chevron--open {
571+
transform: rotate(90deg);
572+
}
532573
`;

0 commit comments

Comments
 (0)