fix: "Show in folder" reveals in the explorer instead of opening a directory in the editor

The open-path interception reroutes EVERY chat-side file open into the
sidebar editor, including the stock "Show in folder" gesture: the
ui-deliverables row passes "." (resolved to "<cwd>/." by the chat view),
so clicking it opened a bogus editor tab for the workspace directory
(host error: '"..." is a directory').

- openpath-intercept: detect the folder-reveal gesture ("." / "./" /
  "<cwd>/.") and route it to a new revealInExplorer dep instead of the editor.
- intercept: revealInExplorer switches to the explorer tab (expanding the
  panel), expands the produced files' parent directories, and highlights /
  scrolls to them; the intercepted produced-files row now also renders its
  own "Show in folder" button when files are hidden.
- produced-files: selectProducedFiles reads the engine Turn data
  (owner.turn.data.get("deliverables")) — the same source ui-deliverables
  uses — with the node replica as fallback. The old code read owner.nodes,
  which the real TurnTailOwnerProps ({turn, seq, openFile}) does not carry,
  so the takeover never claimed the chain.
- state: transient `revealed` set (never persisted) plus a revealPaths
  reducer that expands ancestor directories.
- ExplorerView: highlight + scroll revealed rows; Sidebar/tabs/service
  thread `revealed` through TabComponentProps.
- tests: folder-gesture routing, engine-data selector, wiring seat, zh/en copy.
This commit is contained in:
GammaChineYov
2026-08-15 17:08:04 +08:00
parent 5d2d6e5801
commit 1ca4d4ef4d
11 changed files with 267 additions and 22 deletions
+33 -4
View File
@@ -65,14 +65,43 @@ export function producedForClosing(nodes: readonly unknown[], seq: number): read
/**
* Claim the turn-tail chain only when the closing turn produced files.
* @param owner - the turn-tail owner currency ({nodes, seq}).
*
* The authoritative source is the engine Turn data — the same value
* ui-deliverables reads (`owner.turn.data.get('deliverables')`): a
* `{ produced: [{ seq, path }, ...] }` record accumulated per Turn. The
* node-based replica below stays as a fallback for compositions that do not
* publish it.
* @param owner - the turn-tail owner currency ({turn, seq, openFile}).
* @returns produced paths as the matched value, or null to decline.
*/
export function selectProducedFiles(owner: unknown): readonly string[] | null {
const record = owner as { nodes?: unknown; seq?: unknown } | null
const record = owner as {
turn?: { data?: { get?: (key: string) => unknown } }
nodes?: unknown
seq?: unknown
} | null
if (record === null || typeof record !== 'object') return null
if (!Array.isArray(record.nodes) || typeof record.seq !== 'number') return null
const paths = producedForClosing(record.nodes, record.seq)
const seq = typeof record.seq === 'number' ? record.seq : Number.POSITIVE_INFINITY
const data = record.turn?.data?.get?.('deliverables') as
| { produced?: unknown }
| null
| undefined
if (data !== null && typeof data === 'object' && Array.isArray(data.produced)) {
const paths: string[] = []
const seen = new Set<string>()
for (const item of data.produced) {
if (item === null || typeof item !== 'object') continue
const produced = item as { path?: unknown; seq?: unknown }
if (typeof produced.path !== 'string' || produced.path === '') continue
if (typeof produced.seq === 'number' && produced.seq > seq) continue
if (seen.has(produced.path)) continue
seen.add(produced.path)
paths.push(produced.path)
}
return paths.length === 0 ? null : paths
}
if (!Array.isArray(record.nodes)) return null
const paths = producedForClosing(record.nodes, seq)
return paths.length === 0 ? null : paths
}