Merge pull request #525 from omdsh-dev/feat/dedup-core
feat(client): 收敛重复实现(api envelope/extOf/lineage/错误归一化)并删除死代码
This commit is contained in:
@@ -715,7 +715,7 @@ export function SubagentView(props: {
|
||||
try {
|
||||
sessions.openSubagent?.(address)
|
||||
} catch (error) {
|
||||
console.warn('[dsh-better-sidebar] openSubagent failed:', error)
|
||||
console.error('[dsh-better-sidebar] openSubagent failed:', error)
|
||||
}
|
||||
}, [sessions, onOpenChild])
|
||||
|
||||
@@ -725,7 +725,7 @@ export function SubagentView(props: {
|
||||
try {
|
||||
sessions.open?.(rootId)
|
||||
} catch (error) {
|
||||
console.warn('[dsh-better-sidebar] open session failed:', error)
|
||||
console.error('[dsh-better-sidebar] open session failed:', error)
|
||||
}
|
||||
}, [sessions, rootId])
|
||||
|
||||
|
||||
+21
-18
@@ -129,6 +129,25 @@ export type TerminalDepsStatus =
|
||||
note?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse one `/sidebar` JSON response envelope into its value. A non-ok
|
||||
* status, an unparseable body, or any shape other than `{ok: true, value}`
|
||||
* surfaces as {@link SidebarApiError} carrying the wire code (falling back
|
||||
* to the HTTP status). Shared by the JSON api route and the raw upload
|
||||
* route, whose envelopes are identical.
|
||||
*/
|
||||
async function readEnvelope<T>(response: Response): Promise<T> {
|
||||
const parsed: { ok?: boolean; value?: unknown; error?: { code?: string; message?: string } } | null
|
||||
= await response.json().catch(() => null)
|
||||
if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) {
|
||||
throw new SidebarApiError(
|
||||
parsed?.error?.code ?? 'http',
|
||||
parsed?.error?.message ?? `HTTP ${response.status}`,
|
||||
)
|
||||
}
|
||||
return parsed.value as T
|
||||
}
|
||||
|
||||
async function call<T>(method: string, payload: Record<string, unknown>, signal?: AbortSignal): Promise<T> {
|
||||
let response: Response
|
||||
try {
|
||||
@@ -141,15 +160,7 @@ async function call<T>(method: string, payload: Record<string, unknown>, signal?
|
||||
} catch (error) {
|
||||
throw new SidebarApiError('network', error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
const parsed: { ok?: boolean; value?: unknown; error?: { code?: string; message?: string } } | null
|
||||
= await response.json().catch(() => null)
|
||||
if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) {
|
||||
throw new SidebarApiError(
|
||||
parsed?.error?.code ?? 'http',
|
||||
parsed?.error?.message ?? `HTTP ${response.status}`,
|
||||
)
|
||||
}
|
||||
return parsed.value as T
|
||||
return readEnvelope<T>(response)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,15 +191,7 @@ async function fetchUpload<T>(
|
||||
if (error instanceof DOMException && error.name === 'AbortError') throw error
|
||||
throw new SidebarApiError('network', error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
const parsed: { ok?: boolean; value?: unknown; error?: { code?: string; message?: string } } | null
|
||||
= await response.json().catch(() => null)
|
||||
if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) {
|
||||
throw new SidebarApiError(
|
||||
parsed?.error?.code ?? 'http',
|
||||
parsed?.error?.message ?? `HTTP ${response.status}`,
|
||||
)
|
||||
}
|
||||
return parsed.value as T
|
||||
return readEnvelope<T>(response)
|
||||
}
|
||||
|
||||
/** One request's session scope: the conversation id plus its cwd when known. */
|
||||
|
||||
@@ -64,6 +64,12 @@ function refNames(refs: string): string[] {
|
||||
)]
|
||||
}
|
||||
|
||||
/** One thrown value as display text (every error banner/row here normalizes
|
||||
* through this so non-Error rejections never render as '[object Object]'). */
|
||||
function errorMessage(reason: unknown): string {
|
||||
return reason instanceof Error ? reason.message : String(reason)
|
||||
}
|
||||
|
||||
/** The pending destructive action (discard / revert / cherry-pick), gated by a confirm modal. */
|
||||
interface ConfirmState {
|
||||
title: string
|
||||
@@ -157,7 +163,7 @@ export function GitLens(props: GitLensProps) {
|
||||
setLogEnded(logResult.length < LOG_BATCH)
|
||||
} catch (reason) {
|
||||
if (options.generation === refreshGeneration.current) {
|
||||
setError(reason instanceof Error ? reason.message : String(reason))
|
||||
setError(errorMessage(reason))
|
||||
}
|
||||
} finally {
|
||||
if (options.loading && options.generation === refreshGeneration.current) setLoading(false)
|
||||
@@ -219,7 +225,7 @@ export function GitLens(props: GitLensProps) {
|
||||
await refreshTarget(target, { loading: !silent, generation })
|
||||
} catch (reason) {
|
||||
if (generation === refreshGeneration.current) {
|
||||
setError(reason instanceof Error ? reason.message : String(reason))
|
||||
setError(errorMessage(reason))
|
||||
if (!silent) setLoading(false)
|
||||
}
|
||||
} finally {
|
||||
@@ -289,7 +295,7 @@ export function GitLens(props: GitLensProps) {
|
||||
if (next.length < LOG_BATCH) setLogEnded(true)
|
||||
} catch (reason) {
|
||||
if (generation === refreshGeneration.current && target === chosenPathRef.current) {
|
||||
setCommitError(`${t('historyLoadError')}: ${reason instanceof Error ? reason.message : String(reason)}`)
|
||||
setCommitError(`${t('historyLoadError')}: ${errorMessage(reason)}`)
|
||||
}
|
||||
} finally {
|
||||
if (generation === refreshGeneration.current && target === chosenPathRef.current) setLogLoadingMore(false)
|
||||
@@ -355,7 +361,7 @@ export function GitLens(props: GitLensProps) {
|
||||
setCommitMsg('')
|
||||
await refresh()
|
||||
} catch (reason) {
|
||||
setCommitError(reason instanceof Error ? reason.message : String(reason))
|
||||
setCommitError(errorMessage(reason))
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
@@ -369,7 +375,7 @@ export function GitLens(props: GitLensProps) {
|
||||
await api.gitCheckout(gitScope, branch, selectedWorktree)
|
||||
await refresh()
|
||||
} catch (reason) {
|
||||
setCommitError(`${t('checkoutError')}: ${reason instanceof Error ? reason.message : String(reason)}`)
|
||||
setCommitError(`${t('checkoutError')}: ${errorMessage(reason)}`)
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
@@ -384,7 +390,7 @@ export function GitLens(props: GitLensProps) {
|
||||
await confirmState.onConfirm()
|
||||
await refresh()
|
||||
} catch (reason) {
|
||||
setCommitError(reason instanceof Error ? reason.message : String(reason))
|
||||
setCommitError(errorMessage(reason))
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
|
||||
+5
-7
@@ -48,14 +48,12 @@ import { pascal } from '@codemirror/legacy-modes/mode/pascal'
|
||||
import { vb } from '@codemirror/legacy-modes/mode/vb'
|
||||
import { vhdl } from '@codemirror/legacy-modes/mode/vhdl'
|
||||
import { stex } from '@codemirror/legacy-modes/mode/stex'
|
||||
import { extOf } from './paths.ts'
|
||||
|
||||
/** The lowercased file extension of a path ('' when none). */
|
||||
export function extOf(path: string): string {
|
||||
const at = path.lastIndexOf('.')
|
||||
if (at === -1) return ''
|
||||
const base = path.slice(at + 1).toLowerCase()
|
||||
return base.includes('/') || base.includes('\\') ? '' : base
|
||||
}
|
||||
// Re-exported for the established import surface (tests import extOf from
|
||||
// this module); the implementation lives in the dependency-free paths.ts so
|
||||
// core-bundle modules can share it without pulling the CodeMirror packages.
|
||||
export { extOf }
|
||||
|
||||
/** Language key for an extension, or null for plain text. Pure (tested). */
|
||||
export function languageKeyForExt(ext: string): string | null {
|
||||
|
||||
@@ -70,3 +70,16 @@ export function baseName(path: string): string {
|
||||
const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
|
||||
return at === -1 ? path : path.slice(at + 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* The lowercased file extension of a path ('' when none). The dot must sit
|
||||
* inside the last segment — a dot in a directory name is not an extension.
|
||||
* Shared by the editor language mapping (lang.ts) and the viewer registry's
|
||||
* extension matching (service.ts), which both live in the core bundle.
|
||||
*/
|
||||
export function extOf(path: string): string {
|
||||
const at = path.lastIndexOf('.')
|
||||
if (at === -1) return ''
|
||||
const base = path.slice(at + 1).toLowerCase()
|
||||
return base.includes('/') || base.includes('\\') ? '' : base
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
type SidebarSnapshot, type SidebarState, type SidebarStore, type SidebarTab,
|
||||
} from './state.ts'
|
||||
import { isNarrowWidth } from './breakpoints.ts'
|
||||
import { extOf } from './paths.ts'
|
||||
import type { SessionScope } from './api.ts'
|
||||
import type { SidebarPrefs } from '../prefs-shared.ts'
|
||||
|
||||
@@ -427,14 +428,6 @@ export interface BetterSidebarService {
|
||||
openFile(scope: SessionScope, path: string, title?: string): void
|
||||
}
|
||||
|
||||
/** Extract the lowercase extension without leading dot from a path. */
|
||||
function extOfPath(path: string): string {
|
||||
const at = path.lastIndexOf('.')
|
||||
if (at === -1) return ''
|
||||
const base = path.slice(at + 1).toLowerCase()
|
||||
return base.includes('/') || base.includes('\\') ? '' : base
|
||||
}
|
||||
|
||||
/** The file name of a path (both separators). */
|
||||
function baseNameOf(path: string): string {
|
||||
const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
|
||||
@@ -571,7 +564,7 @@ export function createBetterSidebarService(store: SidebarStore): BetterSidebarSe
|
||||
const isViewerEnabled = (id: string): boolean => store.getPrefs().viewersEnabled[id] !== false
|
||||
|
||||
const matchFileViewer = (path: string, head?: Uint8Array): FileViewerDescriptor | undefined => {
|
||||
const ext = extOfPath(path)
|
||||
const ext = extOf(path)
|
||||
// Single pass in priority order (descending; stable for equal
|
||||
// priorities — insertion order). Each descriptor gets first refusal in
|
||||
// its own turn: `detect` (when head bytes are available) beats its own
|
||||
|
||||
@@ -9,23 +9,20 @@
|
||||
* - {@link countSubagentDescendants}: uninterrupted subagent-origin lineage
|
||||
* totals (mirror of the official `indexSubagentDescendants` over the
|
||||
* plugin's own summary rows).
|
||||
*
|
||||
* The lineage walks themselves ({@link isSideThreadSummary}, {@link
|
||||
* rootAncestor}, {@link countSubagentDescendants}) live in
|
||||
* ./subagent-lineage.ts — the single shared walk implementation — and are
|
||||
* re-exported below for their established import sites.
|
||||
*/
|
||||
import type {
|
||||
SidebarSessionList,
|
||||
SidebarSessionSummary,
|
||||
SidebarSubagentCatalog,
|
||||
} from '../context-types.ts'
|
||||
import { SIDE_LABEL_PREFIX } from '../sidechat-core.ts'
|
||||
import { countSubagentDescendants, isSideThreadSummary, rootAncestor } from './subagent-lineage.ts'
|
||||
|
||||
/**
|
||||
* Side Chat threads ride the subagent origin (main-list hiding + the RPC
|
||||
* ownership fence) but they are NOT subagent topology: they carry the
|
||||
* durable 'Side: ' label and live as sidebar tabs. Excluding them here
|
||||
* keeps the auto-open trigger and the Subagent page counts clean.
|
||||
*/
|
||||
export function isSideThreadSummary(summary: SidebarSessionSummary): boolean {
|
||||
return summary.origin === 'subagent' && summary.displayTitle.startsWith(SIDE_LABEL_PREFIX)
|
||||
}
|
||||
export { countSubagentDescendants, isSideThreadSummary, rootAncestor }
|
||||
export type { SubagentDescendantTotals } from './subagent-lineage.ts'
|
||||
|
||||
/** Count the direct subagent children of one session (durable `origin` rows). */
|
||||
export function directSubagentCount(
|
||||
@@ -40,28 +37,6 @@ export function directSubagentCount(
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* The main agent of the current session's tree: walk the durable parent
|
||||
* chain upward until the first non-subagent session. The Subagent page shows
|
||||
* THIS root's full topology regardless of how deep the current selection is
|
||||
* (a session whose row is still hydrating, or a broken chain, degrades to
|
||||
* the session itself).
|
||||
*/
|
||||
export function rootAncestor(
|
||||
byId: SidebarSessionList['byId'],
|
||||
sessionId: string | undefined,
|
||||
): string | undefined {
|
||||
if (sessionId === undefined) return undefined
|
||||
const seen = new Set<string>()
|
||||
let current: SidebarSessionSummary | undefined = byId[sessionId]
|
||||
while (current !== undefined && current.origin === 'subagent'
|
||||
&& current.parentId !== undefined && !seen.has(current.id)) {
|
||||
seen.add(current.id)
|
||||
current = byId[current.parentId]
|
||||
}
|
||||
return current?.id ?? sessionId
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect every catalog branch (an entry with `hasChildren`) reachable from
|
||||
* the root — the set of catalogs the always-expanded topology consumes.
|
||||
@@ -101,37 +76,3 @@ export function detectNewDirectSubagent(
|
||||
return directSubagentCount(prev.byId, sessionId) === 0
|
||||
&& directSubagentCount(next.byId, sessionId) > 0
|
||||
}
|
||||
|
||||
/** Descendant totals of one session through an uninterrupted subagent-origin chain. */
|
||||
export interface SubagentDescendantTotals {
|
||||
count: number
|
||||
runningCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Index every subagent descendant under each ancestor it reaches through an
|
||||
* uninterrupted subagent-origin chain (same semantics as the official
|
||||
* `indexSubagentDescendants`; cycles fail soft).
|
||||
*/
|
||||
export function countSubagentDescendants(
|
||||
byId: SidebarSessionList['byId'],
|
||||
sessionId: string,
|
||||
): SubagentDescendantTotals {
|
||||
const totals: SubagentDescendantTotals = { count: 0, runningCount: 0 }
|
||||
for (const descendant of Object.values(byId)) {
|
||||
if (descendant.origin !== 'subagent' || isSideThreadSummary(descendant)) continue
|
||||
const seen = new Set<string>()
|
||||
let current: SidebarSessionSummary | undefined = descendant
|
||||
while (current?.origin === 'subagent' && current.parentId !== undefined
|
||||
&& !seen.has(current.id)) {
|
||||
seen.add(current.id)
|
||||
if (current.parentId === sessionId) {
|
||||
totals.count += 1
|
||||
if (descendant.running === true) totals.runningCount += 1
|
||||
break
|
||||
}
|
||||
current = byId[current.parentId]
|
||||
}
|
||||
}
|
||||
return totals
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
*/
|
||||
import type {
|
||||
SidebarSessionList,
|
||||
SidebarSessionSummary,
|
||||
SidebarJobStatus,
|
||||
SidebarJobView,
|
||||
} from '../context-types.ts'
|
||||
import type { CopyKey } from './locales.ts'
|
||||
import { treeSessionIds } from './subagent-lineage.ts'
|
||||
|
||||
/** One row of the jobs section: the job plus its owning session's title. */
|
||||
export interface TreeJob {
|
||||
@@ -26,36 +26,10 @@ export function isJobLive(job: SidebarJobView): boolean {
|
||||
return job.status === 'running' || job.status === 'stopping'
|
||||
}
|
||||
|
||||
/**
|
||||
* Every session id of the topology tree rooted at `rootId` (the root plus
|
||||
* each session whose uninterrupted subagent-origin chain reaches it — same
|
||||
* lineage semantics as {@link countSubagentDescendants}; cycles fail soft).
|
||||
* Sessions outside the tree (orphans, other trees) are excluded, so the
|
||||
* jobs section never shows foreign work.
|
||||
*/
|
||||
export function treeSessionIds(
|
||||
byId: SidebarSessionList['byId'],
|
||||
rootId: string | undefined,
|
||||
): Set<string> {
|
||||
const ids = new Set<string>()
|
||||
if (rootId === undefined) return ids
|
||||
for (const summary of Object.values(byId)) {
|
||||
const seen = new Set<string>()
|
||||
let current: SidebarSessionSummary | undefined = summary
|
||||
let reachesRoot = false
|
||||
while (current !== undefined && !seen.has(current.id)) {
|
||||
seen.add(current.id)
|
||||
if (current.id === rootId) {
|
||||
reachesRoot = true
|
||||
break
|
||||
}
|
||||
if (current.origin !== 'subagent' || current.parentId === undefined) break
|
||||
current = byId[current.parentId]
|
||||
}
|
||||
if (reachesRoot) ids.add(summary.id)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
// The lineage walk itself lives in ./subagent-lineage.ts (the single shared
|
||||
// subagent-origin chain implementation); re-exported for the established
|
||||
// import sites of this module.
|
||||
export { treeSessionIds }
|
||||
|
||||
/**
|
||||
* Whether a NEW background job appeared for one session between two
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* The shared subagent-lineage walks over the sessions list feed (structural
|
||||
* mirror world — no runtime imports). Every public walk here answers a
|
||||
* topology question by following the DURABLE subagent-origin chain upward
|
||||
* (child → parent → …); one private generator ({@link subagentOriginChain})
|
||||
* is the single implementation of that walk, seen-set guarded so cycles fail
|
||||
* soft (the walk just stops; nothing throws).
|
||||
*
|
||||
* subagent-detect.ts and subagent-jobs.ts re-export these for their
|
||||
* established import sites — the auto-activation triggers and the Subagent
|
||||
* page both consume them through the original module paths.
|
||||
*/
|
||||
import type { SidebarSessionList, SidebarSessionSummary } from '../context-types.ts'
|
||||
import { SIDE_LABEL_PREFIX } from '../sidechat-core.ts'
|
||||
|
||||
/**
|
||||
* Side Chat threads ride the subagent origin (main-list hiding + the RPC
|
||||
* ownership fence) but they are NOT subagent topology: they carry the
|
||||
* durable 'Side: ' label and live as sidebar tabs. Excluding them here
|
||||
* keeps the auto-open trigger and the Subagent page counts clean.
|
||||
*/
|
||||
export function isSideThreadSummary(summary: SidebarSessionSummary): boolean {
|
||||
return summary.origin === 'subagent' && summary.displayTitle.startsWith(SIDE_LABEL_PREFIX)
|
||||
}
|
||||
|
||||
/**
|
||||
* Yield `start`'s uninterrupted subagent-origin chain upward: each summary
|
||||
* while it is a subagent with a known parent, then its parent row, and so
|
||||
* on. A revisited id ends the walk (cycles fail soft); the row that BREAKS
|
||||
* the chain — the first non-subagent ancestor, or a parent id with no row —
|
||||
* is deliberately NOT yielded: callers observe it through where the walk
|
||||
* stopped (the row after the last yielded one is `byId[last.parentId]`).
|
||||
*/
|
||||
function* subagentOriginChain(
|
||||
byId: SidebarSessionList['byId'],
|
||||
start: SidebarSessionSummary,
|
||||
): Generator<SidebarSessionSummary> {
|
||||
const seen = new Set<string>()
|
||||
let current: SidebarSessionSummary | undefined = start
|
||||
while (current?.origin === 'subagent' && current.parentId !== undefined && !seen.has(current.id)) {
|
||||
seen.add(current.id)
|
||||
yield current
|
||||
current = byId[current.parentId]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main agent of the current session's tree: walk the durable parent
|
||||
* chain upward until the first non-subagent session. The Subagent page shows
|
||||
* THIS root's full topology regardless of how deep the current selection is
|
||||
* (a session whose row is still hydrating, or a broken chain, degrades to
|
||||
* the session itself).
|
||||
*/
|
||||
export function rootAncestor(
|
||||
byId: SidebarSessionList['byId'],
|
||||
sessionId: string | undefined,
|
||||
): string | undefined {
|
||||
if (sessionId === undefined) return undefined
|
||||
const start = byId[sessionId]
|
||||
// A hydrating (or unknown) row degrades to the session itself.
|
||||
if (start === undefined) return sessionId
|
||||
let last: SidebarSessionSummary | undefined
|
||||
for (const node of subagentOriginChain(byId, start)) last = node
|
||||
if (last === undefined) return start.id
|
||||
// The walk stopped one row past the last chain node: that parent row is
|
||||
// the root (still missing from the feed → the session itself). Every
|
||||
// yielded node carries a parent id (the generator's walk condition).
|
||||
return byId[last.parentId!]?.id ?? sessionId
|
||||
}
|
||||
|
||||
/** Descendant totals of one session through an uninterrupted subagent-origin chain. */
|
||||
export interface SubagentDescendantTotals {
|
||||
count: number
|
||||
runningCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Index every subagent descendant under each ancestor it reaches through an
|
||||
* uninterrupted subagent-origin chain (same semantics as the official
|
||||
* `indexSubagentDescendants`; cycles fail soft).
|
||||
*/
|
||||
export function countSubagentDescendants(
|
||||
byId: SidebarSessionList['byId'],
|
||||
sessionId: string,
|
||||
): SubagentDescendantTotals {
|
||||
const totals: SubagentDescendantTotals = { count: 0, runningCount: 0 }
|
||||
for (const descendant of Object.values(byId)) {
|
||||
if (descendant.origin !== 'subagent' || isSideThreadSummary(descendant)) continue
|
||||
for (const node of subagentOriginChain(byId, descendant)) {
|
||||
if (node.parentId === sessionId) {
|
||||
totals.count += 1
|
||||
if (descendant.running === true) totals.runningCount += 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return totals
|
||||
}
|
||||
|
||||
/**
|
||||
* Every session id of the topology tree rooted at `rootId` (the root plus
|
||||
* each session whose uninterrupted subagent-origin chain reaches it — same
|
||||
* lineage semantics as {@link countSubagentDescendants}; cycles fail soft).
|
||||
* Sessions outside the tree (orphans, other trees) are excluded, so the
|
||||
* jobs section never shows foreign work.
|
||||
*/
|
||||
export function treeSessionIds(
|
||||
byId: SidebarSessionList['byId'],
|
||||
rootId: string | undefined,
|
||||
): Set<string> {
|
||||
const ids = new Set<string>()
|
||||
// A root with no row in the feed cannot be reached by walking: only real
|
||||
// rows anchor tree membership (a parent pointer naming a hydrating root
|
||||
// does not pull the orphaned branch into the tree).
|
||||
if (rootId === undefined || byId[rootId] === undefined) return ids
|
||||
for (const summary of Object.values(byId)) {
|
||||
// The root row itself belongs to its own tree even though it is not a
|
||||
// subagent (it is usually the main agent).
|
||||
if (summary.id === rootId) {
|
||||
ids.add(summary.id)
|
||||
continue
|
||||
}
|
||||
for (const node of subagentOriginChain(byId, summary)) {
|
||||
if (node.parentId === rootId) {
|
||||
ids.add(summary.id)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
@@ -245,11 +245,6 @@ export function buildSidechatInheritance(events: readonly SidechatLogEvent[]): S
|
||||
return { seed, snapshot: null }
|
||||
}
|
||||
|
||||
/** The seed half of {@link buildSidechatInheritance} (test convenience). */
|
||||
export function sidechatSeed(events: readonly SidechatLogEvent[]): SeedEvent[] {
|
||||
return buildSidechatInheritance(events).seed
|
||||
}
|
||||
|
||||
/**
|
||||
* Structured text snapshot of the parent's OPEN turn (from its `turn/start`
|
||||
* to the log tail): the accumulated assistant/reasoning output verbatim
|
||||
|
||||
Reference in New Issue
Block a user