@selesai/code 0.13.26 → 0.13.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/cli/args.js +1 -0
  3. package/dist/cli/session-picker.d.ts +1 -1
  4. package/dist/config.js +1 -1
  5. package/dist/core/agent-session-auto-handoff.test.js +2 -1
  6. package/dist/core/agent-session-length-continuation.test.js +2 -1
  7. package/dist/core/agent-session.d.ts +58 -7
  8. package/dist/core/agent-session.js +334 -167
  9. package/dist/core/bug-report-upload.d.ts +11 -0
  10. package/dist/core/bug-report-upload.js +21 -0
  11. package/dist/core/bug-report.d.ts +175 -0
  12. package/dist/core/bug-report.js +291 -0
  13. package/dist/core/cache-stats.js +12 -1
  14. package/dist/core/cache-warmer.d.ts +101 -0
  15. package/dist/core/cache-warmer.js +341 -0
  16. package/dist/core/compaction/branch-summarization.js +2 -2
  17. package/dist/core/compaction/compaction.d.ts +6 -5
  18. package/dist/core/compaction/compaction.js +49 -42
  19. package/dist/core/crash-log.d.ts +21 -0
  20. package/dist/core/crash-log.js +70 -0
  21. package/dist/core/experimental.d.ts +0 -4
  22. package/dist/core/experimental.js +0 -4
  23. package/dist/core/extensions/index.d.ts +1 -1
  24. package/dist/core/extensions/jiti-loader.d.ts +1 -0
  25. package/dist/core/extensions/jiti-loader.js +3 -0
  26. package/dist/core/extensions/jiti-static-loader.d.ts +1 -0
  27. package/dist/core/extensions/jiti-static-loader.js +3 -0
  28. package/dist/core/extensions/loader.js +39 -49
  29. package/dist/core/extensions/runner.d.ts +10 -7
  30. package/dist/core/extensions/runner.js +85 -67
  31. package/dist/core/extensions/types.d.ts +66 -52
  32. package/dist/core/extensions/virtual-modules.d.ts +2 -0
  33. package/dist/core/extensions/virtual-modules.js +39 -0
  34. package/dist/core/extensions/wrapper.js +1 -20
  35. package/dist/core/index.d.ts +2 -1
  36. package/dist/core/keybindings.d.ts +1 -1
  37. package/dist/core/keybindings.js +1 -1
  38. package/dist/core/messages.js +1 -0
  39. package/dist/core/model-config.d.ts +101 -20
  40. package/dist/core/model-config.js +25 -18
  41. package/dist/core/model-resolver.js +2 -1
  42. package/dist/core/model-runtime.js +5 -3
  43. package/dist/core/provider-composer.d.ts +7 -2
  44. package/dist/core/provider-composer.js +10 -5
  45. package/dist/core/radius.d.ts +3 -0
  46. package/dist/core/radius.js +6 -0
  47. package/dist/core/sdk.js +60 -38
  48. package/dist/core/session-export.d.ts +6 -2
  49. package/dist/core/session-export.js +14 -13
  50. package/dist/core/session-format.test.d.ts +1 -0
  51. package/dist/core/session-format.test.js +96 -0
  52. package/dist/core/session-manager.d.ts +29 -6
  53. package/dist/core/session-manager.js +159 -96
  54. package/dist/core/settings-manager.d.ts +22 -5
  55. package/dist/core/settings-manager.js +56 -23
  56. package/dist/core/slash-commands.js +1 -0
  57. package/dist/core/system-prompt.d.ts +49 -4
  58. package/dist/core/system-prompt.js +155 -102
  59. package/dist/core/system-prompt.test.js +61 -23
  60. package/dist/core/tools/bash.d.ts +2 -1
  61. package/dist/core/tools/bash.js +10 -4
  62. package/dist/core/tools/edit.js +1 -2
  63. package/dist/core/tools/read.js +1 -2
  64. package/dist/core/tools/renderers/bash.js +9 -1
  65. package/dist/core/tools/write.js +1 -2
  66. package/dist/core/usage-totals.d.ts +1 -1
  67. package/dist/core/usage-totals.js +5 -1
  68. package/dist/extensions/pi-web-agent/src/backends/factory.ts +14 -6
  69. package/dist/extensions/pi-web-agent/src/extension.ts +3 -1
  70. package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +7 -3
  71. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +3 -0
  72. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +2 -0
  73. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +4 -2
  74. package/dist/extensions/pi-web-agent/src/search/tokenin.ts +4 -1
  75. package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +2 -1
  76. package/dist/extensions/pi-web-agent/src/types.ts +4 -0
  77. package/dist/index.d.ts +3 -2
  78. package/dist/main.js +13 -9
  79. package/dist/migrations.js +2 -2
  80. package/dist/modes/interactive/bug-report.d.ts +15 -0
  81. package/dist/modes/interactive/bug-report.js +205 -0
  82. package/dist/modes/interactive/chat-viewport.js +1 -1
  83. package/dist/modes/interactive/components/branch-summary-message.js +12 -5
  84. package/dist/modes/interactive/components/compaction-summary-message.js +12 -5
  85. package/dist/modes/interactive/components/custom-editor.d.ts +3 -3
  86. package/dist/modes/interactive/components/extension-editor.d.ts +4 -1
  87. package/dist/modes/interactive/components/extension-editor.js +7 -2
  88. package/dist/modes/interactive/components/extension-input.d.ts +2 -0
  89. package/dist/modes/interactive/components/extension-input.js +6 -0
  90. package/dist/modes/interactive/components/extension-selector.d.ts +1 -0
  91. package/dist/modes/interactive/components/extension-selector.js +4 -0
  92. package/dist/modes/interactive/components/footer.js +4 -1
  93. package/dist/modes/interactive/components/session-selector.d.ts +5 -5
  94. package/dist/modes/interactive/components/session-selector.js +74 -48
  95. package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
  96. package/dist/modes/interactive/components/settings-selector.js +11 -0
  97. package/dist/modes/interactive/components/skill-invocation-message.js +11 -4
  98. package/dist/modes/interactive/components/status-indicator.d.ts +2 -2
  99. package/dist/modes/interactive/components/status-indicator.js +7 -7
  100. package/dist/modes/interactive/components/tool-execution.js +18 -9
  101. package/dist/modes/interactive/components/tree-selector.js +2 -0
  102. package/dist/modes/interactive/interactive-mode.d.ts +15 -2
  103. package/dist/modes/interactive/interactive-mode.js +257 -81
  104. package/dist/modes/interactive/session-share.d.ts +2 -0
  105. package/dist/modes/interactive/session-share.js +8 -4
  106. package/dist/modes/interactive/tui-renderer.js +2 -2
  107. package/dist/modes/rpc/rpc-mode.js +2 -2
  108. package/dist/utils/clipboard-command.d.ts +6 -0
  109. package/dist/utils/clipboard-command.js +44 -0
  110. package/dist/utils/clipboard-image.js +55 -89
  111. package/dist/utils/clipboard.d.ts +1 -1
  112. package/dist/utils/clipboard.js +118 -108
  113. package/dist/utils/wsl.d.ts +2 -0
  114. package/dist/utils/wsl.js +14 -0
  115. package/dist/utils/zip.d.ts +6 -0
  116. package/dist/utils/zip.js +59 -0
  117. package/package.json +9 -9
  118. package/dist/utils/clipboard-native.d.ts +0 -10
  119. package/dist/utils/clipboard-native.js +0 -19
