byterover-cli 1.4.0 → 1.6.0

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 (174) hide show
  1. package/README.md +193 -12
  2. package/dist/core/domain/cipher/process/types.d.ts +1 -1
  3. package/dist/core/domain/entities/provider-config.d.ts +92 -0
  4. package/dist/core/domain/entities/provider-config.js +181 -0
  5. package/dist/core/domain/entities/provider-registry.d.ts +55 -0
  6. package/dist/core/domain/entities/provider-registry.js +74 -0
  7. package/dist/core/domain/errors/headless-prompt-error.d.ts +11 -0
  8. package/dist/core/domain/errors/headless-prompt-error.js +18 -0
  9. package/dist/core/interfaces/cipher/i-content-generator.d.ts +30 -0
  10. package/dist/core/interfaces/cipher/i-content-generator.js +12 -1
  11. package/dist/core/interfaces/cipher/message-factory.d.ts +4 -1
  12. package/dist/core/interfaces/cipher/message-factory.js +5 -0
  13. package/dist/core/interfaces/cipher/message-types.d.ts +19 -1
  14. package/dist/core/interfaces/i-cogit-pull-service.d.ts +0 -1
  15. package/dist/core/interfaces/i-memory-retrieval-service.d.ts +0 -1
  16. package/dist/core/interfaces/i-memory-storage-service.d.ts +0 -2
  17. package/dist/core/interfaces/i-provider-config-store.d.ts +88 -0
  18. package/dist/core/interfaces/i-provider-config-store.js +1 -0
  19. package/dist/core/interfaces/i-provider-keychain-store.d.ts +33 -0
  20. package/dist/core/interfaces/i-provider-keychain-store.js +1 -0
  21. package/dist/core/interfaces/i-space-service.d.ts +1 -2
  22. package/dist/core/interfaces/i-team-service.d.ts +1 -2
  23. package/dist/core/interfaces/i-user-service.d.ts +1 -2
  24. package/dist/core/interfaces/usecase/i-curate-use-case.d.ts +2 -0
  25. package/dist/core/interfaces/usecase/i-init-use-case.d.ts +9 -3
  26. package/dist/core/interfaces/usecase/i-login-use-case.d.ts +4 -1
  27. package/dist/core/interfaces/usecase/i-pull-use-case.d.ts +5 -3
  28. package/dist/core/interfaces/usecase/i-push-use-case.d.ts +6 -4
  29. package/dist/core/interfaces/usecase/i-query-use-case.d.ts +2 -0
  30. package/dist/core/interfaces/usecase/i-status-use-case.d.ts +1 -0
  31. package/dist/infra/cipher/agent/service-initializer.d.ts +1 -1
  32. package/dist/infra/cipher/agent/service-initializer.js +0 -1
  33. package/dist/infra/cipher/file-system/file-system-service.js +5 -5
  34. package/dist/infra/cipher/http/internal-llm-http-service.d.ts +40 -1
  35. package/dist/infra/cipher/http/internal-llm-http-service.js +153 -4
  36. package/dist/infra/cipher/llm/formatters/gemini-formatter.js +8 -1
  37. package/dist/infra/cipher/llm/generators/byterover-content-generator.d.ts +2 -3
  38. package/dist/infra/cipher/llm/generators/byterover-content-generator.js +20 -11
  39. package/dist/infra/cipher/llm/generators/openrouter-content-generator.d.ts +1 -0
  40. package/dist/infra/cipher/llm/generators/openrouter-content-generator.js +26 -0
  41. package/dist/infra/cipher/llm/internal-llm-service.d.ts +13 -0
  42. package/dist/infra/cipher/llm/internal-llm-service.js +75 -4
  43. package/dist/infra/cipher/llm/model-capabilities.d.ts +74 -0
  44. package/dist/infra/cipher/llm/model-capabilities.js +157 -0
  45. package/dist/infra/cipher/llm/openrouter-llm-service.d.ts +35 -1
  46. package/dist/infra/cipher/llm/openrouter-llm-service.js +216 -28
  47. package/dist/infra/cipher/llm/stream-processor.d.ts +22 -2
  48. package/dist/infra/cipher/llm/stream-processor.js +78 -4
  49. package/dist/infra/cipher/llm/thought-parser.d.ts +1 -1
  50. package/dist/infra/cipher/llm/thought-parser.js +5 -5
  51. package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.d.ts +49 -0
  52. package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.js +272 -0
  53. package/dist/infra/cipher/llm/transformers/reasoning-extractor.d.ts +71 -0
  54. package/dist/infra/cipher/llm/transformers/reasoning-extractor.js +253 -0
  55. package/dist/infra/cipher/process/process-service.js +1 -1
  56. package/dist/infra/cipher/session/chat-session.d.ts +2 -0
  57. package/dist/infra/cipher/session/chat-session.js +13 -2
  58. package/dist/infra/cipher/storage/message-storage-service.js +4 -0
  59. package/dist/infra/cipher/tools/implementations/bash-exec-tool.js +3 -3
  60. package/dist/infra/cipher/tools/implementations/task-tool.js +1 -1
  61. package/dist/infra/cogit/http-cogit-pull-service.js +1 -1
  62. package/dist/infra/cogit/http-cogit-push-service.js +0 -1
  63. package/dist/infra/http/authenticated-http-client.d.ts +1 -3
  64. package/dist/infra/http/authenticated-http-client.js +1 -5
  65. package/dist/infra/http/openrouter-api-client.d.ts +148 -0
  66. package/dist/infra/http/openrouter-api-client.js +161 -0
  67. package/dist/infra/mcp/tools/task-result-waiter.js +9 -1
  68. package/dist/infra/memory/http-memory-retrieval-service.js +1 -1
  69. package/dist/infra/memory/http-memory-storage-service.js +2 -2
  70. package/dist/infra/process/agent-worker.js +178 -70
  71. package/dist/infra/process/inline-agent-executor.d.ts +32 -0
  72. package/dist/infra/process/inline-agent-executor.js +259 -0
  73. package/dist/infra/process/transport-handlers.d.ts +25 -4
  74. package/dist/infra/process/transport-handlers.js +57 -10
  75. package/dist/infra/repl/commands/connectors-command.js +2 -2
  76. package/dist/infra/repl/commands/index.js +5 -0
  77. package/dist/infra/repl/commands/model-command.d.ts +13 -0
  78. package/dist/infra/repl/commands/model-command.js +212 -0
  79. package/dist/infra/repl/commands/provider-command.d.ts +13 -0
  80. package/dist/infra/repl/commands/provider-command.js +181 -0
  81. package/dist/infra/repl/transport-client-helper.js +6 -2
  82. package/dist/infra/space/http-space-service.d.ts +1 -1
  83. package/dist/infra/space/http-space-service.js +2 -2
  84. package/dist/infra/storage/file-provider-config-store.d.ts +83 -0
  85. package/dist/infra/storage/file-provider-config-store.js +157 -0
  86. package/dist/infra/storage/provider-keychain-store.d.ts +37 -0
  87. package/dist/infra/storage/provider-keychain-store.js +75 -0
  88. package/dist/infra/storage/token-store.d.ts +4 -3
  89. package/dist/infra/storage/token-store.js +6 -5
  90. package/dist/infra/team/http-team-service.d.ts +1 -1
  91. package/dist/infra/team/http-team-service.js +2 -2
  92. package/dist/infra/terminal/headless-terminal.d.ts +91 -0
  93. package/dist/infra/terminal/headless-terminal.js +211 -0
  94. package/dist/infra/transport/socket-io-transport-client.d.ts +20 -0
  95. package/dist/infra/transport/socket-io-transport-client.js +88 -1
  96. package/dist/infra/usecase/curate-use-case.d.ts +40 -1
  97. package/dist/infra/usecase/curate-use-case.js +176 -15
  98. package/dist/infra/usecase/init-use-case.d.ts +27 -5
  99. package/dist/infra/usecase/init-use-case.js +200 -34
  100. package/dist/infra/usecase/login-use-case.d.ts +10 -8
  101. package/dist/infra/usecase/login-use-case.js +35 -2
  102. package/dist/infra/usecase/pull-use-case.d.ts +19 -5
  103. package/dist/infra/usecase/pull-use-case.js +71 -13
  104. package/dist/infra/usecase/push-use-case.d.ts +18 -5
  105. package/dist/infra/usecase/push-use-case.js +81 -14
  106. package/dist/infra/usecase/query-use-case.d.ts +21 -0
  107. package/dist/infra/usecase/query-use-case.js +114 -29
  108. package/dist/infra/usecase/space-list-use-case.js +1 -1
  109. package/dist/infra/usecase/space-switch-use-case.js +2 -2
  110. package/dist/infra/usecase/status-use-case.d.ts +36 -0
  111. package/dist/infra/usecase/status-use-case.js +185 -48
  112. package/dist/infra/user/http-user-service.d.ts +1 -1
  113. package/dist/infra/user/http-user-service.js +2 -2
  114. package/dist/oclif/commands/curate.d.ts +6 -1
  115. package/dist/oclif/commands/curate.js +24 -3
  116. package/dist/oclif/commands/init.d.ts +18 -0
  117. package/dist/oclif/commands/init.js +129 -0
  118. package/dist/oclif/commands/login.d.ts +9 -0
  119. package/dist/oclif/commands/login.js +45 -0
  120. package/dist/oclif/commands/pull.d.ts +16 -0
  121. package/dist/oclif/commands/pull.js +78 -0
  122. package/dist/oclif/commands/push.d.ts +17 -0
  123. package/dist/oclif/commands/push.js +87 -0
  124. package/dist/oclif/commands/query.d.ts +6 -1
  125. package/dist/oclif/commands/query.js +29 -4
  126. package/dist/oclif/commands/status.d.ts +5 -1
  127. package/dist/oclif/commands/status.js +17 -5
  128. package/dist/resources/tools/bash_exec.txt +1 -1
  129. package/dist/tui/components/api-key-dialog.d.ts +39 -0
  130. package/dist/tui/components/api-key-dialog.js +94 -0
  131. package/dist/tui/components/execution/execution-changes.d.ts +3 -1
  132. package/dist/tui/components/execution/execution-changes.js +4 -4
  133. package/dist/tui/components/execution/execution-content.d.ts +1 -1
  134. package/dist/tui/components/execution/execution-content.js +4 -12
  135. package/dist/tui/components/execution/execution-input.js +1 -1
  136. package/dist/tui/components/execution/execution-progress.d.ts +10 -13
  137. package/dist/tui/components/execution/execution-progress.js +70 -17
  138. package/dist/tui/components/execution/execution-reasoning.d.ts +16 -0
  139. package/dist/tui/components/execution/execution-reasoning.js +34 -0
  140. package/dist/tui/components/execution/execution-tool.d.ts +23 -0
  141. package/dist/tui/components/execution/execution-tool.js +125 -0
  142. package/dist/tui/components/execution/expanded-log-view.js +3 -3
  143. package/dist/tui/components/execution/log-item.d.ts +2 -0
  144. package/dist/tui/components/execution/log-item.js +6 -4
  145. package/dist/tui/components/index.d.ts +2 -0
  146. package/dist/tui/components/index.js +2 -0
  147. package/dist/tui/components/inline-prompts/inline-select.js +3 -2
  148. package/dist/tui/components/model-dialog.d.ts +63 -0
  149. package/dist/tui/components/model-dialog.js +89 -0
  150. package/dist/tui/components/onboarding/onboarding-flow.js +8 -2
  151. package/dist/tui/components/provider-dialog.d.ts +27 -0
  152. package/dist/tui/components/provider-dialog.js +31 -0
  153. package/dist/tui/components/reasoning-text.d.ts +26 -0
  154. package/dist/tui/components/reasoning-text.js +49 -0
  155. package/dist/tui/components/selectable-list.d.ts +54 -0
  156. package/dist/tui/components/selectable-list.js +180 -0
  157. package/dist/tui/components/streaming-text.d.ts +30 -0
  158. package/dist/tui/components/streaming-text.js +52 -0
  159. package/dist/tui/contexts/tasks-context.d.ts +15 -0
  160. package/dist/tui/contexts/tasks-context.js +224 -40
  161. package/dist/tui/contexts/theme-context.d.ts +1 -0
  162. package/dist/tui/contexts/theme-context.js +3 -2
  163. package/dist/tui/hooks/use-activity-logs.js +7 -1
  164. package/dist/tui/hooks/use-auth-polling.js +1 -1
  165. package/dist/tui/types/messages.d.ts +32 -5
  166. package/dist/tui/utils/index.d.ts +1 -1
  167. package/dist/tui/utils/index.js +1 -1
  168. package/dist/tui/utils/log.d.ts +0 -9
  169. package/dist/tui/utils/log.js +2 -53
  170. package/dist/tui/views/command-view.js +4 -1
  171. package/dist/utils/environment-detector.d.ts +15 -0
  172. package/dist/utils/environment-detector.js +62 -1
  173. package/oclif.manifest.json +287 -5
  174. package/package.json +1 -1
