osai-agent 4.0.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/LICENSE +7 -0
- package/package.json +72 -0
- package/src/agent/context.js +141 -0
- package/src/agent/loop/context-summary.js +196 -0
- package/src/agent/loop/directory-utils.js +102 -0
- package/src/agent/loop/local.js +196 -0
- package/src/agent/loop/loop-detection.js +288 -0
- package/src/agent/loop/stream-parser.js +515 -0
- package/src/agent/loop/tool-executor.js +470 -0
- package/src/agent/loop/verification.js +263 -0
- package/src/agent/loop/websocket.js +80 -0
- package/src/agent/prompt.js +259 -0
- package/src/agent/react-loop.js +697 -0
- package/src/agent/subagent.js +263 -0
- package/src/commands/config.js +53 -0
- package/src/commands/connect.js +190 -0
- package/src/commands/devices.js +121 -0
- package/src/commands/login.js +77 -0
- package/src/commands/logout.js +31 -0
- package/src/commands/mcp.js +258 -0
- package/src/commands/provider.js +633 -0
- package/src/commands/register.js +74 -0
- package/src/commands/run.js +150 -0
- package/src/commands/search.js +64 -0
- package/src/commands/session.js +57 -0
- package/src/commands/skills.js +54 -0
- package/src/commands/stop-subagent.js +58 -0
- package/src/index.js +208 -0
- package/src/llm/direct.js +317 -0
- package/src/memory/store.js +215 -0
- package/src/mock-readline.js +27 -0
- package/src/parser/dependencies.js +71 -0
- package/src/parser/markdown.js +505 -0
- package/src/parser/stream.js +96 -0
- package/src/prompts/modes/CODING.js +160 -0
- package/src/prompts/modes/GENERAL.js +105 -0
- package/src/prompts/modes/NETWORK.js +69 -0
- package/src/prompts/modes/SSH.js +53 -0
- package/src/prompts/systemPrompt.js +85 -0
- package/src/safety/check.js +210 -0
- package/src/services/crypto.js +78 -0
- package/src/services/executor.js +68 -0
- package/src/services/history.js +58 -0
- package/src/services/server-url.js +11 -0
- package/src/services/session.js +194 -0
- package/src/services/ssh.js +176 -0
- package/src/services/websocket.js +112 -0
- package/src/skills/loader.js +231 -0
- package/src/tools/browser.js +434 -0
- package/src/tools/local.js +1254 -0
- package/src/tools/mcp-client.js +209 -0
- package/src/tools/registry.js +132 -0
- package/src/tools/search-providers.js +237 -0
- package/src/tools/ssh.js +74 -0
- package/src/ui/App.js +2031 -0
- package/src/ui/animation.js +47 -0
- package/src/ui/components/AskUserDialog.js +33 -0
- package/src/ui/components/ConfirmationDialog.js +45 -0
- package/src/ui/components/DiffView.js +201 -0
- package/src/ui/components/Header.js +157 -0
- package/src/ui/components/HistoryPicker.js +130 -0
- package/src/ui/components/InputShell.js +22 -0
- package/src/ui/components/MessageHistory.js +1200 -0
- package/src/ui/components/ModalPanel.js +40 -0
- package/src/ui/components/ModePicker.js +161 -0
- package/src/ui/components/PlanDialog.js +48 -0
- package/src/ui/components/ProviderMenu.js +1095 -0
- package/src/ui/components/SavePicker.js +106 -0
- package/src/ui/components/SelectMenu.js +194 -0
- package/src/ui/components/SlashMenu.js +168 -0
- package/src/ui/components/SubagentPanel.js +138 -0
- package/src/ui/components/TextInputSafe.js +117 -0
- package/src/ui/components/TodoPanel.js +54 -0
- package/src/ui/components/ToolExecution.js +261 -0
- package/src/ui/components/TranscriptViewport.js +99 -0
- package/src/ui/diff.js +249 -0
- package/src/ui/h.js +7 -0
- package/src/ui/mouse-scroll.js +63 -0
- package/src/ui/slash-picker.js +58 -0
- package/src/ui/terminal.js +41 -0
- package/src/ui/theme.js +5 -0
- package/src/ui/welcome.js +12 -0
- package/src/utils/constants.js +231 -0
- package/src/utils/helpers.js +154 -0
- package/src/utils/logger.js +81 -0
- package/src/utils/sound.js +33 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Wrapper around ink-text-input that adds Ctrl+O to the list of keys that
|
|
2
|
+
// should NOT be inserted into the input (so our root useInput in App.js can
|
|
3
|
+
// catch it for the "expand thought" action).
|
|
4
|
+
//
|
|
5
|
+
// This is a thin clone of the upstream source with one extra condition.
|
|
6
|
+
// Keeping a local copy avoids forking the dependency and is the only way to
|
|
7
|
+
// make Ctrl+O work while the user is typing in the input.
|
|
8
|
+
|
|
9
|
+
import React, { useState, useEffect } from 'react';
|
|
10
|
+
import { Text, useInput } from 'ink';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { isOnlySgrMouseInput } from '../mouse-scroll.js';
|
|
13
|
+
|
|
14
|
+
function TextInputSafe({ value: originalValue, placeholder = '', focus = true, mask, highlightPastedText = false, showCursor = true, onChange, onSubmit }) {
|
|
15
|
+
const [state, setState] = useState({
|
|
16
|
+
cursorOffset: (originalValue || '').length,
|
|
17
|
+
cursorWidth: 0,
|
|
18
|
+
});
|
|
19
|
+
const { cursorOffset, cursorWidth } = state;
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
setState(previousState => {
|
|
22
|
+
if (!focus || !showCursor) return previousState;
|
|
23
|
+
const newValue = originalValue || '';
|
|
24
|
+
if (previousState.cursorOffset > newValue.length - 1) {
|
|
25
|
+
return { cursorOffset: newValue.length, cursorWidth: 0 };
|
|
26
|
+
}
|
|
27
|
+
return previousState;
|
|
28
|
+
});
|
|
29
|
+
}, [originalValue, focus, showCursor]);
|
|
30
|
+
|
|
31
|
+
const cursorActualWidth = highlightPastedText ? cursorWidth : 0;
|
|
32
|
+
const value = mask ? mask.repeat(originalValue.length) : originalValue;
|
|
33
|
+
let renderedValue = value;
|
|
34
|
+
let renderedPlaceholder = placeholder ? chalk.grey(placeholder) : undefined;
|
|
35
|
+
if (showCursor && focus) {
|
|
36
|
+
renderedPlaceholder =
|
|
37
|
+
placeholder.length > 0
|
|
38
|
+
? chalk.inverse(placeholder[0]) + chalk.grey(placeholder.slice(1))
|
|
39
|
+
: chalk.inverse(' ');
|
|
40
|
+
renderedValue = value.length > 0 ? '' : chalk.inverse(' ');
|
|
41
|
+
let i = 0;
|
|
42
|
+
for (const char of value) {
|
|
43
|
+
renderedValue +=
|
|
44
|
+
i >= cursorOffset - cursorActualWidth && i <= cursorOffset
|
|
45
|
+
? chalk.inverse(char)
|
|
46
|
+
: char;
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
if (value.length > 0 && cursorOffset === value.length) {
|
|
50
|
+
renderedValue += chalk.inverse(' ');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
useInput((input, key) => {
|
|
55
|
+
// [FIX-ctrl-o] Ajout de (key.ctrl && input === 'o') à la liste des keys
|
|
56
|
+
// filtrées : Ctrl+O ne doit pas être inséré comme un caractère texte dans
|
|
57
|
+
// l'input. Il remonte au useInput racine qui gère l'action "déplier le
|
|
58
|
+
// thought".
|
|
59
|
+
if (
|
|
60
|
+
key.upArrow ||
|
|
61
|
+
key.downArrow ||
|
|
62
|
+
(key.ctrl && input === 'c') ||
|
|
63
|
+
key.tab ||
|
|
64
|
+
(key.shift && key.tab) ||
|
|
65
|
+
(key.ctrl && input === 'o')
|
|
66
|
+
) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Filtre les séquences SGR souris qui fuient depuis parseKeypress
|
|
70
|
+
// (ex: [<64;86;11M ou plusieurs concaténées) — Ink les passe comme texte
|
|
71
|
+
if (isOnlySgrMouseInput(input)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (key.return) {
|
|
75
|
+
if (onSubmit) onSubmit(originalValue);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
let nextCursorOffset = cursorOffset;
|
|
79
|
+
let nextValue = originalValue;
|
|
80
|
+
let nextCursorWidth = 0;
|
|
81
|
+
if (key.leftArrow) {
|
|
82
|
+
if (showCursor) nextCursorOffset--;
|
|
83
|
+
} else if (key.rightArrow) {
|
|
84
|
+
if (showCursor) nextCursorOffset++;
|
|
85
|
+
} else if (key.backspace || key.delete) {
|
|
86
|
+
if (cursorOffset > 0) {
|
|
87
|
+
nextValue =
|
|
88
|
+
originalValue.slice(0, cursorOffset - 1) +
|
|
89
|
+
originalValue.slice(cursorOffset, originalValue.length);
|
|
90
|
+
nextCursorOffset--;
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
nextValue =
|
|
94
|
+
originalValue.slice(0, cursorOffset) +
|
|
95
|
+
input +
|
|
96
|
+
originalValue.slice(cursorOffset, originalValue.length);
|
|
97
|
+
nextCursorOffset += input.length;
|
|
98
|
+
if (input.length > 1) nextCursorWidth = input.length;
|
|
99
|
+
}
|
|
100
|
+
if (cursorOffset < 0) nextCursorOffset = 0;
|
|
101
|
+
if (cursorOffset > originalValue.length) nextCursorOffset = originalValue.length;
|
|
102
|
+
setState({ cursorOffset: nextCursorOffset, cursorWidth: nextCursorWidth });
|
|
103
|
+
if (nextValue !== originalValue) onChange(nextValue);
|
|
104
|
+
}, { isActive: focus });
|
|
105
|
+
|
|
106
|
+
return React.createElement(Text, null, placeholder
|
|
107
|
+
? value.length > 0
|
|
108
|
+
? renderedValue
|
|
109
|
+
: renderedPlaceholder
|
|
110
|
+
: renderedValue);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default TextInputSafe;
|
|
114
|
+
export function UncontrolledTextInputSafe({ initialValue = '', ...props }) {
|
|
115
|
+
const [value, setValue] = useState(initialValue);
|
|
116
|
+
return React.createElement(TextInputSafe, { ...props, value, onChange: setValue });
|
|
117
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Box, Text } from 'ink';
|
|
2
|
+
import { h } from '../h.js';
|
|
3
|
+
|
|
4
|
+
function normalizeTodos(input) {
|
|
5
|
+
if (!input) return [];
|
|
6
|
+
if (Array.isArray(input)) return input;
|
|
7
|
+
if (typeof input === 'string') {
|
|
8
|
+
const trimmed = input.trim();
|
|
9
|
+
if (!trimmed) return [];
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(trimmed);
|
|
12
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
13
|
+
} catch {
|
|
14
|
+
return [{ text: trimmed, status: 'pending', priority: 'MEDIUM' }];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function statusStyle(status) {
|
|
21
|
+
const s = String(status || '').toLowerCase();
|
|
22
|
+
if (s === 'done' || s === 'completed') return { icon: '[x]', color: '#9ece6a' };
|
|
23
|
+
if (s === 'in_progress' || s === 'doing') return { icon: '[~]', color: '#e0af68' };
|
|
24
|
+
return { icon: '[ ]', color: '#7aa2f7' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function priorityColor(priority) {
|
|
28
|
+
const p = String(priority || '').toUpperCase();
|
|
29
|
+
if (p === 'HIGH') return '#f7768e';
|
|
30
|
+
if (p === 'MEDIUM') return '#e0af68';
|
|
31
|
+
return '#73daca';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function TodoPanel({ todos }) {
|
|
35
|
+
const list = normalizeTodos(todos);
|
|
36
|
+
if (!list.length) return null;
|
|
37
|
+
|
|
38
|
+
return h(Box, { flexDirection: 'column', paddingY: 1, borderStyle: 'single', borderColor: 'blue' },
|
|
39
|
+
h(Text, { color: 'blue', bold: true }, 'Tasks:'),
|
|
40
|
+
h(Box, { flexDirection: 'column', paddingLeft: 2 },
|
|
41
|
+
...list.map((todo, i) => {
|
|
42
|
+
const label = todo.text || todo.description || `Task ${todo.id ?? i + 1}`;
|
|
43
|
+
const status = statusStyle(todo.status);
|
|
44
|
+
const priority = String(todo.priority || 'MEDIUM').toUpperCase();
|
|
45
|
+
return h(Box, { key: `todo_${todo.id ?? i}`, marginTop: i === 0 ? 0 : 0 },
|
|
46
|
+
h(Text, { color: status.color }, `${status.icon} `),
|
|
47
|
+
h(Text, { color: '#c0caf5' }, label),
|
|
48
|
+
h(Text, { color: '#565f89' }, ' '),
|
|
49
|
+
h(Text, { color: priorityColor(priority) }, `[${priority}]`)
|
|
50
|
+
);
|
|
51
|
+
})
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { h } from '../h.js';
|
|
4
|
+
import { highlightCode } from '../../parser/markdown.js';
|
|
5
|
+
import { ENABLE_UI_ANIMATIONS, useAnimationFrame } from '../animation.js';
|
|
6
|
+
|
|
7
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
8
|
+
|
|
9
|
+
const TOOL_COLORS = {
|
|
10
|
+
READ_FILE: '#9ece6a',
|
|
11
|
+
WRITE_FILE: '#e0af68',
|
|
12
|
+
EDIT_FILE: '#e0af68',
|
|
13
|
+
APPEND_FILE: '#e0af68',
|
|
14
|
+
DELETE_FILE: '#f7768e',
|
|
15
|
+
FETCH_URL: '#7dcfff',
|
|
16
|
+
WEB_SEARCH: '#bb9af7',
|
|
17
|
+
LOCAL_CMD: '#f7768e',
|
|
18
|
+
SSH_CMD: '#f7768e',
|
|
19
|
+
GREP: '#73daca',
|
|
20
|
+
GLOB: '#73daca',
|
|
21
|
+
SEARCH_FILE: '#73daca',
|
|
22
|
+
LIST_DIR: '#9ece6a',
|
|
23
|
+
TREE_VIEW: '#9ece6a',
|
|
24
|
+
FILE_INFO: '#9ece6a',
|
|
25
|
+
CREATE_DIR: '#e0af68',
|
|
26
|
+
RUN_SCRIPT: '#f7768e',
|
|
27
|
+
MOVE_FILE: '#e0af68',
|
|
28
|
+
COPY_FILE: '#e0af68',
|
|
29
|
+
GIT: '#f7768e',
|
|
30
|
+
BROWSE: '#7dcfff',
|
|
31
|
+
BROWSE_SEARCH: '#bb9af7',
|
|
32
|
+
BROWSE_EXTRACT: '#7dcfff',
|
|
33
|
+
ASK_USER: '#7aa2f7',
|
|
34
|
+
PLAN_MODE: '#7dcfff',
|
|
35
|
+
DIAG_POST_EDIT: '#e0af68',
|
|
36
|
+
TODO_ADD: '#9ece6a',
|
|
37
|
+
TODO_COMPLETE: '#9ece6a',
|
|
38
|
+
TODO_UPDATE: '#9ece6a',
|
|
39
|
+
TODO_LIST: '#9ece6a',
|
|
40
|
+
TODO_CLEAR: '#f7768e',
|
|
41
|
+
SKILL_LIST: '#bb9af7',
|
|
42
|
+
LOAD_SKILL: '#bb9af7',
|
|
43
|
+
CREATE_SKILL: '#bb9af7',
|
|
44
|
+
TASK: '#7dcfff',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const DEFAULT_COLOR = '#e0af68';
|
|
48
|
+
const OSC8_START = '\u001B]8;;';
|
|
49
|
+
const OSC8_END = '\u001B]8;;\u0007';
|
|
50
|
+
|
|
51
|
+
function getToolColor(name) {
|
|
52
|
+
return TOOL_COLORS[name] || DEFAULT_COLOR;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function truncate(value, max = 60) {
|
|
56
|
+
const s = String(value || '');
|
|
57
|
+
if (s.length <= max) return s;
|
|
58
|
+
return s.slice(0, max - 3) + '...';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function supportsHyperlinks() {
|
|
62
|
+
if (process.env.OSAI_DISABLE_OSC8 === '1') return false;
|
|
63
|
+
if (process.env.OSAI_ENABLE_OSC8 === '1') return true;
|
|
64
|
+
const term = (process.env.TERM || '').toLowerCase();
|
|
65
|
+
if (term === 'dumb') return false;
|
|
66
|
+
if (process.platform === 'win32') {
|
|
67
|
+
return Boolean(process.env.WT_SESSION || process.env.TERM_PROGRAM || term.includes('xterm'));
|
|
68
|
+
}
|
|
69
|
+
return Boolean(
|
|
70
|
+
process.env.WT_SESSION ||
|
|
71
|
+
process.env.KITTY_WINDOW_ID ||
|
|
72
|
+
process.env.VTE_VERSION ||
|
|
73
|
+
process.env.TERM_PROGRAM ||
|
|
74
|
+
process.env.KONSOLE_VERSION
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeUrl(raw) {
|
|
79
|
+
const input = String(raw || '').trim().replace(/[),.;]+$/, '');
|
|
80
|
+
if (!input) return null;
|
|
81
|
+
const candidate = /^www\./i.test(input) ? `https://${input}` : input;
|
|
82
|
+
try {
|
|
83
|
+
const parsed = new URL(candidate);
|
|
84
|
+
if (!['http:', 'https:', 'mailto:'].includes(parsed.protocol)) return null;
|
|
85
|
+
return parsed.toString();
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function linkText(label, rawUrl) {
|
|
92
|
+
const safeUrl = normalizeUrl(rawUrl);
|
|
93
|
+
if (!safeUrl) return label;
|
|
94
|
+
if (!supportsHyperlinks()) return `${label} (${safeUrl})`;
|
|
95
|
+
return `${OSC8_START}${safeUrl}\u0007${label}${OSC8_END}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatTarget(tc) {
|
|
99
|
+
if (!tc) return { text: '', url: null };
|
|
100
|
+
if (tc.path) return { text: truncate(tc.path), url: null };
|
|
101
|
+
if (tc.cmd) return { text: truncate(tc.cmd.replace(/\s+/g, ' ').trim()), url: null };
|
|
102
|
+
if (tc.url) {
|
|
103
|
+
const url = normalizeUrl(tc.url);
|
|
104
|
+
return { text: truncate(url || tc.url), url };
|
|
105
|
+
}
|
|
106
|
+
if (tc.query) return { text: truncate(tc.query), url: null };
|
|
107
|
+
if (tc.pattern) return { text: truncate(tc.pattern), url: null };
|
|
108
|
+
if (tc.source) return { text: truncate(tc.source), url: null };
|
|
109
|
+
if (tc.name) return { text: truncate(tc.name), url: null };
|
|
110
|
+
if (tc.prompt) return { text: truncate(tc.prompt), url: null };
|
|
111
|
+
if (tc.description) return { text: truncate(tc.description), url: null };
|
|
112
|
+
return { text: '', url: null };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function inferCodeLang(text) {
|
|
116
|
+
const src = String(text || '').trim();
|
|
117
|
+
if (!src) return '';
|
|
118
|
+
if ((src.startsWith('{') && src.endsWith('}')) || (src.startsWith('[') && src.endsWith(']'))) {
|
|
119
|
+
try {
|
|
120
|
+
JSON.parse(src);
|
|
121
|
+
return 'json';
|
|
122
|
+
} catch {}
|
|
123
|
+
}
|
|
124
|
+
if (/^\s*(Select-|Get-|Set-|Where-Object|ForEach-Object)\b/m.test(src)) return 'powershell';
|
|
125
|
+
if (/^\s*(npm|node|git|cd|ls|cat|grep|sed|awk|chmod|curl|wget)\b/m.test(src) || /^\s*[$#]\s+/.test(src)) return 'bash';
|
|
126
|
+
if (/\b(import\s+.+\s+from|const\s+\w+\s*=|let\s+\w+\s*=|function\s+\w+\(|=>)\b/.test(src)) return 'javascript';
|
|
127
|
+
if (/\b(def\s+\w+\(|class\s+\w+|from\s+\w+\s+import|import\s+\w+)\b/.test(src)) return 'python';
|
|
128
|
+
return '';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isLikelyCode(text) {
|
|
132
|
+
const src = String(text || '');
|
|
133
|
+
if (!src.trim()) return false;
|
|
134
|
+
if (src.includes('\n')) return true;
|
|
135
|
+
if (/^\s*[\[{]/.test(src) && /[\]}]\s*$/.test(src)) return true;
|
|
136
|
+
return /[;{}()[\]=<>]/.test(src);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function CodeOutput({ text, color = '#c0caf5' }) {
|
|
140
|
+
if (!text) return null;
|
|
141
|
+
const lang = inferCodeLang(text);
|
|
142
|
+
return h(Box, { paddingLeft: 4, flexDirection: 'column', marginTop: 1, borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1 },
|
|
143
|
+
lang ? h(Text, { color: '#565f89' }, lang) : null,
|
|
144
|
+
h(Text, { ansi: true, color }, highlightCode(text, lang))
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function SearchResults({ results }) {
|
|
149
|
+
if (!results || !results.length) return null;
|
|
150
|
+
return h(Box, { flexDirection: 'column', paddingLeft: 4, marginTop: 1 },
|
|
151
|
+
...results.map((item, idx) => {
|
|
152
|
+
const title = item.title || 'Untitled';
|
|
153
|
+
const link = normalizeUrl(item.link || item.url || '');
|
|
154
|
+
const snippet = item.snippet || item.description || '';
|
|
155
|
+
return h(Box, { key: `s_${idx}`, flexDirection: 'column', marginBottom: 1 },
|
|
156
|
+
h(Text, { color: '#c0caf5', bold: true }, `[${idx + 1}] ${title}`),
|
|
157
|
+
link ? h(Text, { ansi: true, color: '#7dcfff', underline: true }, ` ${linkText(link, link)}`) : null,
|
|
158
|
+
snippet ? h(Text, { color: '#9aa5ce' }, ` ${snippet}`) : null
|
|
159
|
+
);
|
|
160
|
+
})
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function ToolItem({ tool }) {
|
|
165
|
+
const frame = useAnimationFrame();
|
|
166
|
+
|
|
167
|
+
const color = getToolColor(tool.name);
|
|
168
|
+
const target = formatTarget(tool.toolCall);
|
|
169
|
+
const isSubagent = tool.toolCall?._subagent;
|
|
170
|
+
const prefix = tool.name === 'TASK' ? 'Subagent' : isSubagent ? 'Subagent tool' : null;
|
|
171
|
+
|
|
172
|
+
return h(Box, { paddingLeft: 2, paddingY: 0 },
|
|
173
|
+
h(Text, { color: '#7aa2f7' }, ENABLE_UI_ANIMATIONS ? ` ${SPINNER_FRAMES[frame % SPINNER_FRAMES.length]} ` : ' … '),
|
|
174
|
+
prefix ? h(Text, { color: '#7dcfff' }, `${prefix} `) : null,
|
|
175
|
+
h(Text, { color, bold: true }, tool.name),
|
|
176
|
+
target.text ? h(Text, { color: '#565f89' }, ': ') : null,
|
|
177
|
+
target.text
|
|
178
|
+
? target.url
|
|
179
|
+
? h(Text, { ansi: true, color: '#7dcfff', underline: true }, linkText(target.text, target.url))
|
|
180
|
+
: h(Text, { color: '#565f89' }, target.text)
|
|
181
|
+
: null
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function ToolResult({ result }) {
|
|
186
|
+
if (!result) return null;
|
|
187
|
+
|
|
188
|
+
const color = getToolColor(result.name);
|
|
189
|
+
const label = result.success ? '✓' : '✗';
|
|
190
|
+
const labelColor = result.success ? '#9ece6a' : '#f7768e';
|
|
191
|
+
const out = String(result.output || '');
|
|
192
|
+
const target = formatTarget(result.toolCall);
|
|
193
|
+
|
|
194
|
+
let display = out;
|
|
195
|
+
let resultsList = null;
|
|
196
|
+
|
|
197
|
+
if (result.name === 'READ_FILE') {
|
|
198
|
+
const lines = out.split('\n');
|
|
199
|
+
display = `${lines[0] || ''} (${Math.max(0, lines.length - 1)} lines)`;
|
|
200
|
+
} else if (out.startsWith('{') || out.startsWith('[')) {
|
|
201
|
+
try {
|
|
202
|
+
const parsed = JSON.parse(out);
|
|
203
|
+
if (parsed?.results && Array.isArray(parsed.results)) {
|
|
204
|
+
resultsList = parsed.results;
|
|
205
|
+
display = '';
|
|
206
|
+
} else {
|
|
207
|
+
display = JSON.stringify(parsed, null, 2);
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
display = out;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return h(Box, { flexDirection: 'column', paddingLeft: 2, paddingY: 0 },
|
|
215
|
+
h(Box,
|
|
216
|
+
h(Text, { color: labelColor, bold: true }, ` ${label} `),
|
|
217
|
+
h(Text, { color }, result.name),
|
|
218
|
+
target.text ? h(Text, { color: '#565f89' }, ': ') : null,
|
|
219
|
+
target.text
|
|
220
|
+
? target.url
|
|
221
|
+
? h(Text, { ansi: true, color: '#7dcfff', underline: true }, linkText(target.text, target.url))
|
|
222
|
+
: h(Text, { color: '#565f89' }, target.text)
|
|
223
|
+
: null
|
|
224
|
+
),
|
|
225
|
+
resultsList ? h(SearchResults, { results: resultsList }) : null,
|
|
226
|
+
result.success && display
|
|
227
|
+
? (() => {
|
|
228
|
+
const shortened = display.length > 500 ? `${display.slice(0, 500)}...` : display;
|
|
229
|
+
return isLikelyCode(shortened)
|
|
230
|
+
? h(CodeOutput, { text: shortened, color: '#c0caf5' })
|
|
231
|
+
: h(Box, { paddingLeft: 4 }, h(Text, { color: '#c0caf5' }, shortened));
|
|
232
|
+
})()
|
|
233
|
+
: null,
|
|
234
|
+
!result.success && out
|
|
235
|
+
? (() => {
|
|
236
|
+
const shortened = out.length > 500 ? `${out.slice(0, 500)}...` : out;
|
|
237
|
+
return isLikelyCode(shortened)
|
|
238
|
+
? h(CodeOutput, { text: shortened, color: '#f7768e' })
|
|
239
|
+
: h(Box, { paddingLeft: 4 }, h(Text, { color: '#f7768e' }, shortened));
|
|
240
|
+
})()
|
|
241
|
+
: null
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function ToolExecution({ activeTools, completedTools }) {
|
|
246
|
+
const hasAny = (activeTools && activeTools.length > 0) || (completedTools && completedTools.length > 0);
|
|
247
|
+
if (!hasAny) return null;
|
|
248
|
+
|
|
249
|
+
return h(Box, { flexDirection: 'column', paddingY: 0 },
|
|
250
|
+
completedTools && completedTools.length > 0
|
|
251
|
+
? h(Box, { flexDirection: 'column', paddingBottom: activeTools && activeTools.length > 0 ? 1 : 0 },
|
|
252
|
+
...completedTools.map(t => h(ToolResult, { key: `${t.id}_done`, result: t }))
|
|
253
|
+
)
|
|
254
|
+
: null,
|
|
255
|
+
activeTools && activeTools.length > 0
|
|
256
|
+
? h(Box, { flexDirection: 'column' },
|
|
257
|
+
...activeTools.map(t => h(ToolItem, { key: t.id, tool: t }))
|
|
258
|
+
)
|
|
259
|
+
: null
|
|
260
|
+
);
|
|
261
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
|
2
|
+
import { Box, Text, useStdout } from 'ink';
|
|
3
|
+
import { ScrollView } from 'ink-scroll-view';
|
|
4
|
+
import { h } from '../h.js';
|
|
5
|
+
import { ExchangeBlock, EventList, UserMessageBlock } from './MessageHistory.js';
|
|
6
|
+
|
|
7
|
+
const SCROLL_BOTTOM_EPSILON = 1;
|
|
8
|
+
|
|
9
|
+
export const TranscriptViewport = forwardRef(function TranscriptViewport({
|
|
10
|
+
height,
|
|
11
|
+
exchanges = [],
|
|
12
|
+
userMessage,
|
|
13
|
+
displayEvents = [],
|
|
14
|
+
columns,
|
|
15
|
+
expandedOutputIndexes,
|
|
16
|
+
expandedThoughtIds,
|
|
17
|
+
thoughtStreaming = false,
|
|
18
|
+
animate = false,
|
|
19
|
+
showDoneSeparator = false,
|
|
20
|
+
TaskSeparator,
|
|
21
|
+
scrollAwayFromLive = false,
|
|
22
|
+
}, ref) {
|
|
23
|
+
const scrollRef = useRef(null);
|
|
24
|
+
const { stdout } = useStdout();
|
|
25
|
+
|
|
26
|
+
useImperativeHandle(ref, () => ({
|
|
27
|
+
scrollBy(delta) {
|
|
28
|
+
scrollRef.current?.scrollBy(delta);
|
|
29
|
+
},
|
|
30
|
+
scrollToBottom() {
|
|
31
|
+
scrollRef.current?.scrollToBottom();
|
|
32
|
+
},
|
|
33
|
+
scrollToTop() {
|
|
34
|
+
scrollRef.current?.scrollToTop();
|
|
35
|
+
},
|
|
36
|
+
remeasure() {
|
|
37
|
+
scrollRef.current?.remeasure();
|
|
38
|
+
},
|
|
39
|
+
isAtBottom() {
|
|
40
|
+
const sv = scrollRef.current;
|
|
41
|
+
if (!sv) return true;
|
|
42
|
+
const bottom = sv.getBottomOffset();
|
|
43
|
+
if (bottom <= 0) return true;
|
|
44
|
+
return sv.getScrollOffset() >= bottom - SCROLL_BOTTOM_EPSILON;
|
|
45
|
+
},
|
|
46
|
+
getViewportHeight() {
|
|
47
|
+
return scrollRef.current?.getViewportHeight() || 1;
|
|
48
|
+
},
|
|
49
|
+
}), []);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const handleResize = () => scrollRef.current?.remeasure();
|
|
53
|
+
stdout?.on?.('resize', handleResize);
|
|
54
|
+
return () => stdout?.off?.('resize', handleResize);
|
|
55
|
+
}, [stdout]);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
scrollRef.current?.remeasure();
|
|
59
|
+
}, [height, exchanges.length, displayEvents.length, userMessage]);
|
|
60
|
+
|
|
61
|
+
if (!height || height < 1) return null;
|
|
62
|
+
|
|
63
|
+
return h(Box, {
|
|
64
|
+
flexDirection: 'column',
|
|
65
|
+
width: '100%',
|
|
66
|
+
height,
|
|
67
|
+
overflow: 'hidden',
|
|
68
|
+
flexShrink: 1,
|
|
69
|
+
minHeight: 0,
|
|
70
|
+
},
|
|
71
|
+
h(ScrollView, { ref: scrollRef, flexGrow: 1, width: '100%' },
|
|
72
|
+
exchanges.map((ex, i) =>
|
|
73
|
+
h(ExchangeBlock, {
|
|
74
|
+
key: `ex_${i}`,
|
|
75
|
+
exchange: ex,
|
|
76
|
+
columns,
|
|
77
|
+
expandedOutputIndexes,
|
|
78
|
+
expandedThoughtIds,
|
|
79
|
+
})
|
|
80
|
+
),
|
|
81
|
+
userMessage ? h(Box, { key: 'current_exchange', flexDirection: 'column' },
|
|
82
|
+
TaskSeparator ? h(TaskSeparator, { columns }) : null,
|
|
83
|
+
h(UserMessageBlock, { message: userMessage }),
|
|
84
|
+
h(Box, { height: 1 }),
|
|
85
|
+
h(EventList, {
|
|
86
|
+
events: displayEvents,
|
|
87
|
+
expandedOutputIndexes,
|
|
88
|
+
thoughtStreaming,
|
|
89
|
+
animate,
|
|
90
|
+
expandedThoughtIds,
|
|
91
|
+
}),
|
|
92
|
+
showDoneSeparator && TaskSeparator ? h(TaskSeparator, { columns }) : null,
|
|
93
|
+
) : null,
|
|
94
|
+
scrollAwayFromLive ? h(Box, { key: 'scroll_hint', paddingLeft: 2, paddingY: 1 },
|
|
95
|
+
h(Text, { color: '#565f89', italic: true }, 'Scrolled up — End or scroll down to follow live')
|
|
96
|
+
) : null
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
});
|