@@ -28,23 +28,27 @@ function joinReasons(reasons: string[]) {
28
28
  return `${reasons.slice(0, -1).join(', ')}, and ${reasons.at(-1)}`;
29
29
  }
30
30
 
31
- function caveatText(partial: boolean, caveatReasons: EvidenceCaveatReason[] = []) {
31
+ function caveatText(partial: boolean, caveatReasons: EvidenceCaveatReason[] = [], failureReason?: string) {
32
32
  if (!partial) return undefined;
33
33
  const specificReasons = caveatReasons.map(sentenceForReason);
34
34
  if (specificReasons.length > 0) {
35
35
  return `Evidence is partial: ${joinReasons(specificReasons)}.`;
36
36
  }
37
+ // With nothing to show, the budget line is a cover-up: say what actually failed.
38
+ if (failureReason) return `No usable evidence: ${failureReason}`;
37
39
  return 'Evidence is partial, so this answer is based on the strongest source found within the bounded research budget.';
38
40
  }
39
41
 
40
42
  export function synthesizeAnswer({
41
43
  evidence,
42
44
  partial,
43
- caveatReasons = []
45
+ caveatReasons = [],
46
+ failureReason
44
47
  }: {
45
48
  evidence: ResearchEvidence[];
46
49
  partial: boolean;
47
50
  caveatReasons?: EvidenceCaveatReason[];
51
+ failureReason?: string;
48
52
  }) {
49
53
  const findings = evidence.slice(0, 5).map((item) => {
50
54
  const summary = normalizeSummary(item.summary);
@@ -55,6 +59,6 @@ export function synthesizeAnswer({
55
59
 
56
60
  return {
57
61
  findings,
58
- caveat: caveatText(partial, caveatReasons)
62
+ caveat: caveatText(partial, caveatReasons, failureReason)
59
63
  };
60
64
  }
@@ -136,6 +136,9 @@ function buildMetadata({
136
136
  headlessAttempts,
137
137
  exhaustedBudget,
138
138
  caveatReasons,
139
+ // The first gap is why the run came up dry; it was collected and then thrown away, which
140
+ // is how a DNS-blocked DuckDuckGo surfaced as a contentless "No usable evidence found".
141
+ failureReason: allEvidence.length === 0 ? allGaps[0]?.message : undefined,
139
142
  fanoutProviders,
140
143
  fanoutSkipped
141
144
  };
@@ -48,6 +48,8 @@ export type ResearchRunMetadata = {
48
48
  fetchedPages: number;
49
49
  headlessAttempts: number;
50
50
  exhaustedBudget: boolean;
51
+ caveatReasons?: string[];
52
+ failureReason?: string;
51
53
  fanoutProviders?: SearchProviderName[];
52
54
  fanoutSkipped?: SearchProviderName[];
53
55
  };
@@ -34,7 +34,7 @@ export function buildExplorePresentation(result: WebExploreResponse): Presentati
34
34
  const hasEvidence = result.findings.length > 0 || result.sources.length > 0;
35
35
  const evidenceLines = hasEvidence
36
36
  ? result.findings.map((finding, index) => `- [${internalReaderLabel(result.sources[index]?.method)}] ${finding}`)
37
- : ['No usable evidence found.'];
37
+ : [result.metadata?.failureReason ? `No usable evidence: ${result.metadata.failureReason}` : 'No usable evidence found.'];
38
38
  const preview = [
39
39
  ...evidenceLines,
40
40
  internalSummary ? `\n${internalSummary}` : undefined
@@ -55,7 +55,9 @@ export function buildExplorePresentation(result: WebExploreResponse): Presentati
55
55
 
56
56
  const compact = hasEvidence
57
57
  ? `Reviewed ${result.sources.length} sources · synthesized answer with ${result.findings.length} findings`
58
- : 'No usable evidence found';
58
+ : result.metadata?.failureReason
59
+ ? `No usable evidence: ${result.metadata.failureReason}`
60
+ : 'No usable evidence found';
59
61
 
60
62
  return {
61
63
  mode: 'compact',
@@ -6,7 +6,10 @@ import type { SearchResult, WebSearchResponse } from '../types.js';
6
6
 
7
7
  export const TOKENIN_SEARCH_TOOL_NAME = 'firecrawl';
8
8
  export const TOKENIN_DEFAULT_BASE_URL = 'https://lite.andlet.me/v1';
9
- const TOKENIN_SEARCH_TIMEOUT_MS = 10_000;
9
+ // ponytail: measured 20-36s per query through the LiteLLM proxy, so the old 10s ceiling
10
+ // aborted every search and quietly punted to the fallback chain. Ceiling: a dead proxy now
11
+ // costs 45s before falling back. Lower it if your proxy is local; raise it if still timing out.
12
+ const TOKENIN_SEARCH_TIMEOUT_MS = 45_000;
10
13
 
11
14
  type TokenInAccount = { id: string; label: string; apiKey: string; baseUrl?: string };
12
15
  type TokenInAuth = { accounts: TokenInAccount[]; activeId: string | null };
@@ -52,7 +52,8 @@ export function createWebExploreTool({
52
52
  const synthesized = synthesizeAnswer({
53
53
  evidence: result.evidence,
54
54
  partial: result.decision.action !== 'answer',
55
- caveatReasons: result.metadata?.caveatReasons as EvidenceCaveatReason[] | undefined
55
+ caveatReasons: result.metadata?.caveatReasons as EvidenceCaveatReason[] | undefined,
56
+ failureReason: result.metadata?.failureReason
56
57
  });
57
58
 
58
59
  const shaped: WebExploreResponse = {
@@ -95,6 +95,10 @@ export type WebExploreResponse = {
95
95
  headlessAttempts: number;
96
96
  exhaustedBudget: boolean;
97
97
  caveatReasons?: string[];
98
+ // Why nothing came back, when nothing came back. Carried from the research pass's first
99
+ // gap so the empty result reads as "DuckDuckGo search request failed: fetch failed"
100
+ // instead of a bare "No usable evidence found".
101
+ failureReason?: string;
98
102
  fanoutProviders?: SearchProviderName[];
99
103
  fanoutSkipped?: SearchProviderName[];
100
104
  };
package/dist/index.d.ts CHANGED
@@ -2,10 +2,11 @@ export { type Args, parseArgs } from "./cli/args.ts";
2
2
  export { CONFIG_DIR_NAME, getAgentDir, getDocsPath, getModelsPath, getSettingsPath, getExamplesPath, getPackageDir, getReadmePath, VERSION, } from "./config.ts";
3
3
  export { AgentSession, type AgentSessionConfig, type AgentSessionEvent, type AgentSessionEventListener, type ModelCycleResult, type ParsedSkillBlock, type ParsedSkillBlocks, type PromptOptions, parseSkillBlock, parseSkillBlocks, type SessionStats, } from "./core/agent-session.ts";
4
4
  export { readStoredCredential } from "./core/auth-storage.ts";
5
+ export type { CacheWarmingDecision, CacheWarmingStatus } from "./core/cache-warmer.ts";
5
6
  export { ensureTool, getToolPath, type ManagedTool } from "./utils/tools-manager.ts";
6
7
  export { type BranchPreparation, type BranchSummaryResult, type CollectEntriesResult, type CompactionResult, type CutPointResult, calculateContextTokens, collectEntriesForBranchSummary, compact, DEFAULT_COMPACTION_SETTINGS, estimateTokens, type FileOperations, findCutPoint, findTurnStartIndex, type GenerateBranchSummaryOptions, generateBranchSummary, generateSummary, generateSummaryWithUsage, getLastAssistantUsage, prepareBranchEntries, serializeConversation, shouldCompact, } from "./core/compaction/index.ts";
7
8
  export { createEventBus, type EventBus, type EventBusController } from "./core/event-bus.ts";
8
- export type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppKeybinding, AutocompleteProviderFactory, BashToolCallEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderHeadersEvent, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, BuildSystemPromptOptions, CompactOptions, ContextEvent, ContextUsage, CustomToolCallEvent, EditToolCallEvent, EntryRenderer, EntryRenderOptions, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, GrepToolCallEvent, InlineExtension, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, MessageEndEvent, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, ProjectTrustContext, ProjectTrustEvent, ProjectTrustEventDecision, ProjectTrustEventResult, ProjectTrustHandler, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, RegisteredCommand, RegisteredTool, ResolvedCommand, ResolvedSkillInfo, SessionBeforeCompactEvent, SessionBeforeForkEvent, SessionBeforeSwitchEvent, SessionBeforeTreeEvent, SessionCompactEvent, SessionInfoChangedEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent, SlashCommandInfo, SlashCommandSource, SourceInfo, TerminalInputHandler, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionMode, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, TurnEndEvent, TurnStartEvent, UIPromptEndEvent, UIPromptKind, UIPromptStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WorkingIndicatorOptions, WriteToolCallEvent, } from "./core/extensions/index.ts";
9
+ export type { AfterProviderResponseEvent, AgentEndEvent, AgentSettledEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppKeybinding, AutocompleteProviderFactory, BashToolCallEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderHeadersEvent, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, BuildSystemPromptOptions, CacheWarmingDecisionEvent, CacheWarmingDecisionEventResult, CompactOptions, ContextEvent, ContextEventResult, ContextUsage, CustomToolCallEvent, EditToolCallEvent, EntryRenderer, EntryRenderOptions, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, GrepToolCallEvent, InlineExtension, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, MessageEndEvent, MessageEndEventResult, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, ModelSelectEvent, ModelSelectSource, NormalizedBuildSystemPromptOptions, ProjectTrustContext, ProjectTrustEvent, ProjectTrustEventDecision, ProjectTrustEventResult, ProjectTrustHandler, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, RegisteredCommand, RegisteredTool, ResolvedCommand, ResolvedSkillInfo, ResourcesDiscoverEvent, ResourcesDiscoverResult, SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionBeforeForkEvent, SessionBeforeForkResult, SessionBeforeSwitchEvent, SessionBeforeSwitchResult, SessionBeforeTreeEvent, SessionBeforeTreeResult, SessionCompactEvent, SessionCompactFailedEvent, SessionInfoChangedEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent, SlashCommandInfo, SlashCommandSource, SourceInfo, TerminalInputHandler, ThinkingLevelSelectEvent, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionMode, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, ToolResultEventResult, TurnEndEvent, TurnStartEvent, UIPromptEndEvent, UIPromptKind, UIPromptStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WorkingIndicatorOptions, WriteToolCallEvent, } from "./core/extensions/index.ts";
9
10
  export { createExtensionRuntime, defineTool, discoverAndLoadExtensions, ExtensionRunner, isBashToolResult, isEditToolResult, isFindToolResult, isGrepToolResult, isLsToolResult, isReadToolResult, isToolCallEventType, isWriteToolResult, wrapRegisteredTool, wrapRegisteredTools, } from "./core/extensions/index.ts";
10
11
  export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.ts";
11
12
  export { convertToLlm } from "./core/messages.ts";
@@ -18,7 +19,7 @@ export type { ResourceCollision, ResourceDiagnostic, ResourceLoader } from "./co
18
19
  export { DefaultResourceLoader, loadProjectContextFiles } from "./core/resource-loader.ts";
19
20
  export { AgentSessionRuntime, type AgentSessionRuntimeDiagnostic, type AgentSessionServices, type CreateAgentSessionFromServicesOptions, type CreateAgentSessionOptions, type CreateAgentSessionResult, type CreateAgentSessionRuntimeFactory, type CreateAgentSessionRuntimeResult, type CreateAgentSessionServicesOptions, createAgentSession, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createBashTool, createCodingTools, createEditTool, createFindTool, createGrepTool, createLsTool, createReadOnlyTools, createReadTool, createWriteTool, type PromptTemplate, } from "./core/sdk.ts";
20
21
  export { type BranchSummaryEntry, buildContextEntries, buildSessionContext, type CompactionEntry, CURRENT_SESSION_VERSION, type CustomEntry, type CustomMessageEntry, type FileEntry, getLatestCompactionEntry, type ModelChangeEntry, migrateSessionEntries, type NewSessionOptions, parseSessionEntries, type SessionContext, type SessionEntry, type SessionEntryBase, type SessionHeader, type SessionInfo, type SessionInfoEntry, SessionManager, type SessionMessageEntry, type SessionTreeNode, sessionEntryToContextMessages, type ThinkingLevelChangeEntry, } from "./core/session-manager.ts";
21
- export { type AutoHandoffSettings, type CompactionSettings, type DefaultProjectTrust, type FullscreenExitOutput, type ImageSettings, type PackageSource, type RetrySettings, SettingsManager, type SettingsManagerCreateOptions, type TuiMode, } from "./core/settings-manager.ts";
22
+ export { type AutoHandoffSettings, type CacheWarmingMode, type CompactionModelOverride, type CompactionSettings, type DefaultProjectTrust, type FullscreenExitOutput, type ImageSettings, type PackageSource, type RetrySettings, SettingsManager, type SettingsManagerCreateOptions, type TuiMode, } from "./core/settings-manager.ts";
22
23
  export { formatSkillsForPrompt, type LoadSkillsFromDirOptions, type LoadSkillsResult, loadSkills, loadSkillsFromDir, type Skill, type SkillFrontmatter, } from "./core/skills.ts";
23
24
  export { createSyntheticSourceInfo } from "./core/source-info.ts";
24
25
  export { type EditDiffResult, generateDiffString, generateUnifiedPatch } from "./core/tools/edit-diff.ts";
package/dist/main.js CHANGED
@@ -151,19 +151,23 @@ async function prepareInitialMessage(parsed, autoResizeImages, stdinContent) {
151
151
  * Resolve a session argument to a file path.
152
152
  * If it looks like a path, use as-is. Otherwise try to match as session ID prefix.
153
153
  */
154
- async function findLocalSessionByExactId(sessionId, cwd, sessionDir) {
155
- const localSessions = await SessionManager.list(cwd, sessionDir);
156
- const localMatch = localSessions.find((s) => s.id === sessionId);
157
- return localMatch ? { type: "local", path: localMatch.path } : undefined;
154
+ function findLocalSessionByExactId(sessionId, cwd, sessionDir) {
155
+ const path = SessionManager.findById(cwd, sessionId, sessionDir);
156
+ return path ? { type: "local", path } : undefined;
158
157
  }
159
158
  async function resolveSessionPath(sessionArg, cwd, sessionDir) {
160
159
  // If it looks like a file path, resolve it before handing it to the session manager.
161
160
  if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) {
162
161
  return { type: "path", path: resolvePath(sessionArg, cwd) };
163
162
  }
164
- // Try to match as session ID in current project first
163
+ // Exact IDs only require reading session headers. Fall back to the full
164
+ // metadata listing for prefix matches.
165
+ const exactLocalMatch = findLocalSessionByExactId(sessionArg, cwd, sessionDir);
166
+ if (exactLocalMatch) {
167
+ return exactLocalMatch;
168
+ }
165
169
  const localSessions = await SessionManager.list(cwd, sessionDir);
166
- const localMatch = localSessions.find((s) => s.id === sessionArg) ?? localSessions.find((s) => s.id.startsWith(sessionArg));
170
+ const localMatch = localSessions.find((s) => s.id.startsWith(sessionArg));
167
171
  if (localMatch) {
168
172
  return { type: "local", path: localMatch.path };
169
173
  }
@@ -250,7 +254,7 @@ async function createSessionManager(parsed, cwd, sessionDir, settingsManager) {
250
254
  }
251
255
  if (parsed.fork) {
252
256
  if (parsed.sessionId) {
253
- const existingTarget = await findLocalSessionByExactId(parsed.sessionId, cwd, sessionDir);
257
+ const existingTarget = findLocalSessionByExactId(parsed.sessionId, cwd, sessionDir);
254
258
  if (existingTarget) {
255
259
  console.error(chalk.red(`Session already exists with id '${parsed.sessionId}'`));
256
260
  process.exit(1);
@@ -289,7 +293,7 @@ async function createSessionManager(parsed, cwd, sessionDir, settingsManager) {
289
293
  }
290
294
  if (parsed.resume) {
291
295
  try {
292
- const selectedPath = await selectSession((onProgress) => SessionManager.list(cwd, sessionDir, onProgress), (onProgress) => SessionManager.listAll(sessionDir, onProgress), settingsManager);
296
+ const selectedPath = await selectSession((onProgress, signal) => SessionManager.list(cwd, sessionDir, onProgress, signal), (onProgress, signal) => SessionManager.listAll(sessionDir, onProgress, signal), settingsManager);
293
297
  if (!selectedPath) {
294
298
  console.log(chalk.dim("No session selected"));
295
299
  process.exit(0);
@@ -304,7 +308,7 @@ async function createSessionManager(parsed, cwd, sessionDir, settingsManager) {
304
308
  return SessionManager.continueRecent(cwd, sessionDir);
305
309
  }
306
310
  if (parsed.sessionId) {
307
- const existingSession = await findLocalSessionByExactId(parsed.sessionId, cwd, sessionDir);
311
+ const existingSession = findLocalSessionByExactId(parsed.sessionId, cwd, sessionDir);
308
312
  if (existingSession) {
309
313
  return SessionManager.open(existingSession.path, sessionDir);
310
314
  }
@@ -6,8 +6,8 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, w
6
6
  import { dirname, join } from "path";
7
7
  import { CONFIG_DIR_NAME, getAgentDir, getBinDir } from "./config.js";
8
8
  import { migrateKeybindingsConfig } from "./core/keybindings.js";
9
- const MIGRATION_GUIDE_URL = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md#extensions-migration";
10
- const EXTENSIONS_DOC_URL = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/docs/extensions.md";
9
+ const MIGRATION_GUIDE_URL = "https://github.com/earendil-works/pi/blob/main/packages/coding-agent/CHANGELOG.md#extensions-migration";
10
+ const EXTENSIONS_DOC_URL = "https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md";
11
11
  /**
12
12
  * Migrate legacy oauth.json and settings.json apiKeys to auth.json.
13
13
  *
@@ -0,0 +1,15 @@
1
+ import type { Container, EditorComponent, TUI } from "@earendil-works/pi-tui";
2
+ import type { AgentSession } from "../../core/agent-session.ts";
3
+ import type { KeybindingsManager } from "../../core/keybindings.ts";
4
+ interface BugReportContext {
5
+ session: AgentSession;
6
+ ui: TUI;
7
+ editorContainer: Container;
8
+ editor: EditorComponent;
9
+ keybindings: KeybindingsManager;
10
+ showStatus: (message: string) => void;
11
+ showError: (message: string) => void;
12
+ }
13
+ /** Run the `/bug` flow: consent, optional summary, then upload or export. */
14
+ export declare function reportBug(context: BugReportContext, initialHint?: string): Promise<void>;
15
+ export {};
@@ -0,0 +1,205 @@
1
+ import * as path from "node:path";
2
+ import { getAuthCredential } from "../../cli/auth-command.js";
3
+ import { BUG_REPORT_CUSTOM_ENTRY_TYPE, bugReportArchiveFileName, collectBugReportDiagnostics, collectBugReportMetadata, writeBugReportArchive, } from "../../core/bug-report.js";
4
+ import { uploadBugReport } from "../../core/bug-report-upload.js";
5
+ import { clearCrashLog, readCrashLog } from "../../core/crash-log.js";
6
+ import { getRadiusGatewayUrl, RADIUS_PROVIDER_ID } from "../../core/radius.js";
7
+ import { serializeSessionBranch } from "../../core/session-export.js";
8
+ import { BorderedLoader } from "./components/bordered-loader.js";
9
+ import { ExtensionEditorComponent } from "./components/extension-editor.js";
10
+ import { ExtensionSelectorComponent } from "./components/extension-selector.js";
11
+ import { createShareTrailingEntries } from "./session-share.js";
12
+ import { theme } from "./theme/theme.js";
13
+ const DISCLAIMER = "This report goes to the Pi developers (Earendil) and is not shared publicly. It includes your pi version, operating system, the current model and provider configuration (without API keys), loaded extensions, settings, and provider error diagnostics from this session.";
14
+ const TRANSCRIPT_NOTE = "The transcript contains your messages, model output, tool calls and their results, including file contents and command output read during this session.";
15
+ /** Run the `/bug` flow: consent, optional summary, then upload or export. */
16
+ export async function reportBug(context, initialHint) {
17
+ const options = await promptForOptions(context, initialHint);
18
+ if (!options) {
19
+ context.showStatus("Bug report cancelled");
20
+ return;
21
+ }
22
+ let summary;
23
+ if (options.includeSummary) {
24
+ const loader = showLoader(context, `Writing summary with ${context.session.model?.name ?? "the current model"}...`);
25
+ try {
26
+ summary = await context.session.summarizeForBugReport({ hint: options.hint, signal: loader.signal });
27
+ }
28
+ catch (error) {
29
+ restoreEditor(context, loader);
30
+ if (loader.signal.aborted)
31
+ context.showStatus("Bug report cancelled");
32
+ else
33
+ context.showError(`Failed to write bug report summary: ${errorMessage(error)}`);
34
+ return;
35
+ }
36
+ restoreEditor(context, loader);
37
+ if (loader.signal.aborted) {
38
+ context.showStatus("Bug report cancelled");
39
+ return;
40
+ }
41
+ }
42
+ let bundle;
43
+ try {
44
+ bundle = buildBundle(context.session, options, summary);
45
+ }
46
+ catch (error) {
47
+ context.showError(`Failed to build bug report: ${errorMessage(error)}`);
48
+ return;
49
+ }
50
+ if (options.delivery === "upload") {
51
+ const failure = await upload(context, bundle);
52
+ if (failure === undefined)
53
+ return;
54
+ const fallback = await choose(context, "Upload failed", ["Export as Zip", "Cancel"], `${failure}\n\nExport the report as a zip archive instead?`);
55
+ if (fallback !== "Export as Zip") {
56
+ context.showStatus("Bug report cancelled");
57
+ return;
58
+ }
59
+ }
60
+ await exportZip(context, bundle);
61
+ }
62
+ async function promptForOptions(context, initialHint) {
63
+ const hint = await input(context, "Report a bug", `${DISCLAIMER}\n\nWhat went wrong? (optional)`, initialHint);
64
+ if (hint === null)
65
+ return undefined;
66
+ const transcript = await choose(context, "Include the session transcript?", ["Yes, include the transcript", "No"], TRANSCRIPT_NOTE);
67
+ if (!transcript)
68
+ return undefined;
69
+ const includeSession = transcript !== "No";
70
+ let includeSummary = false;
71
+ if (!includeSession) {
72
+ const model = context.session.model;
73
+ const summary = await choose(context, `Attach a summary written by ${model?.name ?? "the current model"} instead?`, ["Yes, generate a summary", "No"], `The transcript is sent to ${model?.provider ?? "your provider"} with your credentials and tokens. Only the generated summary is attached; the transcript stays on your machine.`);
74
+ if (!summary)
75
+ return undefined;
76
+ includeSummary = summary !== "No";
77
+ }
78
+ const description = hint.trim();
79
+ const delivery = await choose(context, "Bug report", ["Upload Report", "Export as Zip", "Cancel"], `Description: ${description || "none"}\nTranscript: ${includeSession ? "included" : "not included"}\nSummary: ${includeSummary ? `written by ${context.session.model?.name ?? "the current model"}` : "none"}\n\nUpload sends the report to ${new URL(getRadiusGatewayUrl()).host}. Export writes a zip archive to the current directory instead.`);
80
+ if (!delivery || delivery === "Cancel")
81
+ return undefined;
82
+ return {
83
+ hint: description || undefined,
84
+ includeSession,
85
+ includeSummary,
86
+ delivery: delivery === "Upload Report" ? "upload" : "zip",
87
+ };
88
+ }
89
+ function buildBundle(session, options, summary) {
90
+ const extensions = session.resourceLoader.getExtensions();
91
+ return {
92
+ metadata: collectBugReportMetadata({
93
+ hint: options.hint,
94
+ sessionId: session.sessionId,
95
+ cwd: session.sessionManager.getCwd(),
96
+ includeSession: options.includeSession,
97
+ includeSummary: summary !== undefined,
98
+ messageCount: session.messages.length,
99
+ model: session.model,
100
+ modelRuntime: session.modelRuntime,
101
+ thinkingLevel: session.thinkingLevel,
102
+ extensions: extensions.extensions,
103
+ extensionErrors: extensions.errors,
104
+ globalSettings: session.settingsManager.getGlobalSettings(),
105
+ projectSettings: session.settingsManager.getProjectSettings(),
106
+ }),
107
+ diagnostics: collectBugReportDiagnostics(session.sessionManager, readCrashLog()),
108
+ summary,
109
+ sessionJsonl: options.includeSession
110
+ ? serializeSessionBranch(session.sessionManager, (parentId, timestamp) => createShareTrailingEntries(session, parentId, timestamp))
111
+ : undefined,
112
+ };
113
+ }
114
+ async function upload(context, bundle) {
115
+ const loader = showLoader(context, "Uploading bug report...");
116
+ try {
117
+ const provider = context.session.modelRuntime.getProvider(RADIUS_PROVIDER_ID);
118
+ const token = provider
119
+ ? getAuthCredential(await context.session.modelRuntime.getAuth(RADIUS_PROVIDER_ID, { minOAuthValidityMs: 5 * 60_000 }))
120
+ : undefined;
121
+ const result = await uploadBugReport(bundle, { token, signal: loader.signal });
122
+ restoreEditor(context, loader);
123
+ recordInSession(context.session, bundle, { delivery: "upload" });
124
+ context.showStatus(`Bug report uploaded. Report ID: ${result.id}`);
125
+ return undefined;
126
+ }
127
+ catch (error) {
128
+ restoreEditor(context, loader);
129
+ if (loader.signal.aborted) {
130
+ context.showStatus("Bug report cancelled");
131
+ return undefined;
132
+ }
133
+ return errorMessage(error);
134
+ }
135
+ }
136
+ async function exportZip(context, bundle) {
137
+ const archivePath = path.join(process.cwd(), bugReportArchiveFileName(bundle.metadata.id));
138
+ try {
139
+ await writeBugReportArchive(bundle, archivePath);
140
+ }
141
+ catch (error) {
142
+ context.showError(`Failed to write bug report: ${errorMessage(error)}`);
143
+ return;
144
+ }
145
+ recordInSession(context.session, bundle, { delivery: "zip", path: archivePath });
146
+ context.showStatus(`Bug report exported to: ${archivePath}\nReport ID: ${bundle.metadata.id}`);
147
+ }
148
+ function recordInSession(session, bundle, delivery) {
149
+ session.sessionManager.appendCustomEntry(BUG_REPORT_CUSTOM_ENTRY_TYPE, {
150
+ id: bundle.metadata.id,
151
+ createdAt: bundle.metadata.createdAt,
152
+ hint: bundle.metadata.hint,
153
+ sessionIncluded: bundle.metadata.session.included,
154
+ summaryIncluded: bundle.metadata.session.summaryIncluded,
155
+ ...delivery,
156
+ });
157
+ if (bundle.diagnostics.crashes.length > 0)
158
+ clearCrashLog();
159
+ }
160
+ function input(context, title, description, initialValue) {
161
+ return new Promise((resolve) => {
162
+ let component;
163
+ const finish = (value) => {
164
+ restoreEditor(context, component);
165
+ resolve(value);
166
+ };
167
+ component = new ExtensionEditorComponent(context.ui, context.keybindings, title, initialValue, (value) => finish(value), () => finish(null), { description }, context.session.settingsManager.getExternalEditorCommand());
168
+ showOverlay(context, component);
169
+ });
170
+ }
171
+ function choose(context, title, options, description) {
172
+ return new Promise((resolve) => {
173
+ let component;
174
+ const finish = (value) => {
175
+ restoreEditor(context, component);
176
+ resolve(value);
177
+ };
178
+ component = new ExtensionSelectorComponent(title, options, finish, () => finish(), {
179
+ tui: context.ui,
180
+ description,
181
+ });
182
+ showOverlay(context, component);
183
+ });
184
+ }
185
+ function showLoader(context, message) {
186
+ const loader = new BorderedLoader(context.ui, theme, message);
187
+ showOverlay(context, loader);
188
+ return loader;
189
+ }
190
+ function showOverlay(context, component) {
191
+ context.editorContainer.clear();
192
+ context.editorContainer.addChild(component);
193
+ context.ui.setFocus(component);
194
+ context.ui.requestRender();
195
+ }
196
+ function restoreEditor(context, component) {
197
+ component.dispose?.();
198
+ context.editorContainer.clear();
199
+ context.editorContainer.addChild(context.editor);
200
+ context.ui.setFocus(context.editor);
201
+ context.ui.requestRender();
202
+ }
203
+ function errorMessage(error) {
204
+ return error instanceof Error ? error.message : "Unknown error";
205
+ }
@@ -15,7 +15,7 @@ export function createChatViewport(options) {
15
15
  ...(options.widgetsAbove === undefined ? [] : [{ component: options.widgetsAbove, shrink: 1, minSize: 0 }]),
16
16
  { component: options.editor, shrink: 1, minSize: 3 },
17
17
  ...(options.widgetsBelow === undefined ? [] : [{ component: options.widgetsBelow, shrink: 1, minSize: 0 }]),
18
- { component: options.footer, shrink: 1, minSize: 1 },
18
+ { component: options.footer, shrink: 1, minSize: 0 },
19
19
  ]);
20
20
  return {
21
21
  transcript,
@@ -1,4 +1,4 @@
1
- import { Box, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
1
+ import { Box, Container, Markdown, MouseRegion, Spacer, Text } from "@earendil-works/pi-tui";
2
2
  import { getMarkdownTheme, theme } from "../theme/theme.js";
3
3
  import { keyText } from "./keybinding-hints.js";
4
4
  /**
@@ -25,19 +25,26 @@ export class BranchSummaryMessageComponent extends Box {
25
25
  }
26
26
  updateDisplay() {
27
27
  this.clear();
28
+ const content = new Container();
28
29
  const label = theme.fg("customMessageLabel", `\x1b[1m[branch]\x1b[22m`);
29
- this.addChild(new Text(label, 0, 0));
30
- this.addChild(new Spacer(1));
30
+ content.addChild(new Text(label, 0, 0));
31
+ content.addChild(new Spacer(1));
31
32
  if (this.expanded) {
32
33
  const header = "**Branch Summary**\n\n";
33
- this.addChild(new Markdown(header + this.message.summary, 0, 0, this.markdownTheme, {
34
+ content.addChild(new Markdown(header + this.message.summary, 0, 0, this.markdownTheme, {
34
35
  color: (text) => theme.fg("customMessageText", text),
35
36
  }));
36
37
  }
37
38
  else {
38
- this.addChild(new Text(theme.fg("customMessageText", "Branch summary (") +
39
+ content.addChild(new Text(theme.fg("customMessageText", "Branch summary (") +
39
40
  theme.fg("dim", keyText("app.tools.expand")) +
40
41
  theme.fg("customMessageText", " to expand)"), 0, 0));
41
42
  }
43
+ this.addChild(new MouseRegion(content, (event) => {
44
+ if (event.type !== "click" || event.button !== "left")
45
+ return undefined;
46
+ this.setExpanded(!this.expanded);
47
+ return { handled: true };
48
+ }));
42
49
  }
43
50
  }
@@ -1,4 +1,4 @@
1
- import { Box, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
1
+ import { Box, Container, Markdown, MouseRegion, Spacer, Text } from "@earendil-works/pi-tui";
2
2
  import { getMarkdownTheme, theme } from "../theme/theme.js";
3
3
  import { keyText } from "./keybinding-hints.js";
4
4
  /**
@@ -25,20 +25,27 @@ export class CompactionSummaryMessageComponent extends Box {
25
25
  }
26
26
  updateDisplay() {
27
27
  this.clear();
28
+ const content = new Container();
28
29
  const tokenStr = this.message.tokensBefore.toLocaleString();
29
30
  const label = theme.fg("customMessageLabel", `\x1b[1m[compaction]\x1b[22m`);
30
- this.addChild(new Text(label, 0, 0));
31
- this.addChild(new Spacer(1));
31
+ content.addChild(new Text(label, 0, 0));
32
+ content.addChild(new Spacer(1));
32
33
  if (this.expanded) {
33
34
  const header = `**Compacted from ${tokenStr} tokens**\n\n`;
34
- this.addChild(new Markdown(header + this.message.summary, 0, 0, this.markdownTheme, {
35
+ content.addChild(new Markdown(header + this.message.summary, 0, 0, this.markdownTheme, {
35
36
  color: (text) => theme.fg("customMessageText", text),
36
37
  }));
37
38
  }
38
39
  else {
39
- this.addChild(new Text(theme.fg("customMessageText", `Compacted from ${tokenStr} tokens (`) +
40
+ content.addChild(new Text(theme.fg("customMessageText", `Compacted from ${tokenStr} tokens (`) +
40
41
  theme.fg("dim", keyText("app.tools.expand")) +
41
42
  theme.fg("customMessageText", " to expand)"), 0, 0));
42
43
  }
44
+ this.addChild(new MouseRegion(content, (event) => {
45
+ if (event.type !== "click" || event.button !== "left")
46
+ return undefined;
47
+ this.setExpanded(!this.expanded);
48
+ return { handled: true };
49
+ }));
43
50
  }
44
51
  }
@@ -1,8 +1,8 @@
1
1
  import { Editor, type EditorOptions, type EditorTheme, type TUI } from "@earendil-works/pi-tui";
2
2
  import type { AppKeybinding, KeybindingsManager } from "../../../core/keybindings.ts";
3
- import type { WorkingStatusIndicator } from "./status-indicator.ts";
3
+ import type { StatusIndicator } from "./status-indicator.ts";
4
4
  export type CustomEditorOptions = EditorOptions & {
5
- /** Render the streaming working status in the editor's top border. */
5
+ /** Render working, compaction, summarization, and retry status in the editor's top border. */
6
6
  embedWorkingStatus?: boolean;
7
7
  };
8
8
  /**
@@ -19,7 +19,7 @@ export declare class CustomEditor extends Editor {
19
19
  /** Handler for extension-registered shortcuts. Returns true if handled. */
20
20
  onExtensionShortcut?: (data: string) => boolean;
21
21
  constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, options?: CustomEditorOptions);
22
- setWorkingStatusIndicator(indicator: WorkingStatusIndicator | undefined): void;
22
+ setWorkingStatusIndicator(indicator: StatusIndicator | undefined): void;
23
23
  protected renderTopBorder(width: number, hiddenLineCount: number): string;
24
24
  /**
25
25
  * Register a handler for an app action.
@@ -4,6 +4,9 @@
4
4
  */
5
5
  import { Container, type EditorOptions, type Focusable, type TUI } from "@earendil-works/pi-tui";
6
6
  import type { KeybindingsManager } from "../../../core/keybindings.ts";
7
+ export interface ExtensionEditorOptions extends EditorOptions {
8
+ description?: string;
9
+ }
7
10
  export declare class ExtensionEditorComponent extends Container implements Focusable {
8
11
  private editor;
9
12
  private onSubmitCallback;
@@ -14,7 +17,7 @@ export declare class ExtensionEditorComponent extends Container implements Focus
14
17
  private _focused;
15
18
  get focused(): boolean;
16
19
  set focused(value: boolean);
17
- constructor(tui: TUI, keybindings: KeybindingsManager, title: string, prefill: string | undefined, onSubmit: (value: string) => void, onCancel: () => void, options?: EditorOptions, externalEditorCommand?: string);
20
+ constructor(tui: TUI, keybindings: KeybindingsManager, title: string, prefill: string | undefined, onSubmit: (value: string) => void, onCancel: () => void, options?: ExtensionEditorOptions, externalEditorCommand?: string);
18
21
  handleInput(keyData: string): void;
19
22
  private handleOpenExternalEditor;
20
23
  }
@@ -33,14 +33,19 @@ export class ExtensionEditorComponent extends Container {
33
33
  (process.platform === "win32" ? "notepad" : "nano");
34
34
  this.onSubmitCallback = onSubmit;
35
35
  this.onCancelCallback = onCancel;
36
+ const { description, ...editorOptions } = options ?? {};
36
37
  // Add top border
37
38
  this.addChild(new DynamicBorder());
38
39
  this.addChild(new Spacer(1));
39
- // Add title
40
+ // Add title and optional description
40
41
  this.addChild(new Text(theme.fg("accent", title), 1, 0));
42
+ if (description) {
43
+ this.addChild(new Spacer(1));
44
+ this.addChild(new Text(theme.fg("text", description), 1, 0));
45
+ }
41
46
  this.addChild(new Spacer(1));
42
47
  // Create editor
43
- this.editor = new Editor(tui, getEditorTheme(), options);
48
+ this.editor = new Editor(tui, getEditorTheme(), editorOptions);
44
49
  if (prefill) {
45
50
  this.editor.setText(prefill);
46
51
  }
@@ -5,6 +5,8 @@ import { Container, type Focusable, type TUI } from "@earendil-works/pi-tui";
5
5
  export interface ExtensionInputOptions {
6
6
  tui?: TUI;
7
7
  timeout?: number;
8
+ initialValue?: string;
9
+ description?: string;
8
10
  }
9
11
  export declare class ExtensionInputComponent extends Container implements Focusable {
10
12
  private input;
@@ -31,11 +31,17 @@ export class ExtensionInputComponent extends Container {
31
31
  this.addChild(new Spacer(1));
32
32
  this.titleText = new Text(theme.fg("accent", title), 1, 0);
33
33
  this.addChild(this.titleText);
34
+ if (opts?.description) {
35
+ this.addChild(new Spacer(1));
36
+ this.addChild(new Text(theme.fg("text", opts.description), 1, 0));
37
+ }
34
38
  this.addChild(new Spacer(1));
35
39
  if (opts?.timeout && opts.timeout > 0 && opts.tui) {
36
40
  this.countdown = new CountdownTimer(opts.timeout, opts.tui, (s) => this.titleText.setText(theme.fg("accent", `${this.baseTitle} (${s}s)`)), () => this.onCancelCallback());
37
41
  }
38
42
  this.input = new Input();
43
+ if (opts?.initialValue)
44
+ this.input.setValue(opts.initialValue);
39
45
  this.addChild(this.input);
40
46
  this.addChild(new Spacer(1));
41
47
  this.addChild(new Text(`${keyHint("tui.select.confirm", "submit")} ${keyHint("tui.select.cancel", "cancel")}`, 1, 0));
@@ -7,6 +7,7 @@ export interface ExtensionSelectorOptions {
7
7
  tui?: TUI;
8
8
  timeout?: number;
9
9
  onToggleToolsExpanded?: () => void;
10
+ description?: string;
10
11
  }
11
12
  export declare class ExtensionSelectorComponent extends Container {
12
13
  private options;
@@ -28,6 +28,10 @@ export class ExtensionSelectorComponent extends Container {
28
28
  this.addChild(new Spacer(1));
29
29
  this.titleText = new Text(theme.fg("accent", theme.bold(title)), 1, 0);
30
30
  this.addChild(this.titleText);
31
+ if (opts?.description) {
32
+ this.addChild(new Spacer(1));
33
+ this.addChild(new Text(theme.fg("text", opts.description), 1, 0));
34
+ }
31
35
  this.addChild(new Spacer(1));
32
36
  if (opts?.timeout && opts.timeout > 0 && opts.tui) {
33
37
  this.countdown = new CountdownTimer(opts.timeout, opts.tui, (s) => this.titleText.setText(theme.fg("accent", theme.bold(`${this.baseTitle} (${s}s)`))), () => this.onCancelCallback());