@@ -29,6 +29,7 @@ export function InlineSelect({ choices, maxVisibleItems = 7, message, onSelect,
29
29
  });
30
30
  return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: colors.text, children: [_jsx(Text, { color: colors.secondary, children: "? " }), message] }), _jsx(Box, { flexDirection: "column", children: visibleChoices.map((choice, index) => {
31
31
  const actualIndex = windowStart + index;
32
- return (_jsxs(Text, { color: actualIndex === selectedIndex ? colors.primary : colors.text, children: [actualIndex === selectedIndex ? '❯ ' : ' ', choice.name] }, choice.name));
33
- }) }), selectedChoice?.description && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.secondary, children: selectedChoice.description }) }))] }));
32
+ const isSelected = actualIndex === selectedIndex;
33
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? colors.primary : colors.text, children: [isSelected ? '❯ ' : ' ', choice.name] }), choice.description && (_jsxs(Text, { color: colors.dimText, children: [" \u00B7 ", choice.description] }))] }, choice.name));
34
+ }) })] }));
34
35
  }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * ModelDialog Component
3
+ *
4
+ * Interactive dialog for selecting LLM models.
5
+ * Features:
6
+ * - Grouped display: Favorites, Recent, All models
7
+ * - Tags: [Current], [Free], pricing info
8
+ * - Fuzzy search filtering
9
+ * - Favorite toggle with 'f' key
10
+ * - Keyboard navigation
11
+ */
12
+ import React from 'react';
13
+ /**
14
+ * Model information for display in the dialog.
15
+ */
16
+ export interface ModelItem {
17
+ /** Context window size */
18
+ contextLength?: number;
19
+ /** Optional description */
20
+ description?: string;
21
+ /** Model ID (e.g., 'anthropic/claude-3.5-sonnet') */
22
+ id: string;
23
+ /** Whether this is the current active model */
24
+ isCurrent: boolean;
25
+ /** Whether this model is a favorite */
26
+ isFavorite: boolean;
27
+ /** Whether this model is free */
28
+ isFree?: boolean;
29
+ /** Whether this model was recently used */
30
+ isRecent: boolean;
31
+ /** Display name */
32
+ name: string;
33
+ /** Pricing per million tokens */
34
+ pricing?: {
35
+ inputPerM: number;
36
+ outputPerM: number;
37
+ };
38
+ /** Provider name (e.g., 'Anthropic', 'OpenAI') */
39
+ provider?: string;
40
+ }
41
+ /**
42
+ * Props for ModelDialog.
43
+ */
44
+ export interface ModelDialogProps {
45
+ /** Currently active model ID */
46
+ activeModelId?: string;
47
+ /** Whether the dialog is active for keyboard input */
48
+ isActive?: boolean;
49
+ /** Array of models to display */
50
+ models: ModelItem[];
51
+ /** Callback when dialog is cancelled */
52
+ onCancel: () => void;
53
+ /** Callback when a model is selected */
54
+ onSelect: (model: ModelItem) => void;
55
+ /** Callback when favorite is toggled */
56
+ onToggleFavorite?: (model: ModelItem) => void;
57
+ /** Provider name for title */
58
+ providerName?: string;
59
+ }
60
+ /**
61
+ * ModelDialog displays a list of models for selection.
62
+ */
63
+ export declare const ModelDialog: React.FC<ModelDialogProps>;
@@ -0,0 +1,89 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ModelDialog Component
4
+ *
5
+ * Interactive dialog for selecting LLM models.
6
+ * Features:
7
+ * - Grouped display: Favorites, Recent, All models
8
+ * - Tags: [Current], [Free], pricing info
9
+ * - Fuzzy search filtering
10
+ * - Favorite toggle with 'f' key
11
+ * - Keyboard navigation
12
+ */
13
+ import { Box, Text } from 'ink';
14
+ import { useMemo } from 'react';
15
+ import { useTheme } from '../contexts/theme-context.js';
16
+ import { SelectableList } from './selectable-list.js';
17
+ /**
18
+ * Format pricing for display.
19
+ */
20
+ function formatPricing(pricing) {
21
+ if (!pricing)
22
+ return '';
23
+ const avgPrice = (pricing.inputPerM + pricing.outputPerM) / 2;
24
+ if (avgPrice < 0.01)
25
+ return '$<0.01/M';
26
+ return `$${avgPrice.toFixed(2)}/M`;
27
+ }
28
+ /**
29
+ * Format context length for display.
30
+ */
31
+ function formatContextLength(contextLength) {
32
+ if (!contextLength)
33
+ return '';
34
+ if (contextLength >= 1_000_000) {
35
+ return `${(contextLength / 1_000_000).toFixed(1)}M ctx`;
36
+ }
37
+ if (contextLength >= 1000) {
38
+ return `${Math.round(contextLength / 1000)}K ctx`;
39
+ }
40
+ return `${contextLength} ctx`;
41
+ }
42
+ /**
43
+ * Get group name for a model item.
44
+ */
45
+ function getModelGroup(model) {
46
+ if (model.isFavorite)
47
+ return 'Favorites';
48
+ if (model.isRecent)
49
+ return 'Recent';
50
+ return model.provider ?? 'Models';
51
+ }
52
+ /**
53
+ * ModelDialog displays a list of models for selection.
54
+ */
55
+ export const ModelDialog = ({ activeModelId, isActive = true, models, onCancel, onSelect, onToggleFavorite, providerName = 'Provider', }) => {
56
+ const { theme: { colors } } = useTheme();
57
+ // Sort models: favorites first, then recent, then by provider
58
+ const sortedModels = useMemo(() => [...models].sort((a, b) => {
59
+ // Favorites first
60
+ if (a.isFavorite && !b.isFavorite)
61
+ return -1;
62
+ if (!a.isFavorite && b.isFavorite)
63
+ return 1;
64
+ // Then recent
65
+ if (a.isRecent && !b.isRecent)
66
+ return -1;
67
+ if (!a.isRecent && b.isRecent)
68
+ return 1;
69
+ // Then by provider
70
+ const providerCompare = (a.provider ?? '').localeCompare(b.provider ?? '');
71
+ if (providerCompare !== 0)
72
+ return providerCompare;
73
+ // Then by name
74
+ return a.name.localeCompare(b.name);
75
+ }), [models]);
76
+ // Find current model for the list
77
+ const currentModel = sortedModels.find((m) => m.id === activeModelId);
78
+ // Custom keybinds for favorite toggle
79
+ const keybinds = onToggleFavorite
80
+ ? [
81
+ {
82
+ action: (item) => onToggleFavorite(item),
83
+ key: 'f',
84
+ label: 'Favorite',
85
+ },
86
+ ]
87
+ : [];
88
+ return (_jsx(SelectableList, { currentItem: currentModel, filterKeys: (item) => [item.id, item.name, item.description ?? '', item.provider ?? ''], getCurrentKey: (item) => item.id, groupBy: getModelGroup, isActive: isActive, items: sortedModels, keybinds: keybinds, keyExtractor: (item) => item.id, onCancel: onCancel, onSelect: (item) => onSelect(item), renderItem: (item, isActive, isCurrent) => (_jsxs(Box, { gap: 2, children: [_jsx(Text, { backgroundColor: isActive ? colors.dimText : undefined, color: isActive ? colors.text : colors.text, children: item.name.padEnd(30) }), _jsxs(Box, { gap: 1, children: [isCurrent && (_jsx(Text, { color: colors.primary, children: "(Current)" })), item.isFree && !isCurrent && (_jsx(Text, { color: colors.primary, children: "[Free]" })), item.isFavorite && !isCurrent && (_jsx(Text, { color: colors.warning, children: "\u2605" }))] }), _jsxs(Box, { gap: 1, children: [item.pricing && !item.isFree && (_jsx(Text, { color: colors.dimText, children: formatPricing(item.pricing) })), item.contextLength && (_jsx(Text, { color: colors.dimText, children: formatContextLength(item.contextLength) }))] })] })), searchPlaceholder: "Search models...", title: `Select Model - ${providerName}` }));
89
+ };
@@ -77,7 +77,10 @@ export const OnboardingFlow = ({ availableHeight, onInitComplete }) => {
77
77
  const renderCurateContent = () => {
78
78
  // Show execution progress if curate is running
79
79
  if (curateLog) {
80
- return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsx(LogItem, { heights: { ...messageItem, maxContentLines: maxOutputLines }, log: curateLog }), hasCurated && !curateAcknowledged && (_jsx(EnterPrompt, { action: "continue", active: mode === 'activity' && currentStep === 'curate', onEnter: () => setCurateAcknowledged(true) }))] }));
80
+ return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsx(LogItem, { heights: {
81
+ ...messageItem,
82
+ maxContentLines: maxOutputLines
83
+ }, isExpand: false, isSelected: true, log: curateLog, shouldShowExpand: false }), hasCurated && !curateAcknowledged && (_jsx(EnterPrompt, { action: "continue", active: mode === 'activity' && currentStep === 'curate', onEnter: () => setCurateAcknowledged(true) }))] }));
81
84
  }
