Skip to content

Commit 8c8400a

Browse files
authored
fix: remove flattenSingleChild/promoteMainEntry from CapturingToken (#4701)
XtabProvider entries appeared unnested at the top level when the sibling NES MarkdownContentRequest entry was hidden (via setIsSkipped or cancellation). This happened because flattenSingleChild promoted the sole remaining child to the top level. Instead of adding the log context document as a child that gets 'promoted' into the parent, ChatPromptItem now directly owns the document via setMainEntry(). A MarkdownContentRequest whose debugName matches the token label is wired to the parent's icon and click command — never added as a tree child. Groups with no visible children render as non-expandable leaf items. This removes flattenSingleChild and promoteMainEntry from CapturingToken, making it a pure correlation token with no rendering hints. The tree view owns all rendering conventions.
1 parent 026f0a2 commit 8c8400a

16 files changed

Lines changed: 67 additions & 101 deletions

File tree

src/extension/chatSessions/claude/node/claudeCodeAgent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,7 @@ export class ClaudeCodeSession extends Disposable {
556556
const promptLabel = request.prompt.filter(p => p.type === 'text').at(-1)?.text ?? 'Claude Session Prompt';
557557
this.sessionStateService.setCapturingTokenForSession(
558558
this.sessionId,
559-
new CapturingToken(promptLabel, 'claude', false, false, undefined, undefined, this.sessionId)
559+
new CapturingToken(promptLabel, 'claude', undefined, undefined, this.sessionId)
560560
);
561561

562562
// Emit a user_message span event for the debug panel

src/extension/chatSessions/claude/vscode-node/slashCommands/terminalCommand.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export class TerminalSlashCommand implements IClaudeSlashCommandHandler {
108108
// Set capturing token only after terminal is successfully created to avoid leaking stale session state
109109
this.sessionStateService.setCapturingTokenForSession(
110110
sessionId,
111-
new CapturingToken(`Claude CLI (${sessionId})`, 'claude', false)
111+
new CapturingToken(`Claude CLI (${sessionId})`, 'claude')
112112
);
113113

114114
this.logService.info(`[TerminalSlashCommand] Created terminal with Claude CLI configured on port ${config.port}, command: ${cliCommand}, sessionId: ${sessionId}`);

src/extension/chatSessions/claude/vscode-node/slashCommands/test/terminalCommand.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,6 @@ describe('TerminalSlashCommand', () => {
234234
expect.objectContaining({
235235
label: `Claude CLI (${TEST_SESSION_ID})`,
236236
icon: 'claude',
237-
flattenSingleChild: false,
238237
})
239238
);
240239
});

src/extension/chatSessions/copilotcli/node/copilotcliSession.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
229229
}
230230
const label = getPromptLabel(input);
231231
const promptLabel = truncate(label, 50);
232-
const capturingToken = new CapturingToken(`Copilot CLI | ${promptLabel}`, 'worktree', false, true, undefined, undefined, this.sessionId);
232+
const capturingToken = new CapturingToken(`Copilot CLI | ${promptLabel}`, 'worktree', undefined, undefined, this.sessionId);
233233
const isAlreadyBusyWithAnotherRequest = !!this._status && (this._status === ChatSessionStatus.InProgress || this._status === ChatSessionStatus.NeedsInput);
234234
this._toolInvocationToken = request.toolInvocationToken;
235235

src/extension/completions-core/vscode-node/extension/src/vscodeInlineCompletionItemProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export class CopilotInlineCompletionItemProvider extends Disposable implements I
114114

115115
const label = `Ghost | ${basename(doc.uri.toString())} (v${doc.version})`;
116116

117-
const capturingToken = new CapturingToken(label, undefined, true, true);
117+
const capturingToken = new CapturingToken(label, undefined);
118118

119119
return await this.requestLogger.captureInvocation(capturingToken, async () => {
120120

src/extension/inlineEdits/node/nextEditProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1258,7 +1258,7 @@ export class NextEditProvider extends Disposable implements INextEditProvider<Ne
12581258
// Start the provider call - this runs in the background and populates the cache
12591259
const label = `NES | spec | ${basename(doc.id.toUri().fsPath)} (v${doc.version.get()})`;
12601260

1261-
const capturingToken = new CapturingToken(label, undefined, false, true);
1261+
const capturingToken = new CapturingToken(label, undefined);
12621262

12631263
void this._requestLogger.captureInvocation(capturingToken, async () => {
12641264
try {

src/extension/inlineEdits/vscode-node/inlineCompletionProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ export class InlineCompletionProviderImpl extends Disposable implements InlineCo
221221
): Promise<NesCompletionList | undefined> {
222222
const label = `NES | ${basename(document.uri.fsPath)} (v${document.version})`;
223223

224-
const capturingToken = new CapturingToken(label, undefined, true, true);
224+
const capturingToken = new CapturingToken(label, undefined);
225225

226226
assert(context.changeHint === undefined || NesChangeHint.is(context.changeHint), 'Expected changeHint to be of type TriggerNes or undefined');
227227
const changeHint = context.changeHint as NesChangeHint | undefined;

src/extension/log/vscode-node/requestLogTree.ts

Lines changed: 53 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -588,10 +588,6 @@ class ChatRequestProvider extends Disposable implements vscode.TreeDataProvider<
588588

589589
getChildren(element?: TreeItem | undefined): vscode.ProviderResult<TreeItem[]> {
590590
if (element instanceof ChatPromptItem) {
591-
// If mainEntryId is set, filter out the main entry from children
592-
if (element.mainEntryId) {
593-
return element.children.filter(child => child.id !== element.mainEntryId);
594-
}
595591
return element.children;
596592
} else if (element) {
597593
return [];
@@ -600,17 +596,15 @@ class ChatRequestProvider extends Disposable implements vscode.TreeDataProvider<
600596
const tokenToPrompt = new Map<CapturingToken, ChatPromptItem>();
601597

602598
for (const currReq of this.requestLogger.getRequests()) {
603-
// Skip entries that are dynamically hidden (e.g. skipped/cancelled live NES requests)
604-
if (currReq.kind === LoggedInfoKind.Request &&
605-
currReq.entry.type === LoggedRequestKind.MarkdownContentRequest &&
606-
currReq.entry.isVisible && !currReq.entry.isVisible()) {
607-
continue;
608-
}
609-
610-
const currReqTreeItem = this.logToTreeItem(currReq);
611-
612599
if (!currReq.token) {
613-
result.push(currReqTreeItem);
600+
// Skip non-main hidden entries (e.g. skipped/cancelled live NES requests)
601+
if (currReq.kind === LoggedInfoKind.Request &&
602+
currReq.entry.type === LoggedRequestKind.MarkdownContentRequest &&
603+
currReq.entry.isVisible && !currReq.entry.isVisible()) {
604+
continue;
605+
}
606+
607+
result.push(this.logToTreeItem(currReq));
614608
continue;
615609
}
616610

@@ -621,30 +615,38 @@ class ChatRequestProvider extends Disposable implements vscode.TreeDataProvider<
621615
result.push(prompt);
622616
}
623617

618+
// If this entry is the main entry for the group (a MarkdownContentRequest
619+
// whose debugName matches the token label), associate it directly with the
620+
// parent ChatPromptItem — don't add it as a child. The entry stays in the
621+
// request logger for virtual document serving; only tree nesting changes.
622+
// Only wire the main entry if it is visible — for live NES/Ghost entries,
623+
// isVisible() can be false (e.g. skipped/cancelled); wiring a hidden entry
624+
// would make it visible again via the parent's icon and click command.
625+
if (currReq.kind === LoggedInfoKind.Request &&
626+
currReq.entry.type === LoggedRequestKind.MarkdownContentRequest &&
627+
currReq.entry.debugName === currReq.token.label) {
628+
const isHidden = currReq.entry.isVisible && !currReq.entry.isVisible();
629+
if (!isHidden) {
630+
prompt.setMainEntry(currReq);
631+
}
632+
continue;
633+
}
634+
635+
// Skip non-main hidden entries
636+
if (currReq.kind === LoggedInfoKind.Request &&
637+
currReq.entry.type === LoggedRequestKind.MarkdownContentRequest &&
638+
currReq.entry.isVisible && !currReq.entry.isVisible()) {
639+
continue;
640+
}
641+
642+
const currReqTreeItem = this.logToTreeItem(currReq);
624643
const alreadyIncluded = prompt.children.find(existingChild => existingChild.id === currReqTreeItem.id);
625644
if (!alreadyIncluded) {
626645
prompt.children.push(currReqTreeItem);
627646
}
628647
}
629648

630-
// Post-process: flatten single-child items and promote main entries
631-
const processed: (ChatPromptItem | TreeChildItem)[] = [];
632-
for (const item of result) {
633-
if (item instanceof ChatPromptItem) {
634-
if (item.token.flattenSingleChild && item.children.length === 1) {
635-
processed.push(item.children[0]);
636-
} else {
637-
if (item.token.promoteMainEntry) {
638-
item.promoteMainEntry();
639-
}
640-
processed.push(item);
641-
}
642-
} else {
643-
processed.push(item);
644-
}
645-
}
646-
647-
return filterMap(processed, r => {
649+
return filterMap(result, r => {
648650
if (!this.filters.itemIncluded(r)) {
649651
return undefined;
650652
}
@@ -679,72 +681,59 @@ class ChatPromptItem extends vscode.TreeItem {
679681
override readonly contextValue = 'chatprompt';
680682
public children: TreeChildItem[] = [];
681683
public override id: string | undefined;
682-
/**
683-
* The ID of the main entry that was promoted to this parent item.
684-
* When set, clicking the parent opens this entry and it's excluded from children.
685-
*/
686-
public mainEntryId: string | undefined;
687684

688685
public static create(info: LoggedInfo, request: CapturingToken) {
689686
const existing = ChatPromptItem.ids.get(info);
690687
if (existing) {
691688
return existing;
692689
}
693690

694-
// Check if this first info should be promoted to the main entry
695-
let mainEntryId: string | undefined;
696-
if (request.promoteMainEntry && info.kind === LoggedInfoKind.Request) {
697-
const requestInfo = info as ILoggedRequestInfo;
698-
if (requestInfo.entry.debugName === request.label) {
699-
mainEntryId = info.id;
700-
}
701-
}
702-
703-
const item = new ChatPromptItem(request, mainEntryId);
691+
const item = new ChatPromptItem(request);
704692
item.id = info.id + '-prompt';
705693
ChatPromptItem.ids.set(info, item);
706694
return item;
707695
}
708696

709-
protected constructor(public readonly token: CapturingToken, mainEntryId?: string) {
697+
protected constructor(public readonly token: CapturingToken) {
710698
super(token.label, vscode.TreeItemCollapsibleState.Expanded);
711699
if (token.icon) {
712700
this.iconPath = new vscode.ThemeIcon(token.icon);
713701
}
714-
if (mainEntryId) {
715-
this.mainEntryId = mainEntryId;
716-
this.command = {
717-
command: 'vscode.open',
718-
title: '',
719-
arguments: [vscode.Uri.parse(ChatRequestScheme.buildUri({ kind: 'request', id: mainEntryId }))]
720-
};
721-
}
722702
}
723703

724-
public promoteMainEntry() {
725-
const child = this.children.find(child => child.label === this.label);
726-
if (!child || child.id === undefined) {
704+
/**
705+
* Associate a main entry directly with this parent item.
706+
* The main entry's icon and click command are shown on the parent node.
707+
* The entry is NOT added as a child — it stays in the request logger
708+
* for virtual document serving only.
709+
*/
710+
public setMainEntry(info: ILoggedRequestInfo): void {
711+
if (info.entry.type !== LoggedRequestKind.MarkdownContentRequest) {
727712
return;
728713
}
729-
this.mainEntryId = child.id;
730-
if (child.iconPath) {
731-
this.iconPath = child.iconPath;
714+
const resolvedIcon = resolveMarkdownIcon(info.entry);
715+
if (resolvedIcon !== undefined) {
716+
this.iconPath = new vscode.ThemeIcon(resolvedIcon.id);
732717
}
733718
this.command = {
734719
command: 'vscode.open',
735720
title: '',
736-
arguments: [vscode.Uri.parse(ChatRequestScheme.buildUri({ kind: 'request', id: this.mainEntryId }))]
721+
arguments: [vscode.Uri.parse(ChatRequestScheme.buildUri({ kind: 'request', id: info.id }))]
737722
};
738723
}
739724

740725
public withFilteredChildren(filter: (child: TreeChildItem) => boolean): ChatPromptItem {
741-
const item = new ChatPromptItem(this.token, this.mainEntryId);
726+
const item = new ChatPromptItem(this.token);
742727
item.children = this.children.filter(filter);
743728
item.id = this.id;
744729
item.iconPath = this.iconPath;
745730
item.command = this.command;
731+
item.collapsibleState = item.children.length > 0
732+
? vscode.TreeItemCollapsibleState.Expanded
733+
: vscode.TreeItemCollapsibleState.None;
746734
return item;
747735
}
736+
748737
}
749738

750739
class ToolCallItem extends vscode.TreeItem {

src/extension/prompt/node/defaultIntentRequestHandler.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,6 @@ export class DefaultIntentRequestHandler {
141141
const capturingToken = new CapturingToken(
142142
this.request.prompt,
143143
'comment',
144-
false,
145-
false,
146144
this.request.subAgentInvocationId,
147145
this.request.subAgentName,
148146
// For subagents, use invocation ID as chatSessionId so spans get their own log file

src/extension/prompt/node/promptCategorizer.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,6 @@ export class PromptCategorizerService implements IPromptCategorizerService {
195195
const capturingToken = new CapturingToken(
196196
'categorization',
197197
undefined,
198-
false,
199-
false,
200198
undefined,
201199
undefined,
202200
undefined,

0 commit comments

Comments
 (0)