Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions sidecar/scripts/prepare-node-binary.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,21 @@ function main() {
copyFileSync(sysNode, dest);
}

// Strip whatever signature came from Node Foundation; Tauri will re-sign
// with our Developer ID at bundle time.
// Strip Node Foundation's signature, then ad-hoc sign so the binary can
// actually execute. Background: on macOS 14.4+ (and 15+ more strictly),
// `amfid` SIGKILLs unsigned arm64 Mach-O binaries the moment they exec.
// The original Node Foundation signature would work for execution but
// doesn't match our final .app's signing chain, so production builds
// need it stripped. Replacing it with an ad-hoc signature (`--sign -`)
// is the only way to have the binary be both executable *now* (for
// `tauri dev`) and clean-slate for tauri-action's later `codesign
// --force` re-sign with the Developer ID cert.
try {
execSync(`codesign --remove-signature "${dest}"`, { stdio: "ignore" });
} catch {
// already unsigned — fine
}
execSync(`codesign --sign - --force --timestamp=none "${dest}"`, { stdio: "ignore" });
execSync(`chmod +x "${dest}"`);

const size = (statSync(dest).size / 1024 / 1024).toFixed(1);
Expand Down
153 changes: 17 additions & 136 deletions src/renderer/components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1325,142 +1325,8 @@ export function SettingsPanel({ onClose, initialTab }: SettingsPanelProps) {
)}
</div>

{/* AI Models */}
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-600 mb-6">
<div className="mb-3">
<h3 className="font-semibold text-gray-900 dark:text-gray-100">AI Models</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Choose which Claude model to use for each feature. Haiku is fastest and
cheapest, Opus is most capable.
</p>
</div>
<div className="space-y-3">
{[
{
key: "analysis" as const,
label: "Email Analysis",
description: "Triaging which emails need replies",
},
{
key: "drafts" as const,
label: "Draft Generation",
description: "Writing reply drafts",
},
{
key: "refinement" as const,
label: "Draft Refinement",
description: "Improving drafts based on feedback",
},
{
key: "calendaring" as const,
label: "Scheduling Detection",
description: "Identifying calendar-related emails",
},
{
key: "archiveReady" as const,
label: "Archive-Ready Analysis",
description: "Detecting completed conversations",
},
{
key: "senderLookup" as const,
label: "Sender Lookup",
description: "Web search for sender info",
anthropicOnly: true,
},
{
key: "agentDrafter" as const,
label: "Agent Drafter",
description: "Background auto-draft generation",
},
{
key: "agentChat" as const,
label: "Agent Chat",
description: "Interactive agent sidebar conversations",
},
].map(({ key, label, description, ...rest }) => {
// Mirror the Agents tab picker shape: full Anthropic
// model list + OpenRouter free models, auto-save on
// change. anthropicOnly features (Sender Lookup uses
// Anthropic web_search; no OpenRouter equivalent) keep
// the Anthropic-only chip and hide the OpenRouter
// optgroup. The sidecar's resolveModelFor() accepts
// both legacy tier names ("haiku") and concrete model
// ids ("claude-haiku-4-5-…", "deepseek/…:free"), so
// either value space round-trips correctly.
const anthropicOnly =
"anthropicOnly" in rest ? rest.anthropicOnly : false;
const isNonClaude =
anthropicOnly && !modelConfig[key].startsWith("claude-");
return (
<div
key={key}
className="flex items-center justify-between py-2 border-b border-gray-100 dark:border-gray-700 last:border-0"
>
<div className="flex-1 min-w-0 mr-4">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">
{label}
{anthropicOnly && (
<span
className="ml-2 text-xs font-normal text-amber-700 dark:text-amber-400"
title="Uses Anthropic's web_search tool, which has no OpenRouter equivalent"
>
Anthropic-only
</span>
)}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{description}
</p>
{isNonClaude && (
<p className="text-xs text-amber-700 dark:text-amber-400 mt-0.5">
Configured model is not a Claude model — this feature will fail
until a Claude model is selected.
</p>
)}
</div>
<select
value={modelConfig[key]}
onChange={async (e) => {
const next = e.target.value;
const updated = { ...modelConfig, [key]: next };
setModelConfig(updated);
await window.api.settings.set({ modelConfig: updated });
queryClient.invalidateQueries({
queryKey: ["general-config"],
});
}}
className="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-500 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent max-w-[300px]"
>
<optgroup label="Anthropic">
{ANTHROPIC_MODEL_OPTIONS.map((opt) => (
<option key={opt.id} value={opt.id}>
{opt.label}
</option>
))}
</optgroup>
{!anthropicOnly && freeModels.length > 0 && (
<optgroup label="OpenRouter (free)">
{freeModels.map((m) => (
<option key={m.id} value={m.id}>
{m.name}
</option>
))}
</optgroup>
)}
</select>
</div>
);
})}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-3">
Picking a non-Anthropic model routes that feature through OpenRouter — make
sure an OpenRouter API key is configured in Agent Tools → AI Models, otherwise
the call will surface a clear &ldquo;OpenRouter API key required&rdquo; error.
Sender Lookup is the exception: it uses Anthropic&rsquo;s web_search tool,
which has no OpenRouter equivalent, so the picker only offers Claude models
for that feature.
</p>
</div>
{/* AI Models lives in Agent Tools → AI Models now — single
source of truth, directly under the Anthropic auth section. */}

{/* Updates */}
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-600 mb-6">
Expand Down Expand Up @@ -2292,6 +2158,11 @@ export function SettingsPanel({ onClose, initialTab }: SettingsPanelProps) {
description:
"Iterating on a draft from your feedback (falls back to Draft Generation)",
},
{
key: "calendaring" as const,
label: "Scheduling Detection",
description: "Identifying calendar-related emails",
},
{
key: "summary" as const,
label: "Thread Summary",
Expand All @@ -2308,6 +2179,16 @@ export function SettingsPanel({ onClose, initialTab }: SettingsPanelProps) {
description: "Web-search-backed sender profiles",
anthropicOnly: true,
},
{
key: "agentDrafter" as const,
label: "Agent Drafter",
description: "Background auto-draft generation",
},
{
key: "agentChat" as const,
label: "Agent Chat",
description: "Interactive agent sidebar conversations",
},
] as const
).map(({ key, label, description, ...rest }) => {
const anthropicOnly = "anthropicOnly" in rest ? rest.anthropicOnly : false;
Expand Down
Loading