82
85
  // Show copyable prompt when waiting
83
86
  return (_jsxs(Box, { backgroundColor: colors.bg2, flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Text, { color: colors.text, wrap: "wrap", children: "Try saying this to your AI Agent:" }), _jsx(Box, { marginBottom: 1, paddingLeft: 4, children: _jsx(Text, { color: colors.primary, wrap: "wrap", children: CURATE_PROMPT }) }), _jsxs(Text, { children: [_jsx(CopyablePrompt, { buttonLabel: "[ctrl+y] to copy", isActive: mode === 'activity' && currentStep === 'curate', textToCopy: CURATE_PROMPT }), _jsx(Text, { color: colors.dimText, children: " | [Esc] to skip onboarding" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.dimText, children: "Waiting for curate..." }) })] }));
@@ -86,7 +89,10 @@ export const OnboardingFlow = ({ availableHeight, onInitComplete }) => {
86
89
  const renderQueryContent = () => {
87
90
  // Show execution progress if query is running
88
91
  if (queryLog) {
89
- return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsx(LogItem, { heights: { ...messageItem, maxContentLines: maxOutputLines }, log: queryLog }), hasQueried && !queryAcknowledged && (_jsx(EnterPrompt, { action: "continue", active: mode === 'activity' && currentStep === 'query', onEnter: () => setQueryAcknowledged(true) }))] }));
92
+ return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsx(LogItem, { heights: {
93
+ ...messageItem,
94
+ maxContentLines: maxOutputLines
95
+ }, isExpand: false, isSelected: true, log: queryLog, shouldShowExpand: false }), hasQueried && !queryAcknowledged && (_jsx(EnterPrompt, { action: "continue", active: mode === 'activity' && currentStep === 'query', onEnter: () => setQueryAcknowledged(true) }))] }));
90
96
  }
