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,47 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export const ENABLE_UI_ANIMATIONS = process.env.OSAI_UI_ANIMATIONS !== '0';
|
|
4
|
+
|
|
5
|
+
const defaultInterval = process.platform !== 'win32' && process.stdout.isTTY ? '800' : '400';
|
|
6
|
+
const requestedInterval = Number.parseInt(process.env.OSAI_UI_ANIMATION_INTERVAL_MS || defaultInterval, 10);
|
|
7
|
+
const TICK_INTERVAL_MS = Number.isFinite(requestedInterval)
|
|
8
|
+
? Math.max(300, requestedInterval)
|
|
9
|
+
: 400;
|
|
10
|
+
// Astuce : augmentez OSAI_UI_ANIMATION_INTERVAL_MS (ex: 2000) pour ralentir les spinners si le terminal tremble
|
|
11
|
+
// Ou désactivez toutes les animations : OSAI_UI_ANIMATIONS=0
|
|
12
|
+
|
|
13
|
+
let frame = 0;
|
|
14
|
+
let timer = null;
|
|
15
|
+
const subscribers = new Set();
|
|
16
|
+
|
|
17
|
+
function startTicker() {
|
|
18
|
+
if (timer || !ENABLE_UI_ANIMATIONS) return;
|
|
19
|
+
timer = setInterval(() => {
|
|
20
|
+
frame += 1;
|
|
21
|
+
for (const notify of subscribers) notify(frame);
|
|
22
|
+
}, TICK_INTERVAL_MS);
|
|
23
|
+
timer.unref?.();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stopTicker() {
|
|
27
|
+
if (timer && subscribers.size === 0) {
|
|
28
|
+
clearInterval(timer);
|
|
29
|
+
timer = null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useAnimationFrame(enabled = true) {
|
|
34
|
+
const [currentFrame, setCurrentFrame] = useState(frame);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!enabled || !ENABLE_UI_ANIMATIONS) return;
|
|
38
|
+
subscribers.add(setCurrentFrame);
|
|
39
|
+
startTicker();
|
|
40
|
+
return () => {
|
|
41
|
+
subscribers.delete(setCurrentFrame);
|
|
42
|
+
stopTicker();
|
|
43
|
+
};
|
|
44
|
+
}, [enabled]);
|
|
45
|
+
|
|
46
|
+
return ENABLE_UI_ANIMATIONS ? currentFrame : 0;
|
|
47
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import TextInput from './TextInputSafe.js';
|
|
4
|
+
import { h } from '../h.js';
|
|
5
|
+
import { InputShell } from './InputShell.js';
|
|
6
|
+
|
|
7
|
+
export function AskUserDialog({ question, options, onSubmit }) {
|
|
8
|
+
const [value, setValue] = useState('');
|
|
9
|
+
const Input = TextInput.default || TextInput;
|
|
10
|
+
|
|
11
|
+
return h(Box, { flexDirection: 'column', paddingY: 1, borderStyle: 'round', borderColor: '#7aa2f7' },
|
|
12
|
+
h(Text, { color: '#7aa2f7', bold: true }, 'Question from agent'),
|
|
13
|
+
h(Box, { marginTop: 1 },
|
|
14
|
+
h(Text, { color: '#c0caf5' }, question)
|
|
15
|
+
),
|
|
16
|
+
options && options.length > 0 ? h(Box, { flexDirection: 'column', marginTop: 1 },
|
|
17
|
+
...options.map((opt, i) =>
|
|
18
|
+
h(Text, { key: i, color: '#9ece6a' }, ` ${i + 1}. ${opt}`)
|
|
19
|
+
)
|
|
20
|
+
) : null,
|
|
21
|
+
h(InputShell, { flexDirection: 'row', marginTop: 1, paddingX: 1 },
|
|
22
|
+
h(Text, { color: '#7aa2f7' }, '> '),
|
|
23
|
+
h(Input, {
|
|
24
|
+
value,
|
|
25
|
+
onChange: setValue,
|
|
26
|
+
onSubmit: () => {
|
|
27
|
+
onSubmit(value);
|
|
28
|
+
setValue('');
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { h } from '../h.js';
|
|
4
|
+
import { InputShell } from './InputShell.js';
|
|
5
|
+
import { isOnlySgrMouseInput } from '../mouse-scroll.js';
|
|
6
|
+
|
|
7
|
+
export function ConfirmationDialog({ promptText, details, onConfirm }) {
|
|
8
|
+
const [value, setValue] = useState('');
|
|
9
|
+
|
|
10
|
+
useInput((input, key) => {
|
|
11
|
+
if (key.return) {
|
|
12
|
+
onConfirm(value);
|
|
13
|
+
setValue('');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (key.backspace || key.delete) {
|
|
17
|
+
setValue(v => v.slice(0, -1));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (!isOnlySgrMouseInput(input)) {
|
|
21
|
+
setValue(v => v + input);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const isDangerous = details?.isDangerous || details?.tier === 'DANGEROUS';
|
|
26
|
+
const tool = details?.tool || '';
|
|
27
|
+
const identifier = details?.identifier || '';
|
|
28
|
+
const description = details?.description || '';
|
|
29
|
+
|
|
30
|
+
const borderColor = isDangerous ? 'red' : 'yellow';
|
|
31
|
+
const headerColor = isDangerous ? '#f7768e' : '#e0af68';
|
|
32
|
+
const toolColor = isDangerous ? '#f7768e' : '#7dcfff';
|
|
33
|
+
|
|
34
|
+
return h(Box, { flexDirection: 'column', paddingY: 1, borderStyle: 'round', borderColor },
|
|
35
|
+
details ? h(Box, { flexDirection: 'column', marginBottom: 1 },
|
|
36
|
+
h(Text, { color: headerColor, bold: true }, isDangerous ? ' DANGEROUS OPERATION ' : ' Confirmation required '),
|
|
37
|
+
h(Text, { color: toolColor }, `[${tool}] ${identifier}`),
|
|
38
|
+
h(Text, { color: '#9aa5ce' }, description)
|
|
39
|
+
) : h(Text, { color: 'white', bold: true }, promptText),
|
|
40
|
+
h(InputShell, { flexDirection: 'row', marginTop: 1, paddingX: 1 },
|
|
41
|
+
h(Text, { color: isDangerous ? '#f7768e' : '#e0af68' }, isDangerous ? 'Type "confirm" to proceed: ' : 'Run this? (y/N): '),
|
|
42
|
+
h(Text, { color: '#c0caf5' }, value || ' ')
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { h } from '../h.js';
|
|
4
|
+
import { buildDiff, collapseContext, langFromPath } from '../diff.js';
|
|
5
|
+
import { highlightCode } from '../../parser/markdown.js';
|
|
6
|
+
|
|
7
|
+
const C = {
|
|
8
|
+
removedBg: '#2d1010',
|
|
9
|
+
removedFg: '#f7768e',
|
|
10
|
+
addedBg: '#1a2f1a',
|
|
11
|
+
addedFg: '#9ece6a',
|
|
12
|
+
ctxFg: '#565f89',
|
|
13
|
+
lineNumFg: '#414868',
|
|
14
|
+
border: '#2a2e3f',
|
|
15
|
+
headerBg: '#1a1b26',
|
|
16
|
+
headerFg: '#7aa2f7',
|
|
17
|
+
dim: '#414868',
|
|
18
|
+
addedLabel: '#9ece6a',
|
|
19
|
+
removedLabel: '#f7768e',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function wrapPlain(line, width) {
|
|
23
|
+
const safeWidth = Math.max(1, width | 0);
|
|
24
|
+
const src = String(line ?? '');
|
|
25
|
+
if (!src.length) return [''];
|
|
26
|
+
const out = [];
|
|
27
|
+
for (let i = 0; i < src.length; i += safeWidth) out.push(src.slice(i, i + safeWidth));
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function lineNumPart(oldN, newN, lnW) {
|
|
32
|
+
const o = oldN ? String(oldN).padStart(lnW) : ' '.repeat(lnW);
|
|
33
|
+
const n = newN ? String(newN).padStart(lnW) : ' '.repeat(lnW);
|
|
34
|
+
return h(Text, { color: C.lineNumFg }, `${o}:${n}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function DiffLine({ hunk, lnW, innerW, lang }) {
|
|
38
|
+
const oldNum = hunk.oldN || null;
|
|
39
|
+
const newNum = hunk.newN || null;
|
|
40
|
+
const chunks = wrapPlain(hunk.line, innerW);
|
|
41
|
+
|
|
42
|
+
if (hunk.type === 'ctx') {
|
|
43
|
+
return h(Box, { flexDirection: 'column' },
|
|
44
|
+
...chunks.map((chunk, i) =>
|
|
45
|
+
h(Box, { key: `ctx_${i}`, paddingLeft: 1 },
|
|
46
|
+
lineNumPart(i === 0 ? oldNum : null, i === 0 ? newNum : null, lnW),
|
|
47
|
+
h(Text, { color: C.ctxFg }, ' ' + chunk)
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (hunk.type === 'del') {
|
|
54
|
+
return h(Box, { flexDirection: 'column' },
|
|
55
|
+
...chunks.map((chunk, i) => {
|
|
56
|
+
const highlighted = highlightCode(chunk, lang);
|
|
57
|
+
return h(Box, { key: `del_${i}`, paddingLeft: 1 },
|
|
58
|
+
lineNumPart(i === 0 ? oldNum : null, i === 0 ? newNum : null, lnW),
|
|
59
|
+
h(Box, { backgroundColor: C.removedBg },
|
|
60
|
+
h(Text, { color: C.removedFg }, i === 0 ? '-' : ' '),
|
|
61
|
+
h(Text, { ansi: true }, highlighted)
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (hunk.type === 'add') {
|
|
69
|
+
return h(Box, { flexDirection: 'column' },
|
|
70
|
+
...chunks.map((chunk, i) => {
|
|
71
|
+
const highlighted = highlightCode(chunk, lang);
|
|
72
|
+
return h(Box, { key: `add_${i}`, paddingLeft: 1 },
|
|
73
|
+
lineNumPart(i === 0 ? oldNum : null, i === 0 ? newNum : null, lnW),
|
|
74
|
+
h(Box, { backgroundColor: C.addedBg },
|
|
75
|
+
h(Text, { color: C.addedFg }, i === 0 ? '+' : ' '),
|
|
76
|
+
h(Text, { ansi: true }, highlighted)
|
|
77
|
+
)
|
|
78
|
+
);
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getInnerWidth(lnW) {
|
|
87
|
+
const cols = process.stdout?.columns || 120;
|
|
88
|
+
return Math.max(24, cols - (lnW * 2 + 12));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function SkipLine({ count }) {
|
|
92
|
+
const msg = ` ··· ${count} unchanged line${count > 1 ? 's' : ''}`;
|
|
93
|
+
return h(Box, { paddingLeft: 2 },
|
|
94
|
+
h(Text, { color: C.dim }, msg)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function EditFileDiff({ filePath, find, replace }) {
|
|
99
|
+
if (!find && !replace) return null;
|
|
100
|
+
const hunks = buildDiff(find || '', replace || '');
|
|
101
|
+
const collapsed = collapseContext(hunks, 3);
|
|
102
|
+
const allLines = [...hunks.filter(h => h.oldN || h.newN)];
|
|
103
|
+
const maxN = Math.max(...allLines.map(h => Math.max(h.oldN || 0, h.newN || 0)), 1);
|
|
104
|
+
const lnW = String(maxN).length;
|
|
105
|
+
const innerW = getInnerWidth(lnW);
|
|
106
|
+
const lang = langFromPath(filePath);
|
|
107
|
+
const dels = hunks.filter(h => h.type === 'del').length;
|
|
108
|
+
const adds = hunks.filter(h => h.type === 'add').length;
|
|
109
|
+
|
|
110
|
+
return h(Box, { flexDirection: 'column', marginY: 1 },
|
|
111
|
+
h(Box, { paddingLeft: 2, paddingY: 0 },
|
|
112
|
+
h(Text, { color: C.headerFg, bold: true }, ' ✎ '),
|
|
113
|
+
h(Text, { color: C.headerFg }, filePath || '')
|
|
114
|
+
),
|
|
115
|
+
h(Box, { flexDirection: 'column', paddingLeft: 2, borderStyle: 'round', borderColor: C.border },
|
|
116
|
+
...collapsed.map((hunk, i) => {
|
|
117
|
+
if (hunk.type === 'skip') return h(SkipLine, { key: i, count: hunk.count });
|
|
118
|
+
return h(DiffLine, { key: i, hunk, lnW, innerW, lang });
|
|
119
|
+
}),
|
|
120
|
+
h(Box, { marginTop: 1 },
|
|
121
|
+
adds > 0 ? h(Text, { color: C.addedLabel }, `+${adds} `) : null,
|
|
122
|
+
dels > 0 ? h(Text, { color: C.removedLabel }, `-${dels}`) : null
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function NewFileDiff({ filePath, content }) {
|
|
129
|
+
if (!content) return null;
|
|
130
|
+
const lines = (content || '').split('\n');
|
|
131
|
+
const lnW = String(lines.length).length;
|
|
132
|
+
const innerW = 60;
|
|
133
|
+
const lang = langFromPath(filePath);
|
|
134
|
+
const shown = Math.min(lines.length, 50);
|
|
135
|
+
|
|
136
|
+
return h(Box, { flexDirection: 'column', marginY: 1 },
|
|
137
|
+
h(Box, { paddingLeft: 2, paddingY: 0 },
|
|
138
|
+
h(Text, { color: C.addedLabel, bold: true }, ' + '),
|
|
139
|
+
h(Text, { color: C.addedLabel }, `${filePath} (new file, ${lines.length} lines)`)
|
|
140
|
+
),
|
|
141
|
+
h(Box, { flexDirection: 'column', paddingLeft: 2, borderStyle: 'round', borderColor: C.border },
|
|
142
|
+
...Array.from({ length: shown }, (_, i) => {
|
|
143
|
+
const raw = lines[i].length > innerW ? lines[i].slice(0, innerW - 1) + '…' : lines[i];
|
|
144
|
+
return h(Box, { key: i, paddingLeft: 1 },
|
|
145
|
+
h(Text, { color: C.lineNumFg }, `${String(i + 1).padStart(lnW)} `),
|
|
146
|
+
h(Box, { backgroundColor: C.addedBg },
|
|
147
|
+
h(Text, { color: C.addedFg }, '+ '),
|
|
148
|
+
h(Text, { ansi: true }, highlightCode(raw, lang))
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
}),
|
|
152
|
+
lines.length > 50 ? h(Box, { paddingLeft: 2 },
|
|
153
|
+
h(Text, { color: C.dim }, ` ··· ${lines.length - 50} more lines`)
|
|
154
|
+
) : null
|
|
155
|
+
)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function AppendFileDiff({ filePath, content }) {
|
|
160
|
+
if (!content) return null;
|
|
161
|
+
const lines = (content || '').split('\n');
|
|
162
|
+
const lnW = String(lines.length).length;
|
|
163
|
+
const innerW = 60;
|
|
164
|
+
const lang = langFromPath(filePath);
|
|
165
|
+
const skip = lines.length > 30;
|
|
166
|
+
const shown = skip ? lines.slice(0, 15) : lines;
|
|
167
|
+
|
|
168
|
+
return h(Box, { flexDirection: 'column', marginY: 1 },
|
|
169
|
+
h(Box, { paddingLeft: 2, paddingY: 0 },
|
|
170
|
+
h(Text, { color: C.addedLabel, bold: true }, ' + '),
|
|
171
|
+
h(Text, { color: C.addedLabel }, `${filePath} (appending ${lines.length} line${lines.length > 1 ? 's' : ''})`)
|
|
172
|
+
),
|
|
173
|
+
h(Box, { flexDirection: 'column', paddingLeft: 2, borderStyle: 'round', borderColor: C.border },
|
|
174
|
+
...shown.map((line, i) => {
|
|
175
|
+
const raw = line.length > innerW ? line.slice(0, innerW - 1) + '…' : line;
|
|
176
|
+
return h(Box, { key: i, paddingLeft: 1 },
|
|
177
|
+
h(Text, { color: C.lineNumFg }, `${String(i + 1).padStart(lnW)} `),
|
|
178
|
+
h(Box, { backgroundColor: C.addedBg },
|
|
179
|
+
h(Text, { color: C.addedFg }, '+ '),
|
|
180
|
+
h(Text, { ansi: true }, highlightCode(raw, lang))
|
|
181
|
+
)
|
|
182
|
+
);
|
|
183
|
+
}),
|
|
184
|
+
skip ? h(Box, { paddingLeft: 2 },
|
|
185
|
+
h(Text, { color: C.dim }, ` ··· ${lines.length - 15} more lines`)
|
|
186
|
+
) : null
|
|
187
|
+
)
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function DeleteFileDiff({ filePath }) {
|
|
192
|
+
return h(Box, { flexDirection: 'column', marginY: 1 },
|
|
193
|
+
h(Box, { paddingLeft: 2, paddingY: 0 },
|
|
194
|
+
h(Text, { color: C.removedLabel, bold: true }, ' ✗ '),
|
|
195
|
+
h(Text, { color: C.removedLabel }, `${filePath} (deleted)`)
|
|
196
|
+
),
|
|
197
|
+
h(Box, { paddingLeft: 2, borderStyle: 'round', borderColor: C.border },
|
|
198
|
+
h(Text, { color: C.removedLabel }, ' File deleted')
|
|
199
|
+
)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Box, Text } from 'ink';
|
|
2
|
+
import { h } from '../h.js';
|
|
3
|
+
import { ENABLE_UI_ANIMATIONS, useAnimationFrame } from '../animation.js';
|
|
4
|
+
|
|
5
|
+
const chunkLength = (chunks) => chunks.reduce((sum, chunk) => sum + String(chunk.text || '').length, 0);
|
|
6
|
+
|
|
7
|
+
const makeItem = (chunks) => ({ chunks, width: chunkLength(chunks) });
|
|
8
|
+
|
|
9
|
+
function truncateChunks(chunks, maxWidth) {
|
|
10
|
+
if (chunkLength(chunks) <= maxWidth) return chunks;
|
|
11
|
+
if (maxWidth <= 1) return [{ text: '…', color: chunks[0]?.color || 'white' }];
|
|
12
|
+
|
|
13
|
+
const out = [];
|
|
14
|
+
let remaining = maxWidth;
|
|
15
|
+
for (const chunk of chunks) {
|
|
16
|
+
if (remaining <= 0) break;
|
|
17
|
+
const text = String(chunk.text || '');
|
|
18
|
+
if (text.length <= remaining) {
|
|
19
|
+
out.push(chunk);
|
|
20
|
+
remaining -= text.length;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
out.push({ ...chunk, text: text.slice(0, Math.max(0, remaining - 1)) + '…' });
|
|
24
|
+
remaining = 0;
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function wrapItems(items, width) {
|
|
30
|
+
const safeWidth = Math.max(16, Number(width) || 80);
|
|
31
|
+
const lines = [];
|
|
32
|
+
let current = [];
|
|
33
|
+
let currentWidth = 0;
|
|
34
|
+
|
|
35
|
+
for (const item of items) {
|
|
36
|
+
const safeItem = item.width > safeWidth
|
|
37
|
+
? makeItem(truncateChunks(item.chunks, safeWidth))
|
|
38
|
+
: item;
|
|
39
|
+
|
|
40
|
+
if (current.length > 0 && currentWidth + safeItem.width > safeWidth) {
|
|
41
|
+
lines.push(current);
|
|
42
|
+
current = [];
|
|
43
|
+
currentWidth = 0;
|
|
44
|
+
}
|
|
45
|
+
current.push(safeItem);
|
|
46
|
+
currentWidth += safeItem.width;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (current.length > 0) lines.push(current);
|
|
50
|
+
return lines;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getHeaderRows(columns = 80) {
|
|
54
|
+
const contentWidth = Math.max(16, (columns || 80) - 2);
|
|
55
|
+
const estimatedWidth = 104;
|
|
56
|
+
return Math.ceil(estimatedWidth / contentWidth) + 2;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function Header({ mode, device, isConnected, isLocal, provider, executionMode, tokenCount, subagentActive, columns }) {
|
|
60
|
+
const frame = useAnimationFrame(!!subagentActive);
|
|
61
|
+
const providerType = provider?.type || 'osai';
|
|
62
|
+
const providerModel = provider?.model || null;
|
|
63
|
+
const isDefault = providerType === 'osai' || !providerType;
|
|
64
|
+
const subagentDim = ENABLE_UI_ANIMATIONS && subagentActive ? frame % 2 === 1 : false;
|
|
65
|
+
const contentWidth = Math.max(16, (columns || 80) - 2);
|
|
66
|
+
|
|
67
|
+
const tokenLabel = tokenCount != null
|
|
68
|
+
? tokenCount >= 1000
|
|
69
|
+
? `~${(tokenCount / 1000).toFixed(1)}k tokens`
|
|
70
|
+
: `~${tokenCount} tokens`
|
|
71
|
+
: null;
|
|
72
|
+
|
|
73
|
+
const statusChunks = isLocal
|
|
74
|
+
? [{ text: '\u25CF Local', color: '#73daca', bold: true }]
|
|
75
|
+
: [{ text: isConnected ? '\u25CF Connected' : '\u25CB Disconnected', color: isConnected ? '#9ece6a' : '#f7768e', bold: true }];
|
|
76
|
+
|
|
77
|
+
const items = [
|
|
78
|
+
makeItem([
|
|
79
|
+
{ text: '\u2590', color: '#7aa2f7' },
|
|
80
|
+
{ text: '\u25B3', color: '#4a9eff', bold: true },
|
|
81
|
+
{ text: '\u258C ', color: '#7aa2f7' },
|
|
82
|
+
{ text: 'OS AI AGENT ', color: '#c0caf5', bold: true },
|
|
83
|
+
]),
|
|
84
|
+
makeItem([
|
|
85
|
+
{ text: '\u2502 ', color: '#3b4261' },
|
|
86
|
+
{ text: 'Provider: ', color: '#565f89' },
|
|
87
|
+
{
|
|
88
|
+
text: isDefault ? (isLocal ? 'not configured ' : 'osai/auto ') : `${providerType}${providerModel ? '/' + providerModel : ''} `,
|
|
89
|
+
color: isDefault ? '#565f89' : '#9ece6a',
|
|
90
|
+
bold: !isDefault,
|
|
91
|
+
},
|
|
92
|
+
]),
|
|
93
|
+
makeItem([
|
|
94
|
+
{ text: '\u2502 ', color: '#3b4261' },
|
|
95
|
+
{ text: 'Mode: ', color: '#565f89' },
|
|
96
|
+
{ text: `${mode}/${executionMode || 'EXEC'} `, color: '#c0caf5' },
|
|
97
|
+
]),
|
|
98
|
+
makeItem([
|
|
99
|
+
{ text: '\u2502 ', color: '#3b4261' },
|
|
100
|
+
{ text: 'Device: ', color: '#565f89' },
|
|
101
|
+
{ text: `${device || 'local'} `, color: '#c0caf5' },
|
|
102
|
+
]),
|
|
103
|
+
makeItem([
|
|
104
|
+
{ text: '\u2502 ', color: '#3b4261' },
|
|
105
|
+
...statusChunks,
|
|
106
|
+
]),
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
if (subagentActive) {
|
|
110
|
+
items.push(makeItem([
|
|
111
|
+
{ text: ' \u2502 ', color: '#3b4261' },
|
|
112
|
+
{ text: '\u25CF Subagent', color: subagentDim ? '#1a5fad' : '#4a9eff', bold: !subagentDim },
|
|
113
|
+
]));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (tokenLabel) {
|
|
117
|
+
items.push(makeItem([
|
|
118
|
+
{ text: ' \u2502 ', color: '#3b4261' },
|
|
119
|
+
{ text: tokenLabel, color: '#e0af68' },
|
|
120
|
+
]));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const lines = wrapItems(items, contentWidth);
|
|
124
|
+
const renderLine = (line, lineIndex) => {
|
|
125
|
+
const indentWidth = lineIndex > 0 ? 2 : 0;
|
|
126
|
+
const lineWidth = chunkLength(line.flatMap((item) => item.chunks)) + indentWidth;
|
|
127
|
+
const padding = ' '.repeat(Math.max(0, contentWidth - lineWidth));
|
|
128
|
+
|
|
129
|
+
return h(Box, { key: `header_line_${lineIndex}`, flexDirection: 'row', width: '100%' },
|
|
130
|
+
h(Text, { color: 'white' }, '│'),
|
|
131
|
+
lineIndex > 0 ? h(Text, { color: '#3b4261' }, ' ') : null,
|
|
132
|
+
line.map((item, itemIndex) =>
|
|
133
|
+
h(Text, { key: `header_item_${lineIndex}_${itemIndex}` },
|
|
134
|
+
item.chunks.map((chunk, chunkIndex) =>
|
|
135
|
+
h(Text, {
|
|
136
|
+
key: `header_chunk_${lineIndex}_${itemIndex}_${chunkIndex}`,
|
|
137
|
+
color: chunk.color,
|
|
138
|
+
bold: chunk.bold,
|
|
139
|
+
}, chunk.text)
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
),
|
|
143
|
+
h(Text, {}, padding),
|
|
144
|
+
h(Text, { color: 'white' }, '│')
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return h(Box, {
|
|
149
|
+
flexShrink: 0,
|
|
150
|
+
width: '100%',
|
|
151
|
+
flexDirection: 'column',
|
|
152
|
+
},
|
|
153
|
+
h(Text, { color: 'white' }, `┌${'─'.repeat(contentWidth)}┐`),
|
|
154
|
+
lines.map(renderLine),
|
|
155
|
+
h(Text, { color: 'white' }, `└${'─'.repeat(contentWidth)}┘`)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import { Box, Text, useInput, useWindowSize } from 'ink';
|
|
3
|
+
import { h } from '../h.js';
|
|
4
|
+
import { InputShell } from './InputShell.js';
|
|
5
|
+
import { isOnlySgrMouseInput } from '../mouse-scroll.js';
|
|
6
|
+
|
|
7
|
+
export function HistoryPicker({ sessions, visible, onSelect, onCancel }) {
|
|
8
|
+
const [cursor, setCursor] = useState(0);
|
|
9
|
+
const [query, setQuery] = useState('');
|
|
10
|
+
|
|
11
|
+
const maxDisplay = 15;
|
|
12
|
+
|
|
13
|
+
const filtered = useMemo(() => {
|
|
14
|
+
if (!query) return sessions.slice(0, maxDisplay);
|
|
15
|
+
const q = query.toLowerCase();
|
|
16
|
+
return sessions
|
|
17
|
+
.filter(s =>
|
|
18
|
+
(s.title || '').toLowerCase().includes(q) ||
|
|
19
|
+
(s.mode || '').toLowerCase().includes(q) ||
|
|
20
|
+
(s.storage || '').toLowerCase().includes(q)
|
|
21
|
+
)
|
|
22
|
+
.slice(0, maxDisplay);
|
|
23
|
+
}, [sessions, query]);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (cursor >= filtered.length) setCursor(Math.max(0, filtered.length - 1));
|
|
27
|
+
}, [filtered.length]);
|
|
28
|
+
|
|
29
|
+
useInput((input, key) => {
|
|
30
|
+
if (!visible) return;
|
|
31
|
+
|
|
32
|
+
if (key.escape) {
|
|
33
|
+
setQuery('');
|
|
34
|
+
setCursor(0);
|
|
35
|
+
onCancel();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (key.upArrow) {
|
|
40
|
+
setCursor(c => (c > 0 ? c - 1 : filtered.length - 1));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (key.downArrow) {
|
|
45
|
+
setCursor(c => (c < filtered.length - 1 ? c + 1 : 0));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (key.return) {
|
|
50
|
+
if (filtered.length > 0) {
|
|
51
|
+
onSelect(filtered[cursor]);
|
|
52
|
+
}
|
|
53
|
+
setQuery('');
|
|
54
|
+
setCursor(0);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (key.backspace || key.delete) {
|
|
59
|
+
setQuery(q => q.slice(0, -1));
|
|
60
|
+
setCursor(0);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (isOnlySgrMouseInput(input)) return;
|
|
65
|
+
if (input && !key.ctrl && !key.meta) {
|
|
66
|
+
setQuery(q => q + input);
|
|
67
|
+
setCursor(0);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!visible) return null;
|
|
72
|
+
|
|
73
|
+
const { rows } = useWindowSize();
|
|
74
|
+
const maxVis = Math.min(filtered.length, Math.max(5, (rows || 24) - 10));
|
|
75
|
+
let startIdx = 0;
|
|
76
|
+
if (filtered.length > maxVis) {
|
|
77
|
+
const half = Math.floor(maxVis / 2);
|
|
78
|
+
startIdx = Math.max(0, Math.min(cursor - half, filtered.length - maxVis));
|
|
79
|
+
}
|
|
80
|
+
const visibleItems = filtered.slice(startIdx, startIdx + maxVis);
|
|
81
|
+
|
|
82
|
+
const separator = '─'.repeat(52);
|
|
83
|
+
|
|
84
|
+
return h(
|
|
85
|
+
Box,
|
|
86
|
+
{ flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
|
|
87
|
+
|
|
88
|
+
h(Box, { flexDirection: 'row', alignItems: 'center' },
|
|
89
|
+
h(Text, { color: '#7aa2f7', bold: true }, ' Resume a saved session'),
|
|
90
|
+
h(Text, { color: '#565f89', dimColor: true }, ` ${filtered.length} sessions`)
|
|
91
|
+
),
|
|
92
|
+
h(Text, { color: '#3b3f52' }, separator),
|
|
93
|
+
|
|
94
|
+
h(InputShell, { flexDirection: 'row', paddingY: 0, marginY: 0 },
|
|
95
|
+
h(Text, { color: '#e0af68' }, ' Search: '),
|
|
96
|
+
h(Text, { color: '#c0caf5' }, query || ' '),
|
|
97
|
+
h(Text, { color: '#565f89' }, query ? '' : ' type to filter sessions')
|
|
98
|
+
),
|
|
99
|
+
h(Text, { color: '#3b3f52' }, separator),
|
|
100
|
+
|
|
101
|
+
...visibleItems.map((s, i) => {
|
|
102
|
+
const realIdx = startIdx + i;
|
|
103
|
+
const isHL = realIdx === cursor;
|
|
104
|
+
const prefix = isHL ? h(Text, { color: '#9ece6a' }, ' ▸ ') : h(Text, { color: '#3b3f52' }, ' ');
|
|
105
|
+
const storageIcon = s.storage === 'cloud' ? '☁ ' : ' ';
|
|
106
|
+
|
|
107
|
+
return h(Box, { key: realIdx },
|
|
108
|
+
prefix,
|
|
109
|
+
h(Text, isHL ? { color: '#ffffff', bold: true, backgroundColor: '#2a3a5c' } : { color: '#9aa5ce' },
|
|
110
|
+
` ${storageIcon}[${(s.storage || 'local').toUpperCase()}] ${s.title || 'Untitled'} - ${s.mode} - ${s.messageCount} msgs - ${new Date(s.savedAt).toLocaleDateString()}`
|
|
111
|
+
)
|
|
112
|
+
);
|
|
113
|
+
}),
|
|
114
|
+
|
|
115
|
+
filtered.length > maxVis
|
|
116
|
+
? h(Text, { color: '#565f89', dimColor: true }, ` Showing ${startIdx + 1}-${startIdx + visibleItems.length} of ${filtered.length}`)
|
|
117
|
+
: null,
|
|
118
|
+
|
|
119
|
+
h(Text, { color: '#3b3f52' }, separator),
|
|
120
|
+
|
|
121
|
+
h(
|
|
122
|
+
Box,
|
|
123
|
+
{ flexDirection: 'row' },
|
|
124
|
+
h(Text, { color: '#9ece6a' }, ' ↑↓'), h(Text, { color: '#565f89' }, ' Navigate'),
|
|
125
|
+
h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Enter'), h(Text, { color: '#565f89' }, ' Resume'),
|
|
126
|
+
h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Esc'), h(Text, { color: '#565f89' }, ' Cancel'),
|
|
127
|
+
h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Type'), h(Text, { color: '#565f89' }, ' Search')
|
|
128
|
+
)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Box } from 'ink';
|
|
2
|
+
import { h } from '../h.js';
|
|
3
|
+
import { INPUT_BACKGROUND } from '../theme.js';
|
|
4
|
+
|
|
5
|
+
export function InputShell({
|
|
6
|
+
children,
|
|
7
|
+
paddingX = 1,
|
|
8
|
+
paddingY = 0,
|
|
9
|
+
width = '100%',
|
|
10
|
+
marginY = 0,
|
|
11
|
+
flexDirection = 'column',
|
|
12
|
+
backgroundColor = INPUT_BACKGROUND,
|
|
13
|
+
}) {
|
|
14
|
+
return h(Box, {
|
|
15
|
+
backgroundColor,
|
|
16
|
+
paddingX,
|
|
17
|
+
paddingY,
|
|
18
|
+
width,
|
|
19
|
+
marginY,
|
|
20
|
+
flexDirection,
|
|
21
|
+
}, children);
|
|
22
|
+
}
|