{"version":3,"file":"resource-display.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/resource-display.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,KAAK,SAAS,EAAU,IAAI,EAAgB,MAAM,0BAA0B,CAAC;AACtF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAIvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAItE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACxF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAS5D,MAAM,WAAW,UAAU;IAC1B,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;CACrC;AAQD,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,UAAU,CAE5D;AAED,qBAAa,cAAe,SAAQ,IAAK,YAAW,UAAU;IAI5D,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,QAAQ;IAJjB,OAAO,CAAC,UAAU,CAAU;IAE5B,YACS,SAAS,EAAE,MAAM,MAAM,EACvB,QAAQ,EAAE,MAAM,MAAM,EAC9B,iBAAiB,EAAE,OAAO,EAC1B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAIhB;IAED,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAGnC;IAED;;;;;;OAMG;IACH,OAAO,IAAI,IAAI,CAEd;CACD;AAMD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAUnD;AAsXD,2EAA2E;AAC3E,MAAM,WAAW,mBAAmB;IACnC,aAAa,EAAE,SAAS,CAAC;IACzB,MAAM,IAAI,MAAM,CAAC;IACjB,iBAAiB,IAAI,cAAc,CAAC;IACpC,kBAAkB,IAAI,aAAa,CAAC,cAAc,CAAC,CAAC;IACpD,kBAAkB,IAAI,eAAe,CAAC;IACtC,aAAa,IAAI,MAAM,CAAC;IACxB,kBAAkB,IAAI,OAAO,CAAC;IAC9B,wEAAwE;IACxE,aAAa,IAAI,MAAM,CAAC;IACxB,sEAAsE;IACtE,SAAS,IAAI,eAAe,EAAE,CAAC;IAC/B;;;;;OAKG;IACH,WAAW,IAAI,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACvE,2EAA2E;IAC3E,UAAU,CAAC,IAAI,MAAM,GAAG,SAAS,CAAC;IAClC,YAAY,IAAI,OAAO,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,4DAA4D;IAC5D,UAAU,IAAI,OAAO,CAAC;IACtB,oCAAoC,CAAC,eAAe,EAAE,eAAe,GAAG,kBAAkB,EAAE,CAAC;CAC7F;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACtC,IAAI,EAAE,IAAI,CAAC,mBAAmB,EAAE,SAAS,GAAG,cAAc,CAAC,EAC3D,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAC3B,OAAO,CAET;AAED,wBAAgB,mBAAmB,CAClC,IAAI,EAAE,mBAAmB,EACzB,OAAO,CAAC,EAAE;IACT,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,UAAU,CAAA;KAAE,CAAC,CAAC;IAC9D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACnC,GACC,IAAI,CA+VN","sourcesContent":["/**\n * Startup/reload resource listing: path and source-label formatting, scope\n * grouping, diagnostics rendering, and the [Resources]/[Skills]/… sections\n * shown in the chat. Extracted from interactive-mode.ts; everything here is\n * either pure formatting or rendering into a container passed via deps.\n */\n\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { type Container, Spacer, Text, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport type { AgentDefinition } from \"../../core/agent-frontmatter.js\";\nimport { summarizeAgentDescription } from \"../../core/agent-registry.js\";\nimport { CATEGORY_GLYPH, type CategoryKey, SEGMENT_SEP } from \"../../core/brand.js\";\nimport type { ContextFile } from \"../../core/context-files.js\";\nimport type { ExtensionRunner } from \"../../core/extensions/index.js\";\nimport { renderCompactRows } from \"../../core/format-list.js\";\nimport { formatTokens } from \"../../core/format-tokens.js\";\nimport { getMcpServerStatuses } from \"../../core/mcp-status.js\";\nimport type { PromptTemplate } from \"../../core/prompt-templates.js\";\nimport type { ResourceDiagnostic, ResourceLoader } from \"../../core/resource-loader.js\";\nimport type { SourceInfo } from \"../../core/source-info.js\";\nimport { parseGitUrl } from \"../../utils/git.js\";\nimport { getCwdRelativePath } from \"../../utils/paths.js\";\nimport { type ThemeColor, theme } from \"./theme/theme.js\";\n\n// ============================================================================\n// Expandable sections\n// ============================================================================\n\nexport interface Expandable {\n\tsetExpanded(expanded: boolean): void;\n}\n\n/**\n * Left rail for the startup summary. Flush against the left edge with the\n * banner, so the whole first page reads as one column.\n */\nconst RAIL = \"\";\n\nexport function isExpandable(obj: unknown): obj is Expandable {\n\treturn typeof obj === \"object\" && obj !== null && \"setExpanded\" in obj;\n}\n\nexport class ExpandableText extends Text implements Expandable {\n\tprivate isExpanded: boolean;\n\n\tconstructor(\n\t\tprivate collapsed: () => string,\n\t\tprivate expanded: () => string,\n\t\tinitiallyExpanded: boolean,\n\t\tpaddingX: number,\n\t\tpaddingY: number,\n\t) {\n\t\tsuper(initiallyExpanded ? expanded() : collapsed(), paddingX, paddingY);\n\t\tthis.isExpanded = initiallyExpanded;\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.isExpanded = expanded;\n\t\tthis.setText(expanded ? this.expanded() : this.collapsed());\n\t}\n\n\t/**\n\t * Re-evaluate the current state's text without changing which state it is in.\n\t *\n\t * The two closures are captured once, so anything they read that can change\n\t * later — the startup banner reads the working directory — goes stale until\n\t * something asks for it again. `/cd` is that something.\n\t */\n\trefresh(): void {\n\t\tthis.setText(this.isExpanded ? this.expanded() : this.collapsed());\n\t}\n}\n\n// ============================================================================\n// Path and source-label formatting\n// ============================================================================\n\nexport function formatDisplayPath(p: string): string {\n\tconst home = os.homedir();\n\tlet result = p;\n\n\t// Replace home directory with ~\n\tif (result.startsWith(home)) {\n\t\tresult = `~${result.slice(home.length)}`;\n\t}\n\n\treturn result;\n}\n\nfunction formatExtensionDisplayPath(extensionPath: string): string {\n\tlet result = formatDisplayPath(extensionPath);\n\tresult = result.replace(/\\/index\\.ts$/, \"\").replace(/\\/index\\.js$/, \"\");\n\treturn result;\n}\n\nfunction formatContextPath(p: string, cwd: string): string {\n\tconst resolvedCwd = path.resolve(cwd);\n\tconst absolutePath = path.isAbsolute(p) ? path.resolve(p) : path.resolve(resolvedCwd, p);\n\tconst relativePath = getCwdRelativePath(absolutePath, resolvedCwd);\n\tif (relativePath !== undefined) {\n\t\treturn relativePath;\n\t}\n\n\treturn formatDisplayPath(absolutePath);\n}\n\n/**\n * Inline cost note for a context file: these are injected into the system prompt\n * on every turn, so an oversized one is priced where it is listed instead of in\n * a separate warning line.\n */\nfunction contextSizeNote(file: ContextFile): string {\n\tif (!file.size) return \"\";\n\tconst advice = file.size === \"truncated\" ? \"truncated\" : \"consider trimming\";\n\tconst tokens = file.tokens === undefined ? undefined : `~${formatTokens(file.tokens)} tokens`;\n\treturn theme.fg(\"warning\", ` ${tokens ? `${tokens} ${SEGMENT_SEP} ` : \"\"}${advice}`);\n}\n\n/**\n * Get a short path relative to the package root for display.\n */\nfunction getShortPath(fullPath: string, sourceInfo?: SourceInfo): string {\n\tconst baseDir = sourceInfo?.baseDir;\n\tif (baseDir && isPackageSource(sourceInfo)) {\n\t\tconst relativePath = path.relative(path.resolve(baseDir), path.resolve(fullPath));\n\t\tif (\n\t\t\trelativePath &&\n\t\t\trelativePath !== \".\" &&\n\t\t\t!relativePath.startsWith(\"..\") &&\n\t\t\t!relativePath.startsWith(`..${path.sep}`) &&\n\t\t\t!path.isAbsolute(relativePath)\n\t\t) {\n\t\t\treturn relativePath.replace(/\\\\/g, \"/\");\n\t\t}\n\t}\n\n\tconst source = sourceInfo?.source ?? \"\";\n\tconst npmMatch = fullPath.match(/node_modules\\/(@?[^/]+(?:\\/[^/]+)?)\\/(.*)/);\n\tif (npmMatch && source.startsWith(\"npm:\")) {\n\t\treturn npmMatch[2];\n\t}\n\n\tconst gitMatch = fullPath.match(/git\\/[^/]+\\/[^/]+\\/(.*)/);\n\tif (gitMatch && source.startsWith(\"git:\")) {\n\t\treturn gitMatch[1];\n\t}\n\n\treturn formatDisplayPath(fullPath);\n}\n\nfunction getCompactPathLabel(resourcePath: string, sourceInfo?: SourceInfo): string {\n\tconst shortPath = getShortPath(resourcePath, sourceInfo);\n\tconst normalizedPath = shortPath.replace(/\\\\/g, \"/\");\n\tconst segments = normalizedPath.split(\"/\").filter((segment) => segment.length > 0 && segment !== \"~\");\n\tif (segments.length > 0) {\n\t\treturn segments[segments.length - 1]!;\n\t}\n\treturn shortPath;\n}\n\nfunction getCompactPackageSourceLabel(sourceInfo?: SourceInfo): string {\n\tconst source = sourceInfo?.source ?? \"\";\n\tif (source.startsWith(\"npm:\")) {\n\t\treturn source.slice(\"npm:\".length) || source;\n\t}\n\n\tconst gitSource = parseGitUrl(source);\n\tif (gitSource) {\n\t\treturn gitSource.path || source;\n\t}\n\n\treturn source;\n}\n\nfunction getCompactExtensionLabel(resourcePath: string, sourceInfo?: SourceInfo): string {\n\tif (!isPackageSource(sourceInfo)) {\n\t\treturn getCompactPathLabel(resourcePath, sourceInfo);\n\t}\n\n\tconst sourceLabel = getCompactPackageSourceLabel(sourceInfo);\n\tif (!sourceLabel) {\n\t\treturn getCompactPathLabel(resourcePath, sourceInfo);\n\t}\n\n\tconst shortPath = getShortPath(resourcePath, sourceInfo).replace(/\\\\/g, \"/\");\n\tconst packagePath = shortPath.startsWith(\"extensions/\") ? shortPath.slice(\"extensions/\".length) : shortPath;\n\tconst parsedPath = path.posix.parse(packagePath);\n\n\tif (parsedPath.name === \"index\") {\n\t\treturn !parsedPath.dir || parsedPath.dir === \".\" ? sourceLabel : `${sourceLabel}:${parsedPath.dir}`;\n\t}\n\n\treturn `${sourceLabel}:${packagePath}`;\n}\n\nfunction getCompactDisplayPathSegments(resourcePath: string): string[] {\n\treturn formatDisplayPath(resourcePath)\n\t\t.replace(/\\\\/g, \"/\")\n\t\t.split(\"/\")\n\t\t.filter((segment) => segment.length > 0 && segment !== \"~\");\n}\n\nfunction getCompactNonPackageExtensionLabel(\n\tresourcePath: string,\n\tindex: number,\n\tallPaths: Array<{ path: string; segments: string[] }>,\n): string {\n\tconst segments = allPaths[index]?.segments;\n\tif (!segments || segments.length === 0) {\n\t\treturn getCompactPathLabel(resourcePath);\n\t}\n\n\tfor (let segmentCount = 1; segmentCount <= segments.length; segmentCount += 1) {\n\t\tconst candidate = segments.slice(-segmentCount).join(\"/\");\n\t\tconst isUnique = allPaths.every((item, itemIndex) => {\n\t\t\tif (itemIndex === index) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn item.segments.slice(-segmentCount).join(\"/\") !== candidate;\n\t\t});\n\n\t\tif (isUnique) {\n\t\t\treturn candidate;\n\t\t}\n\t}\n\n\treturn segments.join(\"/\");\n}\n\nfunction getCompactExtensionLabels(\n\textensions: Array<{ path: string; sourceInfo?: SourceInfo; displayName?: string }>,\n): string[] {\n\tconst nonPackageExtensions = extensions\n\t\t.map((extension) => {\n\t\t\tconst segments = getCompactDisplayPathSegments(extension.path);\n\t\t\tconst lastSegment = segments[segments.length - 1];\n\t\t\tif (segments.length > 1 && (lastSegment === \"index.ts\" || lastSegment === \"index.js\")) {\n\t\t\t\tsegments.pop();\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tpath: extension.path,\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t\tsegments,\n\t\t\t};\n\t\t})\n\t\t.filter((extension) => !isPackageSource(extension.sourceInfo));\n\n\treturn extensions.map((extension) => {\n\t\tif (extension.displayName) {\n\t\t\treturn extension.displayName;\n\t\t}\n\n\t\tif (isPackageSource(extension.sourceInfo)) {\n\t\t\treturn getCompactExtensionLabel(extension.path, extension.sourceInfo);\n\t\t}\n\n\t\tconst nonPackageIndex = nonPackageExtensions.findIndex((item) => item.path === extension.path);\n\t\tif (nonPackageIndex === -1) {\n\t\t\treturn getCompactPathLabel(extension.path, extension.sourceInfo);\n\t\t}\n\n\t\treturn getCompactNonPackageExtensionLabel(extension.path, nonPackageIndex, nonPackageExtensions);\n\t});\n}\n\nfunction getDisplaySourceInfo(sourceInfo?: SourceInfo): {\n\tlabel: string;\n\tscopeLabel?: string;\n\tcolor: \"accent\" | \"muted\";\n} {\n\tconst source = sourceInfo?.source ?? \"local\";\n\tconst scope = sourceInfo?.scope ?? \"project\";\n\tif (source === \"local\") {\n\t\tif (scope === \"user\") {\n\t\t\treturn { label: \"user\", color: \"muted\" };\n\t\t}\n\t\tif (scope === \"project\") {\n\t\t\treturn { label: \"project\", color: \"muted\" };\n\t\t}\n\t\tif (scope === \"temporary\") {\n\t\t\treturn { label: \"path\", scopeLabel: \"temp\", color: \"muted\" };\n\t\t}\n\t\treturn { label: \"path\", color: \"muted\" };\n\t}\n\n\tif (source === \"cli\") {\n\t\treturn { label: \"path\", scopeLabel: scope === \"temporary\" ? \"temp\" : undefined, color: \"muted\" };\n\t}\n\n\tconst scopeLabel =\n\t\tscope === \"user\" ? \"user\" : scope === \"project\" ? \"project\" : scope === \"temporary\" ? \"temp\" : undefined;\n\treturn { label: source, scopeLabel, color: \"accent\" };\n}\n\nfunction getScopeGroup(sourceInfo?: SourceInfo): \"user\" | \"project\" | \"path\" {\n\tconst source = sourceInfo?.source ?? \"local\";\n\tconst scope = sourceInfo?.scope ?? \"project\";\n\tif (source === \"cli\" || scope === \"temporary\") return \"path\";\n\tif (scope === \"user\") return \"user\";\n\tif (scope === \"project\") return \"project\";\n\treturn \"path\";\n}\n\nfunction isPackageSource(sourceInfo?: SourceInfo): boolean {\n\tconst source = sourceInfo?.source ?? \"\";\n\treturn source.startsWith(\"npm:\") || source.startsWith(\"git:\");\n}\n\n// ============================================================================\n// Scope grouping and diagnostics\n// ============================================================================\n\ninterface ScopedItem {\n\tpath: string;\n\tsourceInfo?: SourceInfo;\n\tdisplayName?: string;\n}\n\ninterface ScopeGroup {\n\tscope: \"user\" | \"project\" | \"path\";\n\tpaths: ScopedItem[];\n\tpackages: Map<string, ScopedItem[]>;\n}\n\nfunction buildScopeGroups(items: ScopedItem[]): ScopeGroup[] {\n\tconst groups: Record<\"user\" | \"project\" | \"path\", ScopeGroup> = {\n\t\tuser: { scope: \"user\", paths: [], packages: new Map() },\n\t\tproject: { scope: \"project\", paths: [], packages: new Map() },\n\t\tpath: { scope: \"path\", paths: [], packages: new Map() },\n\t};\n\n\tfor (const item of items) {\n\t\tconst groupKey = getScopeGroup(item.sourceInfo);\n\t\tconst group = groups[groupKey];\n\t\tconst source = item.sourceInfo?.source ?? \"local\";\n\n\t\tif (isPackageSource(item.sourceInfo)) {\n\t\t\tconst list = group.packages.get(source) ?? [];\n\t\t\tlist.push(item);\n\t\t\tgroup.packages.set(source, list);\n\t\t} else {\n\t\t\tgroup.paths.push(item);\n\t\t}\n\t}\n\n\treturn [groups.project, groups.user, groups.path].filter(\n\t\t(group) => group.paths.length > 0 || group.packages.size > 0,\n\t);\n}\n\nfunction formatScopeGroups(\n\tgroups: ScopeGroup[],\n\toptions: {\n\t\tformatPath: (item: ScopedItem) => string;\n\t\tformatPackagePath: (item: ScopedItem, source: string) => string;\n\t},\n): string {\n\tconst lines: string[] = [];\n\n\tfor (const group of groups) {\n\t\tlines.push(`  ${theme.fg(\"accent\", group.scope)}`);\n\n\t\tconst sortedPaths = [...group.paths].sort((a, b) => a.path.localeCompare(b.path));\n\t\tfor (const item of sortedPaths) {\n\t\t\tlines.push(theme.fg(\"dim\", `    ${options.formatPath(item)}`));\n\t\t}\n\n\t\tconst sortedPackages = Array.from(group.packages.entries()).sort(([a], [b]) => a.localeCompare(b));\n\t\tfor (const [source, items] of sortedPackages) {\n\t\t\tlines.push(`    ${theme.fg(\"mdLink\", source)}`);\n\t\t\tconst sortedPackagePaths = [...items].sort((a, b) => a.path.localeCompare(b.path));\n\t\t\tfor (const item of sortedPackagePaths) {\n\t\t\t\tlines.push(theme.fg(\"dim\", `      ${options.formatPackagePath(item, source)}`));\n\t\t\t}\n\t\t}\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n\nfunction findSourceInfoForPath(p: string, sourceInfos: Map<string, SourceInfo>): SourceInfo | undefined {\n\tconst exact = sourceInfos.get(p);\n\tif (exact) return exact;\n\n\tlet current = p;\n\twhile (current.includes(\"/\")) {\n\t\tcurrent = current.substring(0, current.lastIndexOf(\"/\"));\n\t\tconst parent = sourceInfos.get(current);\n\t\tif (parent) return parent;\n\t}\n\n\treturn undefined;\n}\n\nfunction formatPathWithSource(p: string, sourceInfo?: SourceInfo): string {\n\tif (sourceInfo) {\n\t\tconst shortPath = getShortPath(p, sourceInfo);\n\t\tconst { label, scopeLabel } = getDisplaySourceInfo(sourceInfo);\n\t\tconst labelText = scopeLabel ? `${label} (${scopeLabel})` : label;\n\t\treturn `${labelText} ${shortPath}`;\n\t}\n\treturn formatDisplayPath(p);\n}\n\nfunction formatDiagnostics(diagnostics: readonly ResourceDiagnostic[], sourceInfos: Map<string, SourceInfo>): string {\n\tconst lines: string[] = [];\n\n\t// Group collision diagnostics by name\n\tconst collisions = new Map<string, ResourceDiagnostic[]>();\n\tconst otherDiagnostics: ResourceDiagnostic[] = [];\n\n\tfor (const d of diagnostics) {\n\t\tif (d.type === \"collision\" && d.collision) {\n\t\t\tconst list = collisions.get(d.collision.name) ?? [];\n\t\t\tlist.push(d);\n\t\t\tcollisions.set(d.collision.name, list);\n\t\t} else {\n\t\t\totherDiagnostics.push(d);\n\t\t}\n\t}\n\n\t// Format collision diagnostics grouped by name\n\tfor (const [name, collisionList] of collisions) {\n\t\tconst first = collisionList[0]?.collision;\n\t\tif (!first) continue;\n\t\tlines.push(theme.fg(\"warning\", `  \"${name}\" collision:`));\n\t\tlines.push(\n\t\t\ttheme.fg(\n\t\t\t\t\"dim\",\n\t\t\t\t`    ${theme.fg(\"success\", \"✓\")} ${formatPathWithSource(first.winnerPath, findSourceInfoForPath(first.winnerPath, sourceInfos))}`,\n\t\t\t),\n\t\t);\n\t\tfor (const d of collisionList) {\n\t\t\tif (d.collision) {\n\t\t\t\tlines.push(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"dim\",\n\t\t\t\t\t\t`    ${theme.fg(\"warning\", \"✗\")} ${formatPathWithSource(d.collision.loserPath, findSourceInfoForPath(d.collision.loserPath, sourceInfos))} (skipped)`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (const d of otherDiagnostics) {\n\t\tif (d.path) {\n\t\t\tconst formattedPath = formatPathWithSource(d.path, findSourceInfoForPath(d.path, sourceInfos));\n\t\t\tlines.push(theme.fg(d.type === \"error\" ? \"error\" : \"warning\", `  ${formattedPath}`));\n\t\t\tlines.push(theme.fg(d.type === \"error\" ? \"error\" : \"warning\", `    ${d.message}`));\n\t\t} else {\n\t\t\tlines.push(theme.fg(d.type === \"error\" ? \"error\" : \"warning\", `  ${d.message}`));\n\t\t}\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n\n// ============================================================================\n// Loaded-resources listing\n// ============================================================================\n\n/** The slice of the interactive mode the resource listing renders into. */\nexport interface ResourceDisplayDeps {\n\tchatContainer: Container;\n\tgetCwd(): string;\n\tgetResourceLoader(): ResourceLoader;\n\tgetPromptTemplates(): ReadonlyArray<PromptTemplate>;\n\tgetExtensionRunner(): ExtensionRunner;\n\tgetActiveMode(): string;\n\tgetSubagentEnabled(): boolean;\n\t/** Dispatchable subagents in this cwd (0 when the Task tool is off). */\n\tgetAgentCount(): number;\n\t/** Dispatchable agents for this cwd (empty when Task tool is off). */\n\tgetAgents(): AgentDefinition[];\n\t/**\n\t * Canvases that could be opened here (`/canvas`). Listing one is free — it\n\t * reads directory entries and never forks — which is why they can appear in a\n\t * startup summary at all, and why a withheld one is reported rather than\n\t * hidden: the point of the trust gate is that a person can see the offer.\n\t */\n\tgetCanvases(): Array<{ id: string; scope: string; withheld: boolean }>;\n\t/** Terminal width, used to keep one-line-per-item sections to one line. */\n\tgetColumns?(): number | undefined;\n\tquietStartup(): boolean;\n\tverbose: boolean;\n\t/** Startup expansion state for the collapsible sections. */\n\tisExpanded(): boolean;\n\tgetBuiltInCommandConflictDiagnostics(extensionRunner: ExtensionRunner): ResourceDiagnostic[];\n}\n\n/**\n * Whether a `showLoadedResources` call renders the full summary rather than\n * diagnostics only. Exported so callers can skip preparing summary-only data\n * (and claiming once-per-session notices) on a quiet startup.\n */\nexport function willShowResourceListing(\n\tdeps: Pick<ResourceDisplayDeps, \"verbose\" | \"quietStartup\">,\n\toptions?: { force?: boolean },\n): boolean {\n\treturn options?.force === true || deps.verbose || !deps.quietStartup();\n}\n\nexport function showLoadedResources(\n\tdeps: ResourceDisplayDeps,\n\toptions?: {\n\t\textensions?: Array<{ path: string; sourceInfo?: SourceInfo }>;\n\t\tforce?: boolean;\n\t\tshowDiagnosticsWhenQuiet?: boolean;\n\t},\n): void {\n\tconst showListing = willShowResourceListing(deps, options);\n\tconst showDiagnostics = showListing || options?.showDiagnosticsWhenQuiet === true;\n\tif (!showListing && !showDiagnostics) {\n\t\treturn;\n\t}\n\n\tconst resourceLoader = deps.getResourceLoader();\n\tconst chatContainer = deps.chatContainer;\n\n\tconst sectionHeader = (name: string, color: ThemeColor = \"mdHeading\") => theme.fg(color, `[${name}]`);\n\tconst formatCompactList = (items: string[], listOptions?: { sort?: boolean }): string => {\n\t\tconst labels = items.map((item) => item.trim()).filter((item) => item.length > 0);\n\t\tif (listOptions?.sort !== false) {\n\t\t\tlabels.sort((a, b) => a.localeCompare(b));\n\t\t}\n\t\treturn theme.fg(\"dim\", `  ${labels.join(\", \")}`);\n\t};\n\n\tconst skillsResult = resourceLoader.getSkills();\n\tconst promptsResult = resourceLoader.getPrompts();\n\tconst themesResult = resourceLoader.getThemes();\n\tconst extensions =\n\t\toptions?.extensions ??\n\t\tresourceLoader\n\t\t\t.getExtensions()\n\t\t\t.extensions.filter((extension) => !extension.internal)\n\t\t\t.map((extension) => ({\n\t\t\t\tpath: extension.path,\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t\tdisplayName: extension.displayName,\n\t\t\t}));\n\tconst sourceInfos = new Map<string, SourceInfo>();\n\tfor (const extension of extensions) {\n\t\tif (extension.sourceInfo) {\n\t\t\tsourceInfos.set(extension.path, extension.sourceInfo);\n\t\t}\n\t}\n\tfor (const skill of skillsResult.skills) {\n\t\tif (skill.sourceInfo) {\n\t\t\tsourceInfos.set(skill.filePath, skill.sourceInfo);\n\t\t}\n\t}\n\tfor (const prompt of promptsResult.prompts) {\n\t\tif (prompt.sourceInfo) {\n\t\t\tsourceInfos.set(prompt.filePath, prompt.sourceInfo);\n\t\t}\n\t}\n\tfor (const loadedTheme of themesResult.themes) {\n\t\tif (loadedTheme.sourcePath && loadedTheme.sourceInfo) {\n\t\t\tsourceInfos.set(loadedTheme.sourcePath, loadedTheme.sourceInfo);\n\t\t}\n\t}\n\n\tif (showListing) {\n\t\tchatContainer.addChild(new Spacer(1));\n\n\t\tconst { agentsFiles: contextFiles, warnings: contextWarnings } = resourceLoader.getAgentsFiles();\n\t\tconst skills = skillsResult.skills;\n\t\tconst templates = deps.getPromptTemplates();\n\t\tconst loadedThemes = themesResult.themes;\n\t\tconst customThemes = loadedThemes.filter((t) => t.sourcePath);\n\t\tconst agentCount = deps.getAgentCount();\n\t\tconst canvases = deps.getCanvases();\n\t\t// Live servers, whatever their source (mcp.json, per-server files, plugins) —\n\t\t// the loader records each connect outcome, so this is what the session can\n\t\t// actually call, not what was merely configured.\n\t\tconst mcpStatuses = getMcpServerStatuses();\n\t\t// Plugins register their synthetic extension factory as `plugin:<id>`;\n\t\t// split them out so plugins and code extensions each get their own count.\n\t\t// (displayName is absent from the options-provided extension shape, so read\n\t\t// it structurally.)\n\t\tconst displayNameOf = (e: object): string | undefined => {\n\t\t\tconst value = (e as { displayName?: unknown }).displayName;\n\t\t\treturn typeof value === \"string\" ? value : undefined;\n\t\t};\n\t\tconst pluginCount = extensions.filter((e) => displayNameOf(e)?.startsWith(\"plugin:\")).length;\n\t\tconst codeExtensionCount = extensions.length - pluginCount;\n\n\t\t// ── Counted capability grid ──────────────────────────────────────────────\n\t\t// One cell per loaded capability class (glyph + count + label), so \"what\n\t\t// can this session do\" reads at a glance; names/paths sit one keypress\n\t\t// away in the details below. Cells pad to a common width, four per row.\n\t\tconst plural = (n: number, s: string, many?: string) => (n === 1 ? s : (many ?? `${s}s`));\n\t\tconst cells: Array<{ key: CategoryKey; count: number; label: string }> = [];\n\t\tif (skills.length > 0) cells.push({ key: \"skills\", count: skills.length, label: plural(skills.length, \"skill\") });\n\t\tif (templates.length > 0) {\n\t\t\tcells.push({ key: \"commands\", count: templates.length, label: plural(templates.length, \"command\") });\n\t\t}\n\t\tif (agentCount > 0) {\n\t\t\tcells.push({ key: \"agents\", count: agentCount, label: plural(agentCount, \"agent\") });\n\t\t}\n\t\tif (mcpStatuses.length > 0) {\n\t\t\tcells.push({ key: \"mcp\", count: mcpStatuses.length, label: plural(mcpStatuses.length, \"mcp server\") });\n\t\t}\n\t\tif (pluginCount > 0) cells.push({ key: \"plugins\", count: pluginCount, label: plural(pluginCount, \"plugin\") });\n\t\tif (codeExtensionCount > 0) {\n\t\t\tcells.push({\n\t\t\t\tkey: \"extensions\",\n\t\t\t\tcount: codeExtensionCount,\n\t\t\t\tlabel: plural(codeExtensionCount, \"extension\"),\n\t\t\t});\n\t\t}\n\t\tif (canvases.length > 0) {\n\t\t\tcells.push({ key: \"canvases\", count: canvases.length, label: plural(canvases.length, \"canvas\", \"canvases\") });\n\t\t}\n\t\tif (customThemes.length > 0) {\n\t\t\tcells.push({ key: \"themes\", count: customThemes.length, label: plural(customThemes.length, \"theme\") });\n\t\t}\n\n\t\tif (cells.length > 0) {\n\t\t\tconst cellPlain = (c: (typeof cells)[number]) => `${CATEGORY_GLYPH[c.key]} ${c.count} ${c.label}`;\n\t\t\tconst cellWidth = Math.max(...cells.map((c) => visibleWidth(cellPlain(c)))) + 3;\n\t\t\tconst styledCell = (c: (typeof cells)[number]) => {\n\t\t\t\tconst pad = \" \".repeat(Math.max(0, cellWidth - visibleWidth(cellPlain(c))));\n\t\t\t\treturn (\n\t\t\t\t\t`${theme.fg(\"accent\", CATEGORY_GLYPH[c.key])} ` +\n\t\t\t\t\t`${theme.bold(String(c.count))} ${theme.fg(\"muted\", c.label)}${pad}`\n\t\t\t\t);\n\t\t\t};\n\t\t\tconst PER_ROW = 4;\n\t\t\tconst rows: string[] = [];\n\t\t\tfor (let i = 0; i < cells.length; i += PER_ROW) {\n\t\t\t\trows.push(\n\t\t\t\t\t`${RAIL}${cells\n\t\t\t\t\t\t.slice(i, i + PER_ROW)\n\t\t\t\t\t\t.map(styledCell)\n\t\t\t\t\t\t.join(\"\")}`.trimEnd(),\n\t\t\t\t);\n\t\t\t}\n\t\t\tchatContainer.addChild(new Text(rows.join(\"\\n\"), 0, 0));\n\t\t} else {\n\t\t\tchatContainer.addChild(new Text(theme.fg(\"dim\", `${RAIL}no project resources loaded`), 0, 0));\n\t\t}\n\n\t\t// Context files stay visible in the summary — they shape every reply. Their\n\t\t// recurring cost is annotated in place rather than repeated as a warning\n\t\t// line below, so the file and its price stay in one spot.\n\t\tif (contextFiles.length > 0) {\n\t\t\tconst names = contextFiles\n\t\t\t\t.map((f) => formatContextPath(f.path, deps.getCwd()) + contextSizeNote(f))\n\t\t\t\t.join(\", \");\n\t\t\t// The per-file note only appears once a file is oversized, which leaves\n\t\t\t// the cost of several small files invisible. The total is what is\n\t\t\t// actually re-sent every turn, so it is always shown.\n\t\t\tconst totalTokens = contextFiles.reduce((sum, f) => sum + (f.tokens ?? 0), 0);\n\t\t\tconst total =\n\t\t\t\ttotalTokens > 0 ? theme.fg(\"muted\", ` ${SEGMENT_SEP} ~${formatTokens(totalTokens)} tokens/turn`) : \"\";\n\t\t\tchatContainer.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\t`${RAIL}${theme.fg(\"accent\", CATEGORY_GLYPH.context)} ${theme.fg(\"muted\", \"context\")} ${names}${total}`,\n\t\t\t\t\t0,\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\t// ── Details: names + scope-grouped paths, one keypress away ─────────────\n\t\t// A single expandable holds every per-category listing; collapsed it is\n\t\t// just the hint line, so a resource-heavy project no longer buries the\n\t\t// prompt under a wall of names at launch.\n\t\tconst detailSections: string[] = [];\n\t\t// Skills\n\t\tif (skills.length > 0) {\n\t\t\tconst groups = buildScopeGroups(\n\t\t\t\tskills.map((skill) => ({ path: skill.filePath, sourceInfo: skill.sourceInfo })),\n\t\t\t);\n\t\t\tconst skillList = formatScopeGroups(groups, {\n\t\t\t\tformatPath: (item) => formatDisplayPath(item.path),\n\t\t\t\tformatPackagePath: (item) => getShortPath(item.path, item.sourceInfo),\n\t\t\t});\n\t\t\tdetailSections.push(\n\t\t\t\t`${sectionHeader(\"Skills\")}\\n${formatCompactList(skills.map((skill) => skill.name))}\\n${skillList}`,\n\t\t\t);\n\t\t}\n\t\tif (templates.length > 0) {\n\t\t\tconst groups = buildScopeGroups(\n\t\t\t\ttemplates.map((template) => ({ path: template.filePath, sourceInfo: template.sourceInfo })),\n\t\t\t);\n\t\t\tconst templateByPath = new Map(templates.map((t) => [t.filePath, t]));\n\t\t\tconst formatTemplate = (item: { path: string }) => {\n\t\t\t\tconst template = templateByPath.get(item.path);\n\t\t\t\treturn template ? `/${template.name}` : formatDisplayPath(item.path);\n\t\t\t};\n\t\t\tconst templateList = formatScopeGroups(groups, {\n\t\t\t\tformatPath: formatTemplate,\n\t\t\t\tformatPackagePath: formatTemplate,\n\t\t\t});\n\t\t\tdetailSections.push(\n\t\t\t\t`${sectionHeader(\"Commands\")}\\n${formatCompactList(templates.map((t) => `/${t.name}`))}\\n${templateList}`,\n\t\t\t);\n\t\t}\n\t\t// Agents\n\t\tconst agents = deps.getAgents();\n\t\tif (agents.length > 0) {\n\t\t\t// One line per agent, truncated to the terminal. `summarizeAgentDescription`\n\t\t\t// caps at 200 characters — about three wrapped lines here — and the TUI\n\t\t\t// wrapper has no hanging indent, so an untruncated summary turned each\n\t\t\t// agent into a paragraph whose continuations read as further agents.\n\t\t\tconst agentList = renderCompactRows(\n\t\t\t\tagents.map((agent) => ({ name: agent.name, detail: summarizeAgentDescription(agent.description) })),\n\t\t\t\t{\n\t\t\t\t\tcolumns: deps.getColumns?.(),\n\t\t\t\t\tstyle: { name: (text) => theme.fg(\"muted\", text), detail: (text) => theme.fg(\"dim\", text) },\n\t\t\t\t},\n\t\t\t);\n\t\t\tdetailSections.push(`${sectionHeader(\"Agents\")}\\n${agentList}`);\n\t\t}\n\t\t// MCP\n\t\tif (mcpStatuses.length > 0) {\n\t\t\tconst mcpList = mcpStatuses\n\t\t\t\t.map((server) => {\n\t\t\t\t\tconst facts =\n\t\t\t\t\t\tserver.state === \"authorizing\"\n\t\t\t\t\t\t\t? [\"awaiting authorization\"]\n\t\t\t\t\t\t\t: [\n\t\t\t\t\t\t\t\t\t`${server.toolCount} ${plural(server.toolCount, \"tool\")}`,\n\t\t\t\t\t\t\t\t\tserver.background ? \"background\" : \"foreground\",\n\t\t\t\t\t\t\t\t\t...(server.deferred ? [\"schemas deferred\"] : []),\n\t\t\t\t\t\t\t\t];\n\t\t\t\t\treturn theme.fg(\"dim\", `  ${server.name} ${facts.join(` ${SEGMENT_SEP} `)}`);\n\t\t\t\t})\n\t\t\t\t.join(\"\\n\");\n\t\t\tdetailSections.push(`${sectionHeader(\"MCP\")}\\n${mcpList}`);\n\t\t}\n\t\t// Plugins\n\t\tconst plugins = extensions.filter((e) => displayNameOf(e)?.startsWith(\"plugin:\"));\n\t\tif (plugins.length > 0) {\n\t\t\tconst groups = buildScopeGroups(plugins);\n\t\t\tconst pluginList = formatScopeGroups(groups, {\n\t\t\t\tformatPath: (item) => item.displayName ?? formatExtensionDisplayPath(item.path),\n\t\t\t\tformatPackagePath: (item) =>\n\t\t\t\t\titem.displayName ?? formatExtensionDisplayPath(getShortPath(item.path, item.sourceInfo)),\n\t\t\t});\n\t\t\tdetailSections.push(`${sectionHeader(\"Plugins\")}\\n${pluginList}`);\n\t\t}\n\t\tif (extensions.length > 0) {\n\t\t\tconst groups = buildScopeGroups(extensions);\n\t\t\tconst extList = formatScopeGroups(groups, {\n\t\t\t\tformatPath: (item) => item.displayName ?? formatExtensionDisplayPath(item.path),\n\t\t\t\tformatPackagePath: (item) =>\n\t\t\t\t\titem.displayName ?? formatExtensionDisplayPath(getShortPath(item.path, item.sourceInfo)),\n\t\t\t});\n\t\t\tdetailSections.push(\n\t\t\t\t`${sectionHeader(\"Extensions\")}\\n${formatCompactList(getCompactExtensionLabels(extensions))}\\n${extList}`,\n\t\t\t);\n\t\t}\n\t\tif (customThemes.length > 0) {\n\t\t\tconst groups = buildScopeGroups(\n\t\t\t\tcustomThemes.map((loadedTheme) => ({\n\t\t\t\t\tpath: loadedTheme.sourcePath!,\n\t\t\t\t\tsourceInfo: loadedTheme.sourceInfo,\n\t\t\t\t})),\n\t\t\t);\n\t\t\tconst themeList = formatScopeGroups(groups, {\n\t\t\t\tformatPath: (item) => formatDisplayPath(item.path),\n\t\t\t\tformatPackagePath: (item) => getShortPath(item.path, item.sourceInfo),\n\t\t\t});\n\t\t\tdetailSections.push(`${sectionHeader(\"Themes\")}\\n${themeList}`);\n\t\t}\n\t\t// Canvases last: they are the one capability nothing here has *loaded* —\n\t\t// each is a process waiting to be asked for, so the section says how to ask\n\t\t// rather than where the file is.\n\t\tif (canvases.length > 0) {\n\t\t\tconst canvasList = renderCompactRows(\n\t\t\t\tcanvases.map((canvas) => ({\n\t\t\t\t\tname: canvas.id,\n\t\t\t\t\tdetail: canvas.withheld\n\t\t\t\t\t\t? \"withheld: untrusted workspace — /plugin trust\"\n\t\t\t\t\t\t: `${canvas.scope} · /canvas open ${canvas.id}`,\n\t\t\t\t})),\n\t\t\t\t{\n\t\t\t\t\tcolumns: deps.getColumns?.(),\n\t\t\t\t\tstyle: { name: (text) => theme.fg(\"muted\", text), detail: (text) => theme.fg(\"dim\", text) },\n\t\t\t\t},\n\t\t\t);\n\t\t\tdetailSections.push(`${sectionHeader(\"Canvases\")}\\n${canvasList}`);\n\t\t}\n\n\t\tif (detailSections.length > 0) {\n\t\t\t// No collapsed hint: the banner already carries the one expand hint, and\n\t\t\t// the same key opens both. Text renders empty content as zero lines, so\n\t\t\t// the collapsed block costs nothing.\n\t\t\tchatContainer.addChild(\n\t\t\t\tnew ExpandableText(\n\t\t\t\t\t() => \"\",\n\t\t\t\t\t() => detailSections.join(\"\\n\\n\"),\n\t\t\t\t\tdeps.isExpanded(),\n\t\t\t\t\t0,\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\t// Remaining context warnings are failures (unreadable files), not sizing\n\t\t// advice — size is reported inline on the context row above.\n\t\tif (contextWarnings.length > 0) {\n\t\t\tfor (const warning of contextWarnings) {\n\t\t\t\tchatContainer.addChild(new Text(theme.fg(\"warning\", `${RAIL}${warning}`), 0, 0));\n\t\t\t}\n\t\t}\n\t}\n\n\tif (showDiagnostics) {\n\t\tconst skillDiagnostics = skillsResult.diagnostics;\n\t\tif (skillDiagnostics.length > 0) {\n\t\t\tconst warningLines = formatDiagnostics(skillDiagnostics, sourceInfos);\n\t\t\tchatContainer.addChild(new Text(`${theme.fg(\"warning\", \"[Skill conflicts]\")}\\n${warningLines}`, 0, 0));\n\t\t\tchatContainer.addChild(new Spacer(1));\n\t\t}\n\n\t\tconst promptDiagnostics = promptsResult.diagnostics;\n\t\tif (promptDiagnostics.length > 0) {\n\t\t\tconst warningLines = formatDiagnostics(promptDiagnostics, sourceInfos);\n\t\t\tchatContainer.addChild(new Text(`${theme.fg(\"warning\", \"[Prompt conflicts]\")}\\n${warningLines}`, 0, 0));\n\t\t\tchatContainer.addChild(new Spacer(1));\n\t\t}\n\n\t\tconst extensionDiagnostics: ResourceDiagnostic[] = [];\n\t\tconst extensionErrors = resourceLoader.getExtensions().errors;\n\t\tif (extensionErrors.length > 0) {\n\t\t\tfor (const error of extensionErrors) {\n\t\t\t\textensionDiagnostics.push({\n\t\t\t\t\ttype: error.severity ?? \"error\",\n\t\t\t\t\tmessage: error.error,\n\t\t\t\t\tpath: error.path,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tconst extensionRunner = deps.getExtensionRunner();\n\t\tconst commandDiagnostics = extensionRunner.getCommandDiagnostics();\n\t\textensionDiagnostics.push(...commandDiagnostics);\n\t\textensionDiagnostics.push(...deps.getBuiltInCommandConflictDiagnostics(extensionRunner));\n\n\t\tconst shortcutDiagnostics = extensionRunner.getShortcutDiagnostics();\n\t\textensionDiagnostics.push(...shortcutDiagnostics);\n\n\t\tif (extensionDiagnostics.length > 0) {\n\t\t\tconst warningLines = formatDiagnostics(extensionDiagnostics, sourceInfos);\n\t\t\tchatContainer.addChild(new Text(`${theme.fg(\"warning\", \"[Extension issues]\")}\\n${warningLines}`, 0, 0));\n\t\t\tchatContainer.addChild(new Spacer(1));\n\t\t}\n\n\t\tconst themeDiagnostics = themesResult.diagnostics;\n\t\tif (themeDiagnostics.length > 0) {\n\t\t\tconst warningLines = formatDiagnostics(themeDiagnostics, sourceInfos);\n\t\t\tchatContainer.addChild(new Text(`${theme.fg(\"warning\", \"[Theme conflicts]\")}\\n${warningLines}`, 0, 0));\n\t\t\tchatContainer.addChild(new Spacer(1));\n\t\t}\n\t}\n}\n"]}