Skip to content

Commit e2b4a97

Browse files
OpenSource03claude
andcommitted
feat: effort levels, context usage persistence, and UI polish
- Add Claude effort level support (low/medium/high/max) with model-aware picker in dropdown - Persist context usage across all engines and background sessions - Queue permission requests to prevent double-response races - Add auto-expand tools toggle, Skill tool renderer, and background agent dismiss UX - Overhaul welcome screen with time-aware greetings and mood-colored ambient backgrounds - Polish flat mode headers, light glass sidebar transparency, and traffic light positioning Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 392af90 commit e2b4a97

47 files changed

Lines changed: 1324 additions & 306 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

electron/src/ipc/claude-sessions.ts

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ function summarizeSpawnOptions(options: Record<string, unknown>): Record<string,
6666
model: options.model,
6767
includePartialMessages: options.includePartialMessages,
6868
thinking: options.thinking,
69+
effort: options.effort,
6970
settingSources: options.settingSources,
7071
enableFileCheckpointing: options.enableFileCheckpointing,
7172
extraArgs: options.extraArgs,
@@ -236,6 +237,7 @@ interface StartOptions {
236237
model?: string;
237238
permissionMode?: string;
238239
thinkingEnabled?: boolean;
240+
effort?: "low" | "medium" | "high" | "max";
239241
resume?: string;
240242
/** Fork to a new session ID when resuming (model forgets messages after resumeSessionAt) */
241243
forkSession?: boolean;
@@ -244,10 +246,8 @@ interface StartOptions {
244246
mcpServers?: McpServerInput[];
245247
}
246248

247-
function buildThinkingConfig(thinkingEnabled?: boolean): { type: "adaptive" } | { type: "disabled" } {
248-
return thinkingEnabled === false
249-
? { type: "disabled" }
250-
: { type: "adaptive" };
249+
function buildThinkingConfig(): { type: "adaptive" } {
250+
return { type: "adaptive" };
251251
}
252252

253253
function logSdkCliPath(context: string, cliPath?: string): void {
@@ -275,7 +275,7 @@ async function revalidateClaudeModelsCache(cwd?: string): Promise<{ models: Arra
275275
const queryOptions: Record<string, unknown> = {
276276
cwd: cwd?.trim() || os.homedir(),
277277
includePartialMessages: true,
278-
thinking: buildThinkingConfig(true),
278+
thinking: buildThinkingConfig(),
279279
settingSources: ["user", "project"],
280280
pathToClaudeCodeExecutable: cliPath,
281281
...fileCheckpointOptions(),
@@ -342,6 +342,8 @@ async function restartSession(
342342
getMainWindow: () => BrowserWindow | null,
343343
mcpServersOverride?: McpServerInput[],
344344
cwdOverride?: string,
345+
effortOverride?: StartOptions["effort"],
346+
modelOverride?: string,
345347
): Promise<{ ok?: boolean; error?: string; restarted?: boolean }> {
346348
const session = sessions.get(sessionId);
347349
if (!session?.queryHandle || !session.startOptions) {
@@ -375,7 +377,13 @@ async function restartSession(
375377
queryHandle: null,
376378
eventCounter: session.eventCounter,
377379
pendingPermissions: new Map(),
378-
startOptions: { ...opts, cwd, mcpServers },
380+
startOptions: {
381+
...opts,
382+
cwd,
383+
mcpServers,
384+
...(effortOverride ? { effort: effortOverride } : {}),
385+
...(modelOverride ? { model: modelOverride } : {}),
386+
},
379387
};
380388

381389
const canUseTool = (toolName: string, input: unknown, context: { toolUseID: string; suggestions: unknown; decisionReason: string }) => {
@@ -397,7 +405,7 @@ async function restartSession(
397405
const queryOptions: Record<string, unknown> = {
398406
cwd,
399407
includePartialMessages: true,
400-
thinking: buildThinkingConfig(opts.thinkingEnabled),
408+
thinking: buildThinkingConfig(),
401409
canUseTool,
402410
settingSources: ["user", "project"],
403411
pathToClaudeCodeExecutable: cliPath,
@@ -416,7 +424,10 @@ async function restartSession(
416424
queryOptions.allowDangerouslySkipPermissions = true;
417425
}
418426
}
419-
if (opts.model) queryOptions.model = opts.model;
427+
if (modelOverride ?? opts.model) queryOptions.model = modelOverride ?? opts.model;
428+
if (effortOverride ?? opts.effort) {
429+
queryOptions.effort = effortOverride ?? opts.effort;
430+
}
420431

421432
if (mcpServers?.length) {
422433
queryOptions.mcpServers = await buildSdkMcpConfig(mcpServers);
@@ -494,7 +505,7 @@ export function register(getMainWindow: () => BrowserWindow | null): void {
494505
const queryOptions: Record<string, unknown> = {
495506
cwd: options.cwd || process.cwd(),
496507
includePartialMessages: true,
497-
thinking: buildThinkingConfig(options.thinkingEnabled),
508+
thinking: buildThinkingConfig(),
498509
canUseTool,
499510
settingSources: ["user", "project"],
500511
pathToClaudeCodeExecutable: cliPath,
@@ -527,6 +538,9 @@ export function register(getMainWindow: () => BrowserWindow | null): void {
527538
if (options.model) {
528539
queryOptions.model = options.model;
529540
}
541+
if (options.effort) {
542+
queryOptions.effort = options.effort;
543+
}
530544

531545
if (options.mcpServers?.length) {
532546
queryOptions.mcpServers = await buildSdkMcpConfig(options.mcpServers);
@@ -666,14 +680,14 @@ export function register(getMainWindow: () => BrowserWindow | null): void {
666680
return { error: "Reasoning toggle is not supported by this Claude SDK version" };
667681
}
668682
try {
669-
await session.queryHandle.setMaxThinkingTokens(thinkingEnabled ? null : 0);
683+
await session.queryHandle.setMaxThinkingTokens(null);
670684
if (session.startOptions) {
671-
session.startOptions.thinkingEnabled = thinkingEnabled;
685+
session.startOptions.thinkingEnabled = true;
672686
}
673-
log("SET_THINKING", `session=${sessionId.slice(0, 8)} thinkingEnabled=${thinkingEnabled}`);
687+
log("SET_THINKING", `session=${sessionId.slice(0, 8)} requested=${thinkingEnabled} applied=true`);
674688
return { ok: true };
675689
} catch (err) {
676-
log("SET_THINKING_ERR", `session=${sessionId.slice(0, 8)} thinkingEnabled=${thinkingEnabled} ${extractErrorMessage(err)}`);
690+
log("SET_THINKING_ERR", `session=${sessionId.slice(0, 8)} requested=${thinkingEnabled} ${extractErrorMessage(err)}`);
677691
return { error: extractErrorMessage(err) };
678692
}
679693
});
@@ -831,7 +845,19 @@ export function register(getMainWindow: () => BrowserWindow | null): void {
831845
});
832846

833847
// Restart the session with a new MCP server list (e.g., after add/remove)
834-
ipcMain.handle("claude:restart-session", async (_event, { sessionId, mcpServers, cwd }: { sessionId: string; mcpServers?: McpServerInput[]; cwd?: string }) => {
835-
return restartSession(sessionId, getMainWindow, mcpServers, cwd);
848+
ipcMain.handle("claude:restart-session", async (_event, {
849+
sessionId,
850+
mcpServers,
851+
cwd,
852+
effort,
853+
model,
854+
}: {
855+
sessionId: string;
856+
mcpServers?: McpServerInput[];
857+
cwd?: string;
858+
effort?: StartOptions["effort"];
859+
model?: string;
860+
}) => {
861+
return restartSession(sessionId, getMainWindow, mcpServers, cwd, effort, model);
836862
});
837863
}

electron/src/lib/claude-model-cache.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ export interface CachedModelInfo {
66
value: string;
77
displayName: string;
88
description: string;
9+
supportsEffort?: boolean;
10+
supportedEffortLevels?: Array<"low" | "medium" | "high" | "max">;
11+
supportsAdaptiveThinking?: boolean;
12+
supportsFastMode?: boolean;
913
}
1014

1115
interface ClaudeModelsCacheData {
@@ -25,10 +29,22 @@ function normalizeModelInfo(value: unknown): CachedModelInfo | null {
2529
if (typeof model.value !== "string" || model.value.trim().length === 0) return null;
2630
const displayName = typeof model.displayName === "string" ? model.displayName : model.value;
2731
const description = typeof model.description === "string" ? model.description : "";
32+
const supportedEffortLevels = Array.isArray(model.supportedEffortLevels)
33+
? model.supportedEffortLevels.filter(
34+
(level): level is "low" | "medium" | "high" | "max" =>
35+
level === "low" || level === "medium" || level === "high" || level === "max",
36+
)
37+
: undefined;
2838
return {
2939
value: model.value,
3040
displayName,
3141
description,
42+
...(typeof model.supportsEffort === "boolean" ? { supportsEffort: model.supportsEffort } : {}),
43+
...(supportedEffortLevels && supportedEffortLevels.length > 0 ? { supportedEffortLevels } : {}),
44+
...(typeof model.supportsAdaptiveThinking === "boolean"
45+
? { supportsAdaptiveThinking: model.supportsAdaptiveThinking }
46+
: {}),
47+
...(typeof model.supportsFastMode === "boolean" ? { supportsFastMode: model.supportsFastMode } : {}),
3248
};
3349
}
3450

electron/src/main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ function createWindow(): void {
9292
// macOS Tahoe+ with liquid glass
9393
windowOptions.titleBarStyle = "hidden";
9494
windowOptions.transparent = true;
95-
windowOptions.trafficLightPosition = { x: 16, y: 16 };
95+
windowOptions.trafficLightPosition = { x: 19, y: 19 };
9696
} else if (process.platform === "win32") {
9797
// Windows: native Electron backgroundMaterial handles DWM mica/acrylic.
9898
// WebContents is automatically transparent (no transparent: true needed),
@@ -102,7 +102,7 @@ function createWindow(): void {
102102
} else {
103103
// macOS without glass / Linux
104104
windowOptions.titleBarStyle = "hiddenInset";
105-
windowOptions.trafficLightPosition = { x: 16, y: 16 };
105+
windowOptions.trafficLightPosition = { x: 19, y: 19 };
106106
windowOptions.backgroundColor = "#18181b";
107107
}
108108

electron/src/preload.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ contextBridge.exposeInMainWorld("claude", {
6363
ipcRenderer.invoke("claude:mcp-reconnect", { sessionId, serverName }),
6464
revertFiles: (sessionId: string, checkpointId: string) =>
6565
ipcRenderer.invoke("claude:revert-files", { sessionId, checkpointId }),
66-
restartSession: (sessionId: string, mcpServers?: unknown[], cwd?: string) =>
67-
ipcRenderer.invoke("claude:restart-session", { sessionId, mcpServers, cwd }),
66+
restartSession: (sessionId: string, mcpServers?: unknown[], cwd?: string, effort?: string, model?: string) =>
67+
ipcRenderer.invoke("claude:restart-session", { sessionId, mcpServers, cwd, effort, model }),
6868
readFile: (filePath: string) => ipcRenderer.invoke("file:read", filePath),
6969
openInEditor: (filePath: string, line?: number, editor?: string) => ipcRenderer.invoke("file:open-in-editor", { filePath, line, editor }),
7070
openExternal: (url: string) => ipcRenderer.invoke("shell:open-external", url),

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "harnss",
3-
"version": "0.17.2",
3+
"version": "0.17.3",
44
"productName": "Harnss",
55
"description": "Harness your AI coding agents — one desktop app for Claude Code, Codex, and any ACP agent",
66
"author": {

shared/types/engine.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface SessionMeta {
2525
isConnected: boolean;
2626
sessionInfo: SessionInfo | null;
2727
totalCost: number;
28+
contextUsage: ContextUsage | null;
2829
isCompacting?: boolean;
2930
}
3031

src/components/AppLayout.tsx

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export function AppLayout() {
6565
spaceTerminals, activeSpaceTerminals,
6666
handleToggleTool, handleToolReorder, handleNewChat, handleSend,
6767
handleModelChange, handlePermissionModeChange, handlePlanModeChange,
68-
handleThinkingChange, handleAgentWorktreeChange, handleStop, handleSelectSession,
68+
handleClaudeModelEffortChange, handleAgentWorktreeChange, handleStop, handleSelectSession,
6969
handleSendQueuedNow,
7070
handleCreateProject, handleImportCCSession, handleNavigateToMessage,
7171
handleViewTurnChanges, handleCreateSpace, handleEditSpace,
@@ -300,7 +300,9 @@ Link: ${issue.url}`;
300300
const titlebarOpacity = Math.round(30 + 50 * spaceOpacity); // 30–80%
301301
const titlebarSurfaceColor =
302302
`linear-gradient(to bottom, color-mix(in oklab, var(--background) ${titlebarOpacity}%, transparent) 0%, transparent 100%)`;
303-
const topFadeBackground = `linear-gradient(to bottom, ${chatSurfaceColor}, transparent)`;
303+
const topFadeBackground = isIsland
304+
? `linear-gradient(to bottom, ${chatSurfaceColor}, transparent)`
305+
: `linear-gradient(to bottom, ${chatSurfaceColor} 0%, ${chatSurfaceColor} 22%, transparent 100%)`;
304306
const bottomFadeBackground = `linear-gradient(to top, ${chatSurfaceColor}, transparent)`;
305307

306308
const { activeTools } = settings;
@@ -367,10 +369,13 @@ Link: ${issue.url}`;
367369
onIslandLayoutChange={settings.setIslandLayout}
368370
autoGroupTools={settings.autoGroupTools}
369371
onAutoGroupToolsChange={settings.setAutoGroupTools}
372+
autoExpandTools={settings.autoExpandTools}
373+
onAutoExpandToolsChange={settings.setAutoExpandTools}
370374
transparency={settings.transparency}
371375
onTransparencyChange={settings.setTransparency}
372376
glassSupported={glassSupported}
373377
sidebarOpen={sidebar.isOpen}
378+
onToggleSidebar={sidebar.toggle}
374379
onReplayWelcome={handleReplayWelcome}
375380
/>
376381
)}
@@ -397,7 +402,9 @@ Link: ${issue.url}`;
397402
{/* Top fade: only visible when chat is scrolled down. Island mode uses dark shadow; flat mode fades content into bg */}
398403
{/* Island: gradient starts at top-0 (behind header, subtle bleed). Flat: starts at top-10 (right below header) so full gradient is visible and strong. */}
399404
<div
400-
className="pointer-events-none absolute inset-x-0 top-0 z-[5] h-16"
405+
className={`pointer-events-none absolute inset-x-0 top-0 z-[5] ${
406+
isIsland ? "h-16" : "h-20"
407+
}`}
401408
style={{
402409
opacity: "calc(var(--chat-fade-strength, 1) * var(--chat-top-progress, 0))",
403410
background: topFadeBackground,
@@ -408,6 +415,7 @@ Link: ${issue.url}`;
408415
style={{ background: titlebarSurfaceColor }}
409416
>
410417
<ChatHeader
418+
islandLayout={isIsland}
411419
sidebarOpen={sidebar.isOpen}
412420
isProcessing={manager.isProcessing}
413421
model={manager.sessionInfo?.model}
@@ -436,6 +444,7 @@ Link: ${issue.url}`;
436444
isProcessing={manager.isProcessing}
437445
showThinking={showThinking}
438446
autoGroupTools={settings.autoGroupTools}
447+
autoExpandTools={settings.autoExpandTools}
439448
extraBottomPadding={!!manager.pendingPermission}
440449
scrollToMessageId={scrollToMessageId}
441450
onScrolledToMessage={() => setScrollToMessageId(undefined)}
@@ -470,11 +479,11 @@ Link: ${issue.url}`;
470479
isProcessing={manager.isProcessing}
471480
queuedCount={manager.queuedCount}
472481
model={settings.model}
473-
thinking={settings.thinking}
482+
claudeEffort={settings.claudeEffort}
474483
planMode={settings.planMode}
475484
permissionMode={settings.permissionMode}
476485
onModelChange={handleModelChange}
477-
onThinkingChange={handleThinkingChange}
486+
onClaudeModelEffortChange={handleClaudeModelEffortChange}
478487
onPlanModeChange={handlePlanModeChange}
479488
onPermissionModeChange={handlePermissionModeChange}
480489
projectPath={activeProjectPath}
@@ -506,16 +515,20 @@ Link: ${issue.url}`;
506515
) : (
507516
<>
508517
<div
509-
className={`chat-titlebar-bg drag-region flex h-12 items-center px-3 ${
510-
!sidebar.isOpen && isMac ? "ps-[78px]" : ""
518+
className={`chat-titlebar-bg drag-region flex items-center ${
519+
isIsland ? "h-12 px-3" : "h-[3.25rem] px-4"
520+
} ${
521+
!sidebar.isOpen && isMac ? (isIsland ? "ps-[78px]" : "ps-[84px]") : ""
511522
}`}
512523
style={{ background: titlebarSurfaceColor }}
513524
>
514525
{!sidebar.isOpen && (
515526
<Button
516527
variant="ghost"
517528
size="icon"
518-
className="no-drag h-7 w-7 text-muted-foreground/60 hover:text-foreground"
529+
className={`no-drag h-7 w-7 text-muted-foreground/60 hover:text-foreground ${
530+
isIsland ? "mt-0.5" : ""
531+
}`}
519532
onClick={sidebar.toggle}
520533
>
521534
<PanelLeft className="h-4 w-4" />
@@ -768,6 +781,10 @@ Link: ${issue.url}`;
768781
onThemeChange={settings.setTheme}
769782
islandLayout={settings.islandLayout}
770783
onIslandLayoutChange={settings.setIslandLayout}
784+
autoGroupTools={settings.autoGroupTools}
785+
onAutoGroupToolsChange={settings.setAutoGroupTools}
786+
autoExpandTools={settings.autoExpandTools}
787+
onAutoExpandToolsChange={settings.setAutoExpandTools}
771788
transparency={settings.transparency}
772789
onTransparencyChange={settings.setTransparency}
773790
glassSupported={glassSupported}

0 commit comments

Comments
 (0)