Skip to content

Commit 0a2b013

Browse files
OpenSource03claude
andcommitted
feat: compaction state tracking, auto-group tools toggle, and MCP renderer improvements
- Track isCompacting state through session lifecycle, background store, and hooks; handle compact_boundary in background handler - Add auto-group tools toggle in Appearance settings to enable/disable tool call grouping - Absorb thinking-only assistant messages into tool groups and render ThinkingBlock inside collapsed groups - Refactor all MCP renderers to typed wrapper pattern, eliminating unsafe as Record<string, unknown> casts - Add Confluence renderers: page descendants, created/updated pages with HTML content preview, page list - Fall back to result.stdout for MCP data extraction when tool_use_result is empty Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 93837c5 commit 0a2b013

26 files changed

Lines changed: 702 additions & 82 deletions

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.16.2",
3+
"version": "0.16.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+
isCompacting?: boolean;
2829
}
2930

3031
/** All supported engine identifiers. */

src/components/AppLayout.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ export function AppLayout() {
199199
onThemeChange={settings.setTheme}
200200
islandLayout={settings.islandLayout}
201201
onIslandLayoutChange={settings.setIslandLayout}
202+
autoGroupTools={settings.autoGroupTools}
203+
onAutoGroupToolsChange={settings.setAutoGroupTools}
202204
transparency={settings.transparency}
203205
onTransparencyChange={settings.setTransparency}
204206
glassSupported={glassSupported}
@@ -256,6 +258,7 @@ export function AppLayout() {
256258
messages={manager.messages}
257259
isProcessing={manager.isProcessing}
258260
showThinking={showThinking}
261+
autoGroupTools={settings.autoGroupTools}
259262
extraBottomPadding={!!manager.pendingPermission}
260263
scrollToMessageId={scrollToMessageId}
261264
onScrolledToMessage={() => setScrollToMessageId(undefined)}

src/components/ChatView.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { ToolGroupBlock } from "./ToolGroupBlock";
99
import { TurnChangesSummary } from "./TurnChangesSummary";
1010
import { extractTurnSummaries } from "@/lib/turn-changes";
1111
import type { TurnSummary } from "@/lib/turn-changes";
12-
import { computeToolGroups } from "@/lib/tool-groups";
12+
import { computeToolGroups, type ToolGroupInfo } from "@/lib/tool-groups";
1313
import { TextShimmer } from "@/components/ui/text-shimmer";
1414
import {
1515
BOTTOM_LOCK_THRESHOLD_PX,
@@ -22,11 +22,16 @@ const LARGE_CHAT_THRESHOLD = 300;
2222
const INITIAL_RENDER_TAIL_COUNT = 180;
2323
const PREPEND_CHUNK_SIZE = 200;
2424
const PREPEND_TRIGGER_PX = 160;
25+
const EMPTY_TOOL_GROUP_INFO: ToolGroupInfo = {
26+
groups: new Map(),
27+
groupedIndices: new Set(),
28+
};
2529

2630
interface ChatViewProps {
2731
messages: UIMessage[];
2832
isProcessing: boolean;
2933
showThinking: boolean;
34+
autoGroupTools: boolean;
3035
extraBottomPadding?: boolean;
3136
scrollToMessageId?: string;
3237
onScrolledToMessage?: () => void;
@@ -48,7 +53,7 @@ interface ChatViewProps {
4853
sendNextId?: string | null;
4954
}
5055

51-
export const ChatView = memo(function ChatView({ messages, isProcessing, showThinking, extraBottomPadding, scrollToMessageId, onScrolledToMessage, sessionId, onRevert, onFullRevert, onViewTurnChanges, onScrolledFromTop, onTopScrollProgress, onSendQueuedNow, sendNextId }: ChatViewProps) {
56+
export const ChatView = memo(function ChatView({ messages, isProcessing, showThinking, autoGroupTools, extraBottomPadding, scrollToMessageId, onScrolledToMessage, sessionId, onRevert, onFullRevert, onViewTurnChanges, onScrolledFromTop, onTopScrollProgress, onSendQueuedNow, sendNextId }: ChatViewProps) {
5257
const scrollAreaRef = useRef<HTMLDivElement>(null);
5358
const contentRef = useRef<HTMLDivElement>(null);
5459
const bottomLockedRef = useRef(true);
@@ -398,11 +403,13 @@ export const ChatView = memo(function ChatView({ messages, isProcessing, showThi
398403
return map;
399404
}, [nonQueuedMessages, isProcessing]);
400405

401-
// Pre-compute tool groups: contiguous tool_call sequences between assistant text messages.
402-
// Finalized groups (with 2+ tools) render as a single ToolGroupBlock instead of individual ToolCalls.
406+
// Pre-compute tool groups when enabled: contiguous tool_call sequences between
407+
// assistant text messages, also absorbing any in-between thinking-only rows.
403408
const { groups: toolGroups, groupedIndices } = useMemo(
404-
() => computeToolGroups(nonQueuedMessages, isProcessing),
405-
[nonQueuedMessages, isProcessing],
409+
() => autoGroupTools
410+
? computeToolGroups(nonQueuedMessages, isProcessing)
411+
: EMPTY_TOOL_GROUP_INFO,
412+
[autoGroupTools, nonQueuedMessages, isProcessing],
406413
);
407414

408415
// Finalized group keys (first tool message ID), used to detect newly formed groups.
@@ -520,7 +527,12 @@ export const ChatView = memo(function ChatView({ messages, isProcessing, showThi
520527

521528
return (
522529
<Fragment key={`group-${groupKey}`}>
523-
<ToolGroupBlock tools={group.tools} animate={isNewGroup} />
530+
<ToolGroupBlock
531+
tools={group.tools}
532+
messages={group.messages}
533+
showThinking={showThinking}
534+
animate={isNewGroup}
535+
/>
524536
{groupTurnSummary && (
525537
<TurnChangesSummary summary={groupTurnSummary} onViewInPanel={onViewTurnChanges} />
526538
)}
@@ -541,6 +553,7 @@ export const ChatView = memo(function ChatView({ messages, isProcessing, showThi
541553
</Fragment>
542554
);
543555
}
556+
if (groupedIndices.has(index)) return null;
544557
if (msg.role === "tool_result") return null;
545558
if (msg.role === "summary") {
546559
return (

src/components/McpToolContent.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { ToolUseResult } from "@/types/protocol";
44

55
// ── MCP renderers (extracted) ──
66
import { JiraIssueList, JiraIssueDetail, JiraProjectList, JiraTransitions } from "./mcp-renderers/jira";
7-
import { ConfluenceSearchResults, ConfluenceSpaces } from "./mcp-renderers/confluence";
7+
import { ConfluenceSearchResults, ConfluenceSpaces, ConfluencePageDescendants, ConfluenceCreatedPage, ConfluenceUpdatedPage, ConfluencePageList } from "./mcp-renderers/confluence";
88
import { RovoSearchResults, RovoFetchResult, AtlassianResourcesList } from "./mcp-renderers/atlassian";
99
import { Context7LibraryList, Context7DocsResult } from "./mcp-renderers/context7";
1010

@@ -50,6 +50,15 @@ function extractMcpData(result: ToolUseResult): unknown {
5050
}
5151
}
5252

53+
// Fallback: normalizeToolResult puts MCP text into stdout when tool_use_result is empty
54+
if (typeof result.stdout === "string" && result.stdout) {
55+
try {
56+
return JSON.parse(result.stdout);
57+
} catch {
58+
return null;
59+
}
60+
}
61+
5362
return null;
5463
}
5564

@@ -67,6 +76,8 @@ function extractMcpText(result: ToolUseResult): string | null {
6776
const items = result as Array<{ type?: string; text?: string }>;
6877
return items.filter((c) => c.type === "text" && c.text).map((c) => c.text).join("") || null;
6978
}
79+
// Fallback: normalizeToolResult puts MCP text into stdout when tool_use_result is empty
80+
if (typeof result.stdout === "string" && result.stdout) return result.stdout;
7081
return null;
7182
}
7283

@@ -83,6 +94,10 @@ const MCP_RENDERERS: Record<string, McpRenderer> = {
8394
// Confluence
8495
"mcp__Atlassian__searchConfluenceUsingCql": ConfluenceSearchResults,
8596
"mcp__Atlassian__getConfluenceSpaces": ConfluenceSpaces,
97+
"mcp__Atlassian__getConfluencePageDescendants": ConfluencePageDescendants,
98+
"mcp__Atlassian__createConfluencePage": ConfluenceCreatedPage,
99+
"mcp__Atlassian__updateConfluencePage": ConfluenceUpdatedPage,
100+
"mcp__Atlassian__getPagesInConfluenceSpace": ConfluencePageList,
86101
// Rovo Search
87102
"mcp__Atlassian__search": RovoSearchResults,
88103
"mcp__Atlassian__fetch": RovoFetchResult,
@@ -103,6 +118,10 @@ const MCP_PATTERN_RENDERERS: Array<{ pattern: RegExp; renderer: McpRenderer }> =
103118
{ pattern: /Atlassian[/_]+getTransitionsForJiraIssue$/, renderer: JiraTransitions },
104119
{ pattern: /Atlassian[/_]+searchConfluenceUsingCql$/, renderer: ConfluenceSearchResults },
105120
{ pattern: /Atlassian[/_]+getConfluenceSpaces$/, renderer: ConfluenceSpaces },
121+
{ pattern: /Atlassian[/_]+getConfluencePageDescendants$/, renderer: ConfluencePageDescendants },
122+
{ pattern: /Atlassian[/_]+createConfluencePage$/, renderer: ConfluenceCreatedPage },
123+
{ pattern: /Atlassian[/_]+updateConfluencePage$/, renderer: ConfluenceUpdatedPage },
124+
{ pattern: /Atlassian[/_]+getPagesInConfluenceSpace$/, renderer: ConfluencePageList },
106125
{ pattern: /Atlassian[/_]+search$/, renderer: RovoSearchResults },
107126
{ pattern: /Atlassian[/_]+fetch$/, renderer: RovoFetchResult },
108127
{ pattern: /Atlassian[/_]+getAccessibleAtlassianResources$/, renderer: AtlassianResourcesList },
@@ -140,6 +159,20 @@ export function getMcpCompactSummary(toolName: string, toolInput: Record<string,
140159
if (/searchConfluenceUsingCql/.test(toolName)) {
141160
return String(toolInput.cql ?? "").slice(0, 80);
142161
}
162+
if (/getConfluencePageDescendants/.test(toolName)) {
163+
return `page ${toolInput.pageId ?? ""}`;
164+
}
165+
if (/createConfluencePage/.test(toolName)) {
166+
return String(toolInput.title ?? "").slice(0, 80);
167+
}
168+
if (/updateConfluencePage/.test(toolName)) {
169+
return toolInput.versionMessage
170+
? String(toolInput.versionMessage).slice(0, 80)
171+
: `page ${toolInput.pageId ?? ""}`;
172+
}
173+
if (/getPagesInConfluenceSpace/.test(toolName)) {
174+
return toolInput.title ? `"${toolInput.title}"` : `space ${toolInput.spaceId ?? ""}`;
175+
}
143176
if (/Atlassian[/_]+search$/.test(toolName)) {
144177
return String(toolInput.query ?? "").slice(0, 80);
145178
}
@@ -174,7 +207,7 @@ export const McpToolContent = memo(function McpToolContent({ message }: { messag
174207

175208
return (
176209
<div className="text-xs">
177-
{renderer({ data, toolInput: message.toolInput ?? {}, rawText })}
210+
{renderer({ data: data ?? {}, toolInput: message.toolInput ?? {}, rawText })}
178211
</div>
179212
);
180213
});

src/components/SettingsView.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ interface SettingsViewProps {
6060
onThemeChange: (t: ThemeOption) => void;
6161
islandLayout: boolean;
6262
onIslandLayoutChange: (enabled: boolean) => void;
63+
autoGroupTools: boolean;
64+
onAutoGroupToolsChange: (enabled: boolean) => void;
6365
transparency: boolean;
6466
onTransparencyChange: (enabled: boolean) => void;
6567
glassSupported: boolean;
@@ -77,6 +79,8 @@ export const SettingsView = memo(function SettingsView({
7779
onThemeChange,
7880
islandLayout,
7981
onIslandLayoutChange,
82+
autoGroupTools,
83+
onAutoGroupToolsChange,
8084
transparency,
8185
onTransparencyChange,
8286
glassSupported,
@@ -124,6 +128,8 @@ export const SettingsView = memo(function SettingsView({
124128
onThemeChange={onThemeChange}
125129
islandLayout={islandLayout}
126130
onIslandLayoutChange={onIslandLayoutChange}
131+
autoGroupTools={autoGroupTools}
132+
onAutoGroupToolsChange={onAutoGroupToolsChange}
127133
transparency={transparency}
128134
onTransparencyChange={onTransparencyChange}
129135
glassSupported={glassSupported}
@@ -185,7 +191,7 @@ export const SettingsView = memo(function SettingsView({
185191
default:
186192
return null;
187193
}
188-
}, [activeSection, appSettings, updateAppSettings, agents, onSaveAgent, onDeleteAgent, theme, onThemeChange, islandLayout, onIslandLayoutChange, transparency, onTransparencyChange, glassSupported]);
194+
}, [activeSection, appSettings, updateAppSettings, agents, onSaveAgent, onDeleteAgent, theme, onThemeChange, islandLayout, onIslandLayoutChange, autoGroupTools, onAutoGroupToolsChange, transparency, onTransparencyChange, glassSupported]);
189195

190196
return (
191197
<div className="island flex flex-1 flex-col overflow-hidden rounded-lg bg-background">

src/components/ToolGroupBlock.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ import {
77
} from "@/components/ui/collapsible";
88
import type { UIMessage } from "@/types";
99
import { ToolCall } from "./ToolCall";
10+
import { ThinkingBlock } from "./ThinkingBlock";
1011
import { getToolLabel, getToolIcon } from "@/components/lib/tool-metadata";
1112
import { formatCompactSummary } from "@/components/lib/tool-formatting";
1213

1314
interface ToolGroupBlockProps {
1415
tools: UIMessage[];
16+
messages: UIMessage[];
17+
showThinking?: boolean;
1518
/** When true (live streaming), runs a one-time tools -> group morph animation.
1619
* When false (restored session), renders collapsed immediately. */
1720
animate: boolean;
@@ -52,6 +55,8 @@ function ToolGroupHeaderContent({
5255

5356
export const ToolGroupBlock = memo(function ToolGroupBlock({
5457
tools,
58+
messages,
59+
showThinking = true,
5560
animate,
5661
}: ToolGroupBlockProps) {
5762
// Lock animation decision at mount. Parent re-renders may flip `animate` to false
@@ -153,6 +158,13 @@ export const ToolGroupBlock = memo(function ToolGroupBlock({
153158
});
154159
}, [tools]);
155160

161+
const groupedRows = useMemo(() => {
162+
return messages.filter((message) => {
163+
if (message.role === "tool_call") return true;
164+
return showThinking && message.role === "assistant" && !!message.thinking && !message.content;
165+
});
166+
}, [messages, showThinking]);
167+
156168
if (isMorphing) {
157169
const shellStyle: CSSProperties = {
158170
"--tool-group-morph-duration": `${morphDurationMs}ms`,
@@ -217,8 +229,18 @@ export const ToolGroupBlock = memo(function ToolGroupBlock({
217229
Uses tool-group-collapse for a slower, smoother animation than default. */}
218230
<CollapsibleContent className="tool-group-collapse">
219231
<div className="mt-0.5">
220-
{tools.map((tool) => (
221-
<ToolCall key={tool.id} message={tool} compact />
232+
{groupedRows.map((message) => (
233+
message.role === "tool_call" ? (
234+
<ToolCall key={message.id} message={message} compact />
235+
) : (
236+
<div key={message.id} className="py-1">
237+
<ThinkingBlock
238+
thinking={message.thinking ?? ""}
239+
isStreaming={message.isStreaming}
240+
thinkingComplete={message.thinkingComplete}
241+
/>
242+
</div>
243+
)
222244
))}
223245
</div>
224246
</CollapsibleContent>
@@ -228,5 +250,7 @@ export const ToolGroupBlock = memo(function ToolGroupBlock({
228250
);
229251
}, (prev, next) =>
230252
prev.tools === next.tools &&
253+
prev.messages === next.messages &&
254+
prev.showThinking === next.showThinking &&
231255
prev.animate === next.animate,
232256
);

src/components/lib/tool-metadata.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ export const MCP_TOOL_LABELS: Array<{ pattern: RegExp; labels: ToolLabels }> = [
7474
{ pattern: /getConfluencePage$/, labels: { past: "Fetched page", active: "Fetching page", failure: "fetch page" } },
7575
{ pattern: /searchConfluenceUsingCql$/, labels: { past: "Searched Confluence", active: "Searching Confluence", failure: "search Confluence" } },
7676
{ pattern: /getConfluenceSpaces$/, labels: { past: "Listed spaces", active: "Listing spaces", failure: "list spaces" } },
77+
{ pattern: /getConfluencePageDescendants$/, labels: { past: "Listed descendants", active: "Listing descendants", failure: "list descendants" } },
78+
{ pattern: /getPagesInConfluenceSpace$/, labels: { past: "Listed pages", active: "Listing pages", failure: "list pages" } },
7779
{ pattern: /createConfluencePage$/, labels: { past: "Created page", active: "Creating page", failure: "create page" } },
7880
{ pattern: /updateConfluencePage$/, labels: { past: "Updated page", active: "Updating page", failure: "update page" } },
7981
{ pattern: /getAccessibleAtlassianResources$/, labels: { past: "Got resources", active: "Getting resources", failure: "get resources" } },

0 commit comments

Comments
 (0)