91
97
  // Show copyable prompt when waiting
92
98
  return (_jsxs(Box, { backgroundColor: colors.bg2, flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Text, { color: colors.text, wrap: "wrap", children: "You can now query your memory:" }), _jsx(Box, { marginBottom: 1, paddingLeft: 4, children: _jsx(Text, { color: colors.primary, wrap: "wrap", children: QUERY_PROMPT }) }), _jsxs(Text, { children: [_jsx(CopyablePrompt, { buttonLabel: "[ctrl+y] to copy", isActive: mode === 'activity' && currentStep === 'query', textToCopy: QUERY_PROMPT }), _jsx(Text, { color: colors.dimText, children: " | [Esc] to skip onboarding" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.dimText, children: "Waiting for query..." }) })] }));
@@ -0,0 +1,27 @@
1
+ /**
2
+ * ProviderDialog Component
3
+ *
4
+ * Interactive dialog for selecting and connecting to LLM providers.
5
+ * Shows available providers grouped by category with connection status.
6
+ */
7
+ import React from 'react';
8
+ import { type ProviderDefinition } from '../../core/domain/entities/provider-registry.js';
9
+ /**
10
+ * Props for ProviderDialog.
11
+ */
12
+ export interface ProviderDialogProps {
13
+ /** Currently active provider ID */
14
+ activeProviderId: string;
15
+ /** Set of connected provider IDs */
16
+ connectedProviders: Set<string>;
17
+ /** Whether the dialog is active for keyboard input */
18
+ isActive?: boolean;
19
+ /** Callback when dialog is cancelled */
20
+ onCancel: () => void;
21
+ /** Callback when a provider is selected */
22
+ onSelect: (provider: ProviderDefinition) => void;
23
+ }
24
+ /**
25
+ * ProviderDialog displays a list of available providers for selection.
26
+ */
27
+ export declare const ProviderDialog: React.FC<ProviderDialogProps>;
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ProviderDialog Component
4
+ *
5
+ * Interactive dialog for selecting and connecting to LLM providers.
6
+ * Shows available providers grouped by category with connection status.
7
+ */
8
+ import { Box, Text } from 'ink';
9
+ import { useMemo } from 'react';
10
+ import { getProvidersGroupedByCategory, } from '../../core/domain/entities/provider-registry.js';
11
+ import { useTheme } from '../contexts/theme-context.js';
12
+ import { SelectableList } from './selectable-list.js';
13
+ /**
14
+ * ProviderDialog displays a list of available providers for selection.
15
+ */
16
+ export const ProviderDialog = ({ activeProviderId, connectedProviders, isActive = true, onCancel, onSelect, }) => {
17
+ const { theme: { colors } } = useTheme();
18
+ // Get providers with connection status
19
+ const providerItems = useMemo(() => {
20
+ const { other, popular } = getProvidersGroupedByCategory();
21
+ const allProviders = [...popular, ...other];
22
+ return allProviders.map((provider) => ({
23
+ ...provider,
24
+ isConnected: connectedProviders.has(provider.id),
25
+ isCurrent: provider.id === activeProviderId,
26
+ }));
27
+ }, [activeProviderId, connectedProviders]);
28
+ // Find current provider for the list
29
+ const currentProvider = providerItems.find((p) => p.isCurrent);
30
+ return (_jsx(SelectableList, { currentItem: currentProvider, filterKeys: (item) => [item.id, item.name, item.description], getCurrentKey: (item) => item.id, groupBy: (item) => (item.category === 'popular' ? 'Popular' : 'Other'), isActive: isActive, items: providerItems, keyExtractor: (item) => item.id, onCancel: onCancel, onSelect: (item) => onSelect(item), renderItem: (item, isActive, isCurrent) => (_jsxs(Box, { gap: 2, children: [_jsx(Text, { backgroundColor: isActive ? colors.dimText : undefined, color: isActive ? colors.text : colors.text, children: item.name.padEnd(15) }), _jsx(Text, { color: colors.dimText, children: item.description }), item.isConnected && !isCurrent && (_jsx(Text, { color: colors.primary, children: "[Connected]" })), isCurrent && (_jsx(Text, { color: colors.primary, children: "(Current)" }))] })), searchPlaceholder: "Search providers...", title: "Connect a Provider" }));
31
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Reasoning Text Component
3
+ *
4
+ * Displays LLM thinking/reasoning content with distinct visual styling.
5
+ * Following OpenCode's pattern: muted colors, left border, "Thinking:" prefix.
6
+ */
7
+ import React from 'react';
8
+ interface ReasoningTextProps {
9
+ /** The accumulated reasoning content to display */
10
+ content: string;
11
+ /** Whether reasoning is still in progress */
12
+ isStreaming: boolean;
13
+ /** Maximum lines to display (0 = unlimited) */
14
+ maxLines?: number;
15
+ }
16
+ /**
17
+ * Component that displays thinking/reasoning content with visual distinction.
18
+ *
19
+ * Features:
20
+ * - Left border to visually separate from regular content
21
+ * - Muted/dim colors to indicate background reasoning
22
+ * - "Thinking:" prefix for clarity
23
+ * - Animated spinner during streaming
24
+ */
25
+ export declare const ReasoningText: React.FC<ReasoningTextProps>;
26
+ export {};
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Reasoning Text Component
4
+ *
5
+ * Displays LLM thinking/reasoning content with distinct visual styling.
6
+ * Following OpenCode's pattern: muted colors, left border, "Thinking:" prefix.
7
+ */
8
+ import { Box, Text } from 'ink';
9
+ import Spinner from 'ink-spinner';
10
+ import { memo } from 'react';
11
+ import { useTheme } from '../hooks/index.js';
12
+ /**
13
+ * Memoized spinner component to prevent unnecessary re-renders
14
+ */
15
+ const ThinkingSpinner = memo(() => {
16
+ const { theme: { colors }, } = useTheme();
17
+ return (_jsx(Text, { color: colors.dimText, children: _jsx(Spinner, { type: "dots" }) }));
18
+ });
19
+ ThinkingSpinner.displayName = 'ThinkingSpinner';
20
+ /**
21
+ * Component that displays thinking/reasoning content with visual distinction.
22
+ *
23
+ * Features:
24
+ * - Left border to visually separate from regular content
25
+ * - Muted/dim colors to indicate background reasoning
26
+ * - "Thinking:" prefix for clarity
27
+ * - Animated spinner during streaming
28
+ */
29
+ export const ReasoningText = memo(({ content, isStreaming, maxLines = 0 }) => {
30
+ const { theme: { colors }, } = useTheme();
31
+ // Truncate content if maxLines is specified
32
+ const displayContent = maxLines > 0 ? truncateToLines(content, maxLines) : content;
33
+ // Don't render anything if no content
34
+ if (!content) {
35
+ return isStreaming ? (_jsxs(Box, { children: [_jsx(ThinkingSpinner, {}), _jsx(Text, { color: colors.dimText, children: " Thinking..." })] })) : null;
36
+ }
37
+ return (_jsxs(Box, { borderColor: colors.border, borderLeft: true, borderStyle: "single", flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.dimText, italic: true, children: " Thinking:" }), isStreaming && (_jsxs(Text, { color: colors.dimText, children: [' ', _jsx(ThinkingSpinner, {})] }))] }), _jsx(Text, { color: colors.dimText, wrap: "wrap", children: displayContent })] }));
38
+ });
39
+ ReasoningText.displayName = 'ReasoningText';
40
+ /**
41
+ * Truncate content to specified number of lines
42
+ */
43
+ function truncateToLines(content, maxLines) {
44
+ const lines = content.split('\n');
45
+ if (lines.length <= maxLines) {
46
+ return content;
47
+ }
48
+ return lines.slice(0, maxLines).join('\n') + '\n...';
49
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * SelectableList Component
3
+ *
4
+ * An interactive list with selection, search, grouping, and keyboard navigation.
5
+ * Inspired by OpenCode's List component patterns.
6
+ *
7
+ * Features:
8
+ * - Keyboard navigation (↑/↓, j/k, Enter, Esc)
9
+ * - Fuzzy search filtering
10
+ * - Grouping with headers
11
+ * - Current item indicator (●)
12
+ * - Customizable item rendering
13
+ */
14
+ import React from 'react';
15
+ /**
16
+ * Props for SelectableList component.
17
+ */
18
+ export interface SelectableListProps<T> {
19
+ /** Available height in lines */
20
+ availableHeight?: number;
21
+ /** Current/selected item (shows ● indicator) */
22
+ currentItem?: T;
23
+ /** Keys to use for filtering (searched with fuzzy match) */
24
+ filterKeys: (item: T) => string[];
25
+ /** Function to get item key for comparison with currentItem */
26
+ getCurrentKey?: (item: T) => string;
27
+ /** Optional grouping function */
28
+ groupBy?: (item: T) => string;
29
+ /** Initial search value */
30
+ initialSearch?: string;
31
+ /** Whether keyboard input is active */
32
+ isActive?: boolean;
33
+ /** Array of items to display */
34
+ items: T[];
35
+ /** Custom keybinds */
36
+ keybinds?: Array<{
37
+ action: (item: T) => void;
38
+ key: string;
39
+ label: string;
40
+ }>;
41
+ /** Function to get unique key for each item */
42
+ keyExtractor: (item: T) => string;
43
+ /** Callback when selection is cancelled (Esc) */
44
+ onCancel?: () => void;
45
+ /** Callback when an item is selected */
46
+ onSelect: (item: T) => void;
47
+ /** Function to render each item */
48
+ renderItem: (item: T, isActive: boolean, isCurrent: boolean) => React.ReactNode;
49
+ /** Placeholder for search input */
50
+ searchPlaceholder?: string;
51
+ /** Title for the list */
52
+ title?: string;
53
+ }
54
+ export declare function SelectableList<T>({ availableHeight, currentItem, filterKeys, getCurrentKey, groupBy, initialSearch, isActive, items, keybinds, keyExtractor, onCancel, onSelect, renderItem, searchPlaceholder, title, }: SelectableListProps<T>): React.ReactElement;
@@ -0,0 +1,180 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * SelectableList Component
4
+ *
5
+ * An interactive list with selection, search, grouping, and keyboard navigation.
6
+ * Inspired by OpenCode's List component patterns.
7
+ *
8
+ * Features:
9
+ * - Keyboard navigation (↑/↓, j/k, Enter, Esc)
10
+ * - Fuzzy search filtering
11
+ * - Grouping with headers
12
+ * - Current item indicator (●)
13
+ * - Customizable item rendering
14
+ */
15
+ import { Box, Text, useInput } from 'ink';
16
+ import { useCallback, useEffect, useMemo, useState } from 'react';
17
+ import { useTheme } from '../contexts/theme-context.js';
18
+ /**
19
+ * Simple fuzzy match function.
20
+ * Returns true if all characters in the search string appear in order in the target.
21
+ */
22
+ function fuzzyMatch(search, target) {
23
+ const searchLower = search.toLowerCase();
24
+ const targetLower = target.toLowerCase();
25
+ let searchIndex = 0;
26
+ for (let i = 0; i < targetLower.length && searchIndex < searchLower.length; i++) {
27
+ if (targetLower[i] === searchLower[searchIndex]) {
28
+ searchIndex++;
29
+ }
30
+ }
31
+ return searchIndex === searchLower.length;
32
+ }
33
+ const MAX_VISIBLE_ITEMS = 10;
34
+ export function SelectableList({ availableHeight = MAX_VISIBLE_ITEMS, currentItem, filterKeys, getCurrentKey, groupBy, initialSearch = '', isActive = true, items, keybinds = [], keyExtractor, onCancel, onSelect, renderItem, searchPlaceholder = 'Search...', title, }) {
35
+ const { theme: { colors } } = useTheme();
36
+ const [searchValue, setSearchValue] = useState(initialSearch);
37
+ const [selectedIndex, setSelectedIndex] = useState(0);
38
+ // Filter items based on search
39
+ const filteredItems = useMemo(() => {
40
+ if (!searchValue.trim()) {
41
+ return items;
42
+ }
43
+ return items.filter((item) => {
44
+ const searchableStrings = filterKeys(item);
45
+ return searchableStrings.some((str) => fuzzyMatch(searchValue, str));
46
+ });
47
+ }, [items, searchValue, filterKeys]);
48
+ // Group items if groupBy is provided
49
+ const groupedItems = useMemo(() => {
50
+ if (!groupBy) {
51
+ return [{ group: '', items: filteredItems }];
52
+ }
53
+ const groups = new Map();
54
+ for (const item of filteredItems) {
55
+ const group = groupBy(item);
56
+ if (!groups.has(group)) {
57
+ groups.set(group, []);
58
+ }
59
+ groups.get(group).push(item);
60
+ }
61
+ return [...groups.entries()].map(([group, groupItems]) => ({
62
+ group,
63
+ items: groupItems,
64
+ }));
65
+ }, [filteredItems, groupBy]);
66
+ // Flat list of items for navigation
67
+ const flatItems = useMemo(() => groupedItems.flatMap((g) => g.items), [groupedItems]);
68
+ // Reset selection when items change
69
+ useEffect(() => {
70
+ setSelectedIndex(0);
71
+ }, [searchValue]);
72
+ // Clamp selected index
73
+ useEffect(() => {
74
+ if (selectedIndex >= flatItems.length) {
75
+ setSelectedIndex(Math.max(0, flatItems.length - 1));
76
+ }
77
+ }, [flatItems.length, selectedIndex]);
78
+ const moveUp = useCallback(() => {
79
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
80
+ }, []);
81
+ const moveDown = useCallback(() => {
82
+ setSelectedIndex((prev) => Math.min(flatItems.length - 1, prev + 1));
83
+ }, [flatItems.length]);
84
+ const selectCurrent = useCallback(() => {
85
+ if (flatItems.length > 0 && selectedIndex < flatItems.length) {
86
+ onSelect(flatItems[selectedIndex]);
87
+ }
88
+ }, [flatItems, selectedIndex, onSelect]);
89
+ const handleNavigation = useCallback((input, key) => {
90
+ if (key.upArrow || input === 'k') {
91
+ moveUp();
92
+ return true;
93
+ }
94
+ if (key.downArrow || input === 'j') {
95
+ moveDown();
96
+ return true;
97
+ }
98
+ if (input === 'g' && !key.ctrl && !key.meta) {
99
+ setSelectedIndex(0);
100
+ return true;
101
+ }
102
+ if (input === 'G') {
103
+ setSelectedIndex(Math.max(0, flatItems.length - 1));
104
+ return true;
105
+ }
106
+ return false;
107
+ }, [flatItems.length, moveDown, moveUp]);
108
+ const handleCustomKeybinds = useCallback((input) => {
109
+ for (const keybind of keybinds) {
110
+ if (input === keybind.key && flatItems.length > 0) {
111
+ keybind.action(flatItems[selectedIndex]);
112
+ return true;
113
+ }
114
+ }
115
+ return false;
116
+ }, [flatItems, keybinds, selectedIndex]);
117
+ // Handle keyboard input
118
+ useInput((input, key) => {
119
+ // Navigation
120
+ if (handleNavigation(input, key)) {
121
+ return;
122
+ }
123
+ // Selection
124
+ if (key.return) {
125
+ selectCurrent();
126
+ return;
127
+ }
128
+ // Cancel
129
+ if (key.escape) {
130
+ onCancel?.();
131
+ return;
132
+ }
133
+ // Backspace for search
134
+ if (key.backspace || key.delete) {
135
+ setSearchValue((prev) => prev.slice(0, -1));
136
+ return;
137
+ }
138
+ // Custom keybinds
139
+ if (handleCustomKeybinds(input)) {
140
+ return;
141
+ }
142
+ // Type to search (printable characters)
143
+ if (input && input.length === 1 && !key.ctrl && !key.meta) {
144
+ setSearchValue((prev) => prev + input);
145
+ }
146
+ }, { isActive });
147
+ // Calculate visible window
148
+ const { visibleGroups, windowStart } = useMemo(() => {
149
+ if (flatItems.length === 0) {
150
+ return { visibleGroups: groupedItems, windowStart: 0 };
151
+ }
152
+ // Calculate how many items fit
153
+ const maxItems = Math.max(1, availableHeight - 4); // Reserve space for title, search, keybinds
154
+ // Center the selected item in the window
155
+ const halfWindow = Math.floor(maxItems / 2);
156
+ let start = selectedIndex - halfWindow;
157
+ start = Math.max(0, start);
158
+ start = Math.min(start, Math.max(0, flatItems.length - maxItems));
159
+ const visibleItemKeys = new Set(flatItems.slice(start, start + maxItems).map((item) => keyExtractor(item)));
160
+ // Filter groups to only include visible items
161
+ const visible = groupedItems
162
+ .map((g) => ({
163
+ group: g.group,
164
+ items: g.items.filter((item) => visibleItemKeys.has(keyExtractor(item))),
165
+ }))
166
+ .filter((g) => g.items.length > 0);
167
+ return { visibleGroups: visible, windowStart: start };
168
+ }, [groupedItems, flatItems, selectedIndex, availableHeight, keyExtractor]);
169
+ const hasMoreAbove = windowStart > 0;
170
+ const hasMoreBelow = windowStart + (availableHeight - 4) < flatItems.length;
171
+ // Get the key of the current item for comparison
172
+ const currentKey = currentItem && getCurrentKey ? getCurrentKey(currentItem) : undefined;
173
+ return (_jsxs(Box, { borderColor: colors.border, borderStyle: "single", flexDirection: "column", paddingX: 1, children: [title && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: colors.text, children: title }) })), _jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: colors.dimText, children: "\uD83D\uDD0D " }), _jsx(Text, { color: searchValue ? colors.text : colors.dimText, children: searchValue || searchPlaceholder }), _jsx(Text, { color: colors.primary, children: "\u258E" })] }), hasMoreAbove && (_jsx(Box, { justifyContent: "center", children: _jsxs(Text, { color: colors.dimText, children: ["\u2191 ", windowStart, " more above"] }) })), flatItems.length === 0 ? (_jsx(Box, { paddingY: 1, children: _jsx(Text, { color: colors.dimText, children: "No items found" }) })) : (visibleGroups.map((group) => (_jsxs(Box, { flexDirection: "column", children: [group.group && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { bold: true, color: colors.primary, children: ["\u2500\u2500 ", group.group, " \u2500\u2500"] }) })), group.items.map((item) => {
174
+ const key = keyExtractor(item);
175
+ const flatIndex = flatItems.findIndex((i) => keyExtractor(i) === key);
176
+ const isActive = flatIndex === selectedIndex;
177
+ const isCurrent = currentKey !== undefined && getCurrentKey?.(item) === currentKey;
178
+ return (_jsxs(Box, { children: [_jsx(Text, { color: isActive ? colors.primary : colors.text, children: isCurrent ? '● ' : ' ' }), _jsx(Text, { backgroundColor: isActive ? colors.dimText : undefined, color: isActive ? colors.text : colors.text, children: isActive ? '❯ ' : ' ' }), renderItem(item, isActive, isCurrent)] }, key));
179
+ })] }, group.group || '__ungrouped__')))), hasMoreBelow && (_jsx(Box, { justifyContent: "center", children: _jsxs(Text, { color: colors.dimText, children: ["\u2193 ", flatItems.length - windowStart - (availableHeight - 4), " more below"] }) })), _jsxs(Box, { gap: 2, marginTop: 1, children: [_jsxs(Text, { color: colors.dimText, children: [_jsx(Text, { color: colors.text, children: "\u2191\u2193" }), " Navigate"] }), _jsxs(Text, { color: colors.dimText, children: [_jsx(Text, { color: colors.text, children: "Enter" }), " Select"] }), _jsxs(Text, { color: colors.dimText, children: [_jsx(Text, { color: colors.text, children: "Esc" }), " Cancel"] }), keybinds.map((kb) => (_jsxs(Text, { color: colors.dimText, children: [_jsx(Text, { color: colors.text, children: kb.key }), " ", kb.label] }, kb.key)))] })] }));
180
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Streaming Text Component
3
+ *
4
+ * Displays incrementally streaming text content with an optional
5
+ * animated cursor indicator. Used for real-time LLM response display.
6
+ *
7
+ * Pattern inspired by OpenCode's streaming UI implementation.
8
+ */
9
+ import React from 'react';
10
+ interface StreamingTextProps {
11
+ /** The accumulated streaming content to display */
12
+ content: string;
13
+ /** Whether streaming is still in progress */
14
+ isStreaming: boolean;
15
+ /** Maximum lines to display (0 = unlimited) */
16
+ maxLines?: number;
17
+ /** Whether to show the streaming cursor/indicator */
18
+ showCursor?: boolean;
19
+ }
20
+ /**
21
+ * Component that displays streaming text with real-time updates.
22
+ *
23
+ * Features:
24
+ * - Markdown rendering for formatted content
25
+ * - Animated spinner during streaming
26
+ * - Optional line limiting for compact views
27
+ * - Smooth transition when streaming completes
28
+ */
29
+ export declare const StreamingText: React.FC<StreamingTextProps>;
30
+ export {};
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Streaming Text Component
4
+ *
5
+ * Displays incrementally streaming text content with an optional
6
+ * animated cursor indicator. Used for real-time LLM response display.
7
+ *
8
+ * Pattern inspired by OpenCode's streaming UI implementation.
9
+ */
10
+ import { Box, Text } from 'ink';
11
+ import Spinner from 'ink-spinner';
12
+ import { memo } from 'react';
13
+ import { useTheme } from '../hooks/index.js';
14
+ import { Markdown } from './markdown.js';
15
+ /**
16
+ * Memoized spinner component to prevent unnecessary re-renders
17
+ */
18
+ const GeneratingSpinner = memo(() => {
19
+ const { theme: { colors }, } = useTheme();
20
+ return (_jsx(Text, { color: colors.dimText, children: _jsx(Spinner, { type: "dots" }) }));
21
+ });
22
+ GeneratingSpinner.displayName = 'GeneratingSpinner';
23
+ /**
24
+ * Component that displays streaming text with real-time updates.
25
+ *
26
+ * Features:
27
+ * - Markdown rendering for formatted content
28
+ * - Animated spinner during streaming
29
+ * - Optional line limiting for compact views
30
+ * - Smooth transition when streaming completes
31
+ */
32
+ export const StreamingText = memo(({ content, isStreaming, maxLines = 0, showCursor = true, }) => {
33
+ const { theme: { colors }, } = useTheme();
34
+ // Truncate content if maxLines is specified
35
+ const displayContent = maxLines > 0 ? truncateToLines(content, maxLines) : content;
36
+ // Don't render anything if no content
37
+ if (!content) {
38
+ return isStreaming ? (_jsxs(Box, { children: [_jsx(GeneratingSpinner, {}), _jsx(Text, { color: colors.dimText, children: " Generating..." })] })) : null;
39
+ }
40
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Markdown, { children: displayContent }), isStreaming && showCursor && (_jsxs(Box, { marginTop: 1, children: [_jsx(GeneratingSpinner, {}), _jsx(Text, { color: colors.dimText, children: " Generating..." })] }))] }));
41
+ });
42
+ StreamingText.displayName = 'StreamingText';
43
+ /**
44
+ * Truncate content to specified number of lines
45
+ */
46
+ function truncateToLines(content, maxLines) {
47
+ const lines = content.split('\n');
48
+ if (lines.length <= maxLines) {
49
+ return content;
50
+ }
51
+ return lines.slice(0, maxLines).join('\n') + '\n...';
52
+ }