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.
- package/README.md +193 -12
- package/dist/core/domain/cipher/process/types.d.ts +1 -1
- package/dist/core/domain/entities/provider-config.d.ts +92 -0
- package/dist/core/domain/entities/provider-config.js +181 -0
- package/dist/core/domain/entities/provider-registry.d.ts +55 -0
- package/dist/core/domain/entities/provider-registry.js +74 -0
- package/dist/core/domain/errors/headless-prompt-error.d.ts +11 -0
- package/dist/core/domain/errors/headless-prompt-error.js +18 -0
- package/dist/core/interfaces/cipher/i-content-generator.d.ts +30 -0
- package/dist/core/interfaces/cipher/i-content-generator.js +12 -1
- package/dist/core/interfaces/cipher/message-factory.d.ts +4 -1
- package/dist/core/interfaces/cipher/message-factory.js +5 -0
- package/dist/core/interfaces/cipher/message-types.d.ts +19 -1
- package/dist/core/interfaces/i-cogit-pull-service.d.ts +0 -1
- package/dist/core/interfaces/i-memory-retrieval-service.d.ts +0 -1
- package/dist/core/interfaces/i-memory-storage-service.d.ts +0 -2
- package/dist/core/interfaces/i-provider-config-store.d.ts +88 -0
- package/dist/core/interfaces/i-provider-config-store.js +1 -0
- package/dist/core/interfaces/i-provider-keychain-store.d.ts +33 -0
- package/dist/core/interfaces/i-provider-keychain-store.js +1 -0
- package/dist/core/interfaces/i-space-service.d.ts +1 -2
- package/dist/core/interfaces/i-team-service.d.ts +1 -2
- package/dist/core/interfaces/i-user-service.d.ts +1 -2
- package/dist/core/interfaces/usecase/i-curate-use-case.d.ts +2 -0
- package/dist/core/interfaces/usecase/i-init-use-case.d.ts +9 -3
- package/dist/core/interfaces/usecase/i-login-use-case.d.ts +4 -1
- package/dist/core/interfaces/usecase/i-pull-use-case.d.ts +5 -3
- package/dist/core/interfaces/usecase/i-push-use-case.d.ts +6 -4
- package/dist/core/interfaces/usecase/i-query-use-case.d.ts +2 -0
- package/dist/core/interfaces/usecase/i-status-use-case.d.ts +1 -0
- package/dist/infra/cipher/agent/service-initializer.d.ts +1 -1
- package/dist/infra/cipher/agent/service-initializer.js +0 -1
- package/dist/infra/cipher/file-system/file-system-service.js +5 -5
- package/dist/infra/cipher/http/internal-llm-http-service.d.ts +40 -1
- package/dist/infra/cipher/http/internal-llm-http-service.js +153 -4
- package/dist/infra/cipher/llm/formatters/gemini-formatter.js +8 -1
- package/dist/infra/cipher/llm/generators/byterover-content-generator.d.ts +2 -3
- package/dist/infra/cipher/llm/generators/byterover-content-generator.js +20 -11
- package/dist/infra/cipher/llm/generators/openrouter-content-generator.d.ts +1 -0
- package/dist/infra/cipher/llm/generators/openrouter-content-generator.js +26 -0
- package/dist/infra/cipher/llm/internal-llm-service.d.ts +13 -0
- package/dist/infra/cipher/llm/internal-llm-service.js +75 -4
- package/dist/infra/cipher/llm/model-capabilities.d.ts +74 -0
- package/dist/infra/cipher/llm/model-capabilities.js +157 -0
- package/dist/infra/cipher/llm/openrouter-llm-service.d.ts +35 -1
- package/dist/infra/cipher/llm/openrouter-llm-service.js +216 -28
- package/dist/infra/cipher/llm/stream-processor.d.ts +22 -2
- package/dist/infra/cipher/llm/stream-processor.js +78 -4
- package/dist/infra/cipher/llm/thought-parser.d.ts +1 -1
- package/dist/infra/cipher/llm/thought-parser.js +5 -5
- package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.d.ts +49 -0
- package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.js +272 -0
- package/dist/infra/cipher/llm/transformers/reasoning-extractor.d.ts +71 -0
- package/dist/infra/cipher/llm/transformers/reasoning-extractor.js +253 -0
- package/dist/infra/cipher/process/process-service.js +1 -1
- package/dist/infra/cipher/session/chat-session.d.ts +2 -0
- package/dist/infra/cipher/session/chat-session.js +13 -2
- package/dist/infra/cipher/storage/message-storage-service.js +4 -0
- package/dist/infra/cipher/tools/implementations/bash-exec-tool.js +3 -3
- package/dist/infra/cipher/tools/implementations/task-tool.js +1 -1
- package/dist/infra/cogit/http-cogit-pull-service.js +1 -1
- package/dist/infra/cogit/http-cogit-push-service.js +0 -1
- package/dist/infra/http/authenticated-http-client.d.ts +1 -3
- package/dist/infra/http/authenticated-http-client.js +1 -5
- package/dist/infra/http/openrouter-api-client.d.ts +148 -0
- package/dist/infra/http/openrouter-api-client.js +161 -0
- package/dist/infra/mcp/tools/task-result-waiter.js +9 -1
- package/dist/infra/memory/http-memory-retrieval-service.js +1 -1
- package/dist/infra/memory/http-memory-storage-service.js +2 -2
- package/dist/infra/process/agent-worker.js +178 -70
- package/dist/infra/process/inline-agent-executor.d.ts +32 -0
- package/dist/infra/process/inline-agent-executor.js +259 -0
- package/dist/infra/process/transport-handlers.d.ts +25 -4
- package/dist/infra/process/transport-handlers.js +57 -10
- package/dist/infra/repl/commands/connectors-command.js +2 -2
- package/dist/infra/repl/commands/index.js +5 -0
- package/dist/infra/repl/commands/model-command.d.ts +13 -0
- package/dist/infra/repl/commands/model-command.js +212 -0
- package/dist/infra/repl/commands/provider-command.d.ts +13 -0
- package/dist/infra/repl/commands/provider-command.js +181 -0
- package/dist/infra/repl/transport-client-helper.js +6 -2
- package/dist/infra/space/http-space-service.d.ts +1 -1
- package/dist/infra/space/http-space-service.js +2 -2
- package/dist/infra/storage/file-provider-config-store.d.ts +83 -0
- package/dist/infra/storage/file-provider-config-store.js +157 -0
- package/dist/infra/storage/provider-keychain-store.d.ts +37 -0
- package/dist/infra/storage/provider-keychain-store.js +75 -0
- package/dist/infra/storage/token-store.d.ts +4 -3
- package/dist/infra/storage/token-store.js +6 -5
- package/dist/infra/team/http-team-service.d.ts +1 -1
- package/dist/infra/team/http-team-service.js +2 -2
- package/dist/infra/terminal/headless-terminal.d.ts +91 -0
- package/dist/infra/terminal/headless-terminal.js +211 -0
- package/dist/infra/transport/socket-io-transport-client.d.ts +20 -0
- package/dist/infra/transport/socket-io-transport-client.js +88 -1
- package/dist/infra/usecase/curate-use-case.d.ts +40 -1
- package/dist/infra/usecase/curate-use-case.js +176 -15
- package/dist/infra/usecase/init-use-case.d.ts +27 -5
- package/dist/infra/usecase/init-use-case.js +200 -34
- package/dist/infra/usecase/login-use-case.d.ts +10 -8
- package/dist/infra/usecase/login-use-case.js +35 -2
- package/dist/infra/usecase/pull-use-case.d.ts +19 -5
- package/dist/infra/usecase/pull-use-case.js +71 -13
- package/dist/infra/usecase/push-use-case.d.ts +18 -5
- package/dist/infra/usecase/push-use-case.js +81 -14
- package/dist/infra/usecase/query-use-case.d.ts +21 -0
- package/dist/infra/usecase/query-use-case.js +114 -29
- package/dist/infra/usecase/space-list-use-case.js +1 -1
- package/dist/infra/usecase/space-switch-use-case.js +2 -2
- package/dist/infra/usecase/status-use-case.d.ts +36 -0
- package/dist/infra/usecase/status-use-case.js +185 -48
- package/dist/infra/user/http-user-service.d.ts +1 -1
- package/dist/infra/user/http-user-service.js +2 -2
- package/dist/oclif/commands/curate.d.ts +6 -1
- package/dist/oclif/commands/curate.js +24 -3
- package/dist/oclif/commands/init.d.ts +18 -0
- package/dist/oclif/commands/init.js +129 -0
- package/dist/oclif/commands/login.d.ts +9 -0
- package/dist/oclif/commands/login.js +45 -0
- package/dist/oclif/commands/pull.d.ts +16 -0
- package/dist/oclif/commands/pull.js +78 -0
- package/dist/oclif/commands/push.d.ts +17 -0
- package/dist/oclif/commands/push.js +87 -0
- package/dist/oclif/commands/query.d.ts +6 -1
- package/dist/oclif/commands/query.js +29 -4
- package/dist/oclif/commands/status.d.ts +5 -1
- package/dist/oclif/commands/status.js +17 -5
- package/dist/resources/tools/bash_exec.txt +1 -1
- package/dist/tui/components/api-key-dialog.d.ts +39 -0
- package/dist/tui/components/api-key-dialog.js +94 -0
- package/dist/tui/components/execution/execution-changes.d.ts +3 -1
- package/dist/tui/components/execution/execution-changes.js +4 -4
- package/dist/tui/components/execution/execution-content.d.ts +1 -1
- package/dist/tui/components/execution/execution-content.js +4 -12
- package/dist/tui/components/execution/execution-input.js +1 -1
- package/dist/tui/components/execution/execution-progress.d.ts +10 -13
- package/dist/tui/components/execution/execution-progress.js +70 -17
- package/dist/tui/components/execution/execution-reasoning.d.ts +16 -0
- package/dist/tui/components/execution/execution-reasoning.js +34 -0
- package/dist/tui/components/execution/execution-tool.d.ts +23 -0
- package/dist/tui/components/execution/execution-tool.js +125 -0
- package/dist/tui/components/execution/expanded-log-view.js +3 -3
- package/dist/tui/components/execution/log-item.d.ts +2 -0
- package/dist/tui/components/execution/log-item.js +6 -4
- package/dist/tui/components/index.d.ts +2 -0
- package/dist/tui/components/index.js +2 -0
- package/dist/tui/components/inline-prompts/inline-select.js +3 -2
- package/dist/tui/components/model-dialog.d.ts +63 -0
- package/dist/tui/components/model-dialog.js +89 -0
- package/dist/tui/components/onboarding/onboarding-flow.js +8 -2
- package/dist/tui/components/provider-dialog.d.ts +27 -0
- package/dist/tui/components/provider-dialog.js +31 -0
- package/dist/tui/components/reasoning-text.d.ts +26 -0
- package/dist/tui/components/reasoning-text.js +49 -0
- package/dist/tui/components/selectable-list.d.ts +54 -0
- package/dist/tui/components/selectable-list.js +180 -0
- package/dist/tui/components/streaming-text.d.ts +30 -0
- package/dist/tui/components/streaming-text.js +52 -0
- package/dist/tui/contexts/tasks-context.d.ts +15 -0
- package/dist/tui/contexts/tasks-context.js +224 -40
- package/dist/tui/contexts/theme-context.d.ts +1 -0
- package/dist/tui/contexts/theme-context.js +3 -2
- package/dist/tui/hooks/use-activity-logs.js +7 -1
- package/dist/tui/hooks/use-auth-polling.js +1 -1
- package/dist/tui/types/messages.d.ts +32 -5
- package/dist/tui/utils/index.d.ts +1 -1
- package/dist/tui/utils/index.js +1 -1
- package/dist/tui/utils/log.d.ts +0 -9
- package/dist/tui/utils/log.js +2 -53
- package/dist/tui/views/command-view.js +4 -1
- package/dist/utils/environment-detector.d.ts +15 -0
- package/dist/utils/environment-detector.js +62 -1
- package/oclif.manifest.json +287 -5
- 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
|
-
|
|
33
|
-
|
|
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: {
|
|
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: {
|
|
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
|
+